Add subterrain features

- Generate authored underground rooms, materials, fluids, and formations.
- Preserve underground biome ownership across generation and saved terrain.
- Add subterrain searches and regression coverage.
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-04 09:59:57 -04:00
1 parent be0d3b0156
commit 5858eb366d
76 files changed
+4187 -201

No files matched your search

@@ -507,7 +507,7 @@ public class DecoratorCoreTest {
@Test
public void sulfurStalagmiteKeepsSulfurMaterialAndTapersToOneTip() {
Hunk<NativeBlockState> output = placeSpikeColumn("minecraft:sulfur_spike", 5, true, false);
Hunk<NativeBlockState> output = placeSpikeColumn("minecraft:sulfur_spike", 5, true, false, false);
assertSpikeColumn(output, "minecraft:sulfur_spike", true, false,
"base", "middle", "middle", "frustum", "tip");
@@ -515,7 +515,7 @@ public class DecoratorCoreTest {
@Test
public void sulfurStalactiteKeepsWaterloggingAndPointsDown() {
Hunk<NativeBlockState> output = placeSpikeColumn("minecraft:sulfur_spike", 5, false, true);
Hunk<NativeBlockState> output = placeSpikeColumn("minecraft:sulfur_spike", 5, false, true, true);
assertSpikeColumn(output, "minecraft:sulfur_spike", false, true,
"base", "middle", "middle", "frustum", "tip");
@@ -524,17 +524,26 @@ public class DecoratorCoreTest {
@Test
public void shortSulfurSpikesHaveCorrectTipsInBothDirections() {
for (boolean upward : new boolean[]{true, false}) {
Hunk<NativeBlockState> single = placeSpikeColumn("minecraft:sulfur_spike", 1, upward, false);
Hunk<NativeBlockState> pair = placeSpikeColumn("minecraft:sulfur_spike", 2, upward, false);
Hunk<NativeBlockState> single = placeSpikeColumn("minecraft:sulfur_spike", 1, upward, false, false);
Hunk<NativeBlockState> pair = placeSpikeColumn("minecraft:sulfur_spike", 2, upward, false, false);
assertSpikeColumn(single, "minecraft:sulfur_spike", upward, false, "tip");
assertSpikeColumn(pair, "minecraft:sulfur_spike", upward, false, "frustum", "tip");
}
}
@Test
public void authoredWaterloggedSpikesBecomeDryInAirInBothDirections() {
for (boolean upward : new boolean[]{true, false}) {
Hunk<NativeBlockState> output = placeSpikeColumn("minecraft:pointed_dripstone", 4, upward, true, false);
assertSpikeColumn(output, "minecraft:pointed_dripstone", upward, false,
"base", "middle", "frustum", "tip");
}
}
@Test
public void pointedDripstoneRetainsWaterloggingWhenStacked() {
Hunk<NativeBlockState> output = placeSpikeColumn("minecraft:pointed_dripstone", 4, true, true);
Hunk<NativeBlockState> output = placeSpikeColumn("minecraft:pointed_dripstone", 4, true, true, true);
assertSpikeColumn(output, "minecraft:pointed_dripstone", true, true,
"base", "middle", "frustum", "tip");
@@ -738,7 +747,7 @@ public class DecoratorCoreTest {
return air;
}
private Hunk<NativeBlockState> placeSpikeColumn(String material, int height, boolean upward, boolean waterlogged) {
private Hunk<NativeBlockState> placeSpikeColumn(String material, int height, boolean upward, boolean waterlogged, boolean submerged) {
IrisDecorator decorator = mock(IrisDecorator.class);
IrisData data = mock(IrisData.class);
NativeBlockState spike = spikeState(material, Map.of(
@@ -750,12 +759,21 @@ public class DecoratorCoreTest {
when(decorator.pickBlockDataTop(any(RNG.class), eq(data), anyDouble(), anyDouble())).thenReturn(spike);
Hunk<NativeBlockState> output = Hunk.newArrayHunk(1, height + 2, 1);
NativeBlockState target = airState();
if (submerged) {
target = mock(NativeBlockState.class);
when(target.key()).thenReturn("minecraft:water");
when(target.isWater()).thenReturn(true);
when(target.isFluid()).thenReturn(true);
}
for (int y = 1; y <= height; y++) {
output.set(0, y, 0, airState());
output.set(0, y, 0, target);
}
NativeBlockState support = sturdyState();
output.set(0, upward ? 0 : height + 1, 0, support);
DecoratorCore.PlaceOpts opts = new DecoratorCore.PlaceOpts();
opts.underwater = submerged;
opts.fluidHeight = height;
if (upward) {
DecoratorCore.placeStackUp(decorator, 0, 0, 0, 0, 0, height, output, new RNG(1L), data, opts);
} else {
@@ -0,0 +1,44 @@
package art.arcane.iris.generation.decoration;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.hunk.Hunk;
import org.junit.Test;
import static org.junit.Assert.assertSame;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisPlacementWaterloggingTest {
@Test
public void authoredWetStateIsClearedForDryAndLavaTargets() {
NativeBlockState wet = state("minecraft:oak_slab[type=bottom,waterlogged=true]");
NativeBlockState dry = state("minecraft:oak_slab[type=bottom,waterlogged=false]");
when(wet.withProperty("waterlogged", "false")).thenReturn(dry);
assertSame(dry, IrisProceduralBlocks.normalizeWaterlogging(wet, state("minecraft:cave_air"), false));
assertSame(dry, IrisProceduralBlocks.normalizeWaterlogging(wet, state("minecraft:lava"), true));
}
@Test
public void waterloggedDecorationRequiresActualWaterAtItsOwnVoxel() {
NativeBlockState dry = state("minecraft:oak_slab[type=bottom,waterlogged=false]");
NativeBlockState wet = state("minecraft:oak_slab[type=bottom,waterlogged=true]");
NativeBlockState water = state("minecraft:water");
when(water.isWater()).thenReturn(true);
when(dry.withProperty("waterlogged", "true")).thenReturn(wet);
assertSame(wet, IrisProceduralBlocks.normalizeWaterlogging(dry, water, true));
assertSame(dry, IrisProceduralBlocks.normalizeWaterlogging(dry, water, false));
assertSame(wet, IrisProceduralBlocks.normalizeWaterlogging(wet, water, false));
Hunk<NativeBlockState> hunk = Hunk.newArrayHunk(16, 64, 16);
hunk.set(3, 28, 5, water);
hunk.set(3, 29, 5, state("minecraft:cave_air"));
when(wet.withProperty("waterlogged", "false")).thenReturn(dry);
assertSame(wet, DecoratorCore.fixFacesForHunk(dry, hunk, 3, 5, 3, 28, 5, null));
assertSame(dry, DecoratorCore.fixFacesForHunk(wet, hunk, 3, 5, 3, 29, 5, null));
}
private NativeBlockState state(String key) {
NativeBlockState state = mock(NativeBlockState.class);
when(state.key()).thenReturn(key);
return state;
}
}
@@ -0,0 +1,62 @@
package art.arcane.iris.generation.decoration.formation;
import art.arcane.iris.generation.block.B;
import art.arcane.iris.generation.geometry.IrisBlockVector;
import art.arcane.iris.generation.subterrain.IrisSubterrainFamily;
import art.arcane.iris.generation.subterrain.SubterrainCell;
import art.arcane.iris.generation.subterrain.SubterrainRoom;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.structure.object.IrisObject;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
public class ContextualFormationTest {
@Test
public void roomVaultSizesFormationAfterAnchorWithoutUsingOrdinaryHeightRange() {
IrisFormation formation = new IrisFormation().setName("vault-spire").setRoomHeightFraction(0.5)
.setHeightMin(80).setHeightMax(80).setBaseWidthMin(2).setBaseWidthMax(2)
.setRoughness(0).setLean(0);
NativeBlockState rock = mock(NativeBlockState.class);
when(rock.key()).thenReturn("minecraft:stone");
try (MockedStatic<B> blocks = mockStatic(B.class)) {
blocks.when(() -> B.getStateOrNull("minecraft:stone", false)).thenReturn(rock);
IrisData data = mock(IrisData.class);
IrisObject shortRoom = formation.getVariantObject(data, new RNG(143L), room(12));
IrisObject tallRoom = formation.getVariantObject(data, new RNG(143L), room(40));
IrisObject repeat = formation.getVariantObject(data, new RNG(143L), room(40));
assertNotNull(shortRoom);
assertNotNull(tallRoom);
assertTrue(shortRoom.getH() <= 12);
assertTrue(tallRoom.getH() <= 40);
assertTrue(tallRoom.getH() > shortRoom.getH());
assertNotNull(repeat);
assertEquals(tallRoom.getW(), repeat.getW());
assertEquals(tallRoom.getH(), repeat.getH());
assertEquals(tallRoom.getD(), repeat.getD());
assertEquals(tallRoom.getBlocks().size(), repeat.getBlocks().size());
for (Map.Entry<IrisBlockVector, NativeBlockState> entry : tallRoom.getBlocks()) {
NativeBlockState repeated = repeat.getBlocks().get(entry.getKey());
assertNotNull(repeated);
assertEquals(entry.getValue().key(), repeated.key());
}
assertEquals("procedural/vault-spire/cenote-room", tallRoom.getLoadKey());
}
}
private SubterrainRoom room(int vaultHeight) {
return new SubterrainRoom("cenote-room", "cave-biome", IrisSubterrainFamily.CENOTE,
0, 20, 0, 0, 12, 0, 10, 11 + vaultHeight, 20, 10,
false, false, SubterrainCell.Kind.AIR);
}
}
@@ -0,0 +1,74 @@
package art.arcane.iris.generation.locator;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.subterrain.IrisSubterrainFamily;
import art.arcane.iris.generation.subterrain.IrisSubterrainFeature;
import art.arcane.iris.generation.subterrain.SubterrainCell;
import art.arcane.iris.generation.subterrain.SubterrainPlanner;
import art.arcane.iris.pack.value.IrisRange;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
public class SubterrainLocatorTest {
@Test
public void wideDistanceSearchReturnsOwnedAbsolutePositionWithoutGenerating() {
SubterrainPlanner planner = planner();
Engine engine = engine(planner);
SubterrainLocator.Result result = SubterrainLocator.nearest(engine,
new SubterrainLocator.Query("basin", null, "wet-cave"), 100_000, -24, -100_000, 8192).orElseThrow();
SubterrainCell sampled = planner.sample(result.x(), result.y(), result.z());
assertTrue(sampled.occupied());
assertEquals(result.featureId(), sampled.room().featureId());
assertEquals("wet-cave", result.biome());
assertTrue(result.y() >= -48 && result.y() <= 48);
verify(engine).getComplex();
verifyNoMoreInteractions(engine);
}
@Test
public void filtersAndDistanceBoundsRejectUnavailableOwnership() {
Engine engine = engine(planner());
assertFalse(SubterrainLocator.nearest(engine,
new SubterrainLocator.Query("missing", null, ""), 0, 0, 0, 1024).isPresent());
assertFalse(SubterrainLocator.nearest(engine,
new SubterrainLocator.Query("", IrisSubterrainFamily.LAVA_TUBE, ""), 0, 0, 0, 1024).isPresent());
assertFalse(SubterrainLocator.nearest(engine,
new SubterrainLocator.Query("", null, "missing"), 0, 0, 0, 1024).isPresent());
assertFalse(SubterrainLocator.nearest(engine,
new SubterrainLocator.Query("", null, ""), 0, 10_000, 0, 1024).isPresent());
assertFalse(SubterrainLocator.nearest(engine,
new SubterrainLocator.Query("", null, ""), 0, 0, 0, 1024, ignored -> false).isPresent());
}
@Test
public void repeatedSearchAndLargeNegativeCoordinatesAreStable() {
Engine engine = engine(planner());
SubterrainLocator.Query query = new SubterrainLocator.Query("", null, "wet-cave");
assertEquals(SubterrainLocator.nearest(engine, query, -1_500_000, -10, -2_000_000, 4096),
SubterrainLocator.nearest(engine, query, -1_500_000, -10, -2_000_000, 4096));
}
private static Engine engine(SubterrainPlanner planner) {
Engine engine = mock(Engine.class);
IrisComplex complex = mock(IrisComplex.class);
when(engine.getComplex()).thenReturn(complex);
when(complex.getSubterrainPlanner()).thenReturn(planner);
return engine;
}
private static SubterrainPlanner planner() {
IrisSubterrainFeature feature = new IrisSubterrainFeature().setId("basin").setBiome("wet-cave")
.setProbability(1).setWorldYRange(new IrisRange(-48, 48));
return new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 74119L, -64, 320));
}
}
@@ -19,6 +19,9 @@
package art.arcane.iris.generation.mantle;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.subterrain.SubterrainCell;
import art.arcane.iris.generation.subterrain.SubterrainRoom;
import art.arcane.iris.generation.subterrain.IrisSubterrainFamily;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.structure.object.IObjectPlacer;
import art.arcane.iris.generation.block.TileData;
@@ -112,6 +115,74 @@ public class CaveObjectPlacementTransactionTest {
assertEquals(61, MantleObjectComponent.caveAnchorScanUpperBound(128, 80, 20));
}
@Test
public void ownedDryAndWetRoomDecorationCanCommitWithoutWeakeningSolidGuards() {
for (SubterrainCell.Kind kind : new SubterrainCell.Kind[]{SubterrainCell.Kind.AIR, SubterrainCell.Kind.WATER}) {
IObjectPlacer delegate = createPlacer(128, 80, 20, 60);
SubterrainRoom room = new SubterrainRoom("cenote", "wet-cave", IrisSubterrainFamily.CENOTE,
4, 35, 7, 4, 21, 7, 19, 61, 8, 34, false, false, kind);
when(delegate.getEngine().getSubterrainCell(4, 30, 7))
.thenReturn(new SubterrainCell(kind, kind == SubterrainCell.Kind.WATER ? "minecraft:water" : "minecraft:cave_air", room));
NativeBlockState decoration = mock(NativeBlockState.class);
CaveObjectPlacementTransaction transaction = new CaveObjectPlacementTransaction(delegate, 20, 10);
transaction.set(4, 30, 7, decoration);
assertEquals(CaveObjectPlacementTransaction.CommitResult.COMMITTED, transaction.commit());
verify(delegate).set(4, 30, 7, decoration);
}
}
@Test
public void ownedWaterAndLavaUseTheirExclusiveAbsoluteFluidHeadAtTheOriginalAnchor() {
for (int minimumY : new int[]{-64, 32}) {
for (SubterrainCell.Kind kind : new SubterrainCell.Kind[]{SubterrainCell.Kind.WATER, SubterrainCell.Kind.LAVA}) {
IObjectPlacer delegate = createPlacer(256, 220, 120, 160);
Engine engine = delegate.getEngine();
when(engine.getMinHeight()).thenReturn(minimumY);
when(delegate.getFluidHeight(4, 7)).thenReturn(16);
SubterrainRoom room = new SubterrainRoom("raised-room", "wet-cave", IrisSubterrainFamily.CENOTE,
4, minimumY + 130, 7, 4, minimumY + 121, 7, minimumY + 119,
minimumY + 161, 8, minimumY + 126, false, false, kind);
when(engine.getSubterrainCell(4, 123, 7)).thenReturn(new SubterrainCell(kind,
kind == SubterrainCell.Kind.WATER ? "minecraft:water" : "minecraft:lava", room));
CaveObjectPlacementTransaction transaction = new CaveObjectPlacementTransaction(delegate, 123, 10);
assertEquals(127, transaction.getFluidHeight(4, 7));
verify(engine).getSubterrainCell(4, 123, 7);
verify(delegate, never()).getFluidHeight(4, 7);
}
}
}
@Test
public void nonFluidAnchorsPreserveTheDelegateCoordinateFluidHeight() {
for (SubterrainCell.Kind kind : new SubterrainCell.Kind[]{SubterrainCell.Kind.AIR, SubterrainCell.Kind.SOLID, SubterrainCell.Kind.OUTSIDE}) {
IObjectPlacer delegate = createPlacer(128, 80, 20, 60);
when(delegate.getFluidHeight(4, 7)).thenReturn(51);
SubterrainRoom room = new SubterrainRoom("dry-room", "dry-cave", IrisSubterrainFamily.CENOTE,
4, 35, 7, 4, 21, 7, 19, 61, 8, 24, false, false, kind);
SubterrainCell cell = kind == SubterrainCell.Kind.OUTSIDE ? SubterrainCell.OUTSIDE
: new SubterrainCell(kind, kind == SubterrainCell.Kind.AIR ? "minecraft:cave_air" : "minecraft:stone", room);
when(delegate.getEngine().getSubterrainCell(4, 20, 7)).thenReturn(cell);
CaveObjectPlacementTransaction transaction = new CaveObjectPlacementTransaction(delegate, 20, 10);
assertEquals(51, transaction.getFluidHeight(4, 7));
verify(delegate).getFluidHeight(4, 7);
}
}
@Test
public void featurePillarRejectsAllObjectMutations() {
IObjectPlacer delegate = createPlacer(128, 80, 20, 60);
SubterrainRoom room = new SubterrainRoom("cenote", "wet-cave", IrisSubterrainFamily.CENOTE,
4, 35, 7, 4, 21, 7, 19, 61, 8, 24, false, true, SubterrainCell.Kind.SOLID);
when(delegate.getEngine().getSubterrainCell(4, 30, 7))
.thenReturn(new SubterrainCell(SubterrainCell.Kind.SOLID, "minecraft:stone", room));
CaveObjectPlacementTransaction transaction = new CaveObjectPlacementTransaction(delegate, 20, 10);
transaction.set(4, 30, 7, mock(NativeBlockState.class));
transaction.setData(5, 30, 7, "decoration");
assertEquals(CaveObjectPlacementTransaction.CommitResult.REJECTED_SUBTERRAIN, transaction.commit());
verify(delegate, never()).set(anyInt(), anyInt(), anyInt(), any());
verify(delegate, never()).setData(anyInt(), anyInt(), anyInt(), any());
}
@Test
public void dryHydrologyHeadroomRejectsTheWholePlacement() {
IObjectPlacer delegate = createPlacer(128, 80, 20, 90);
@@ -1,13 +1,21 @@
package art.arcane.iris.generation.mantle;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.subterrain.IrisSubterrainFamily;
import art.arcane.iris.generation.subterrain.SubterrainCell;
import art.arcane.iris.generation.subterrain.SubterrainRoom;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveAction;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
import art.arcane.volmlib.util.matter.MatterCavern;
import org.junit.Test;
import java.lang.reflect.Method;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class MantleObjectComponentCaveAnchorTest {
@Test
@@ -37,8 +45,10 @@ public class MantleObjectComponentCaveAnchorTest {
public void explicitWetAndForcedAirPlacementsRemainEligible() {
MatterCavern water = new MatterCavern(true, "", (byte) 1);
MatterCavern forcedAir = new MatterCavern(true, "", (byte) 3);
MatterCavern lava = new MatterCavern(true, "", (byte) 2);
assertTrue(MantleObjectComponent.acceptsCaveAnchorFluid(true, water, 20, 8));
assertTrue(MantleObjectComponent.acceptsCaveAnchorFluid(true, lava, 20, 8));
assertTrue(MantleObjectComponent.acceptsCaveAnchorFluid(false, forcedAir, 0, 8));
assertFalse(MantleObjectComponent.acceptsCaveAnchorFluid(
true,
@@ -63,6 +73,56 @@ public class MantleObjectComponentCaveAnchorTest {
false, forcedAir, HydrologyCaveCell.of(HydrologyCaveAction.DRY_AIR), 20, 8));
}
@Test
public void authoredWetAnchorsAcceptWaterAndLavaWhileDryAnchorsRequireAir() throws Exception {
for (SubterrainCell.Kind kind : new SubterrainCell.Kind[]{SubterrainCell.Kind.WATER, SubterrainCell.Kind.LAVA}) {
SubterrainCell cell = featureCell(kind, false);
assertTrue(acceptsAuthoredAnchor(cell, true));
assertFalse(acceptsAuthoredAnchor(cell, false));
}
SubterrainCell air = featureCell(SubterrainCell.Kind.AIR, false);
assertTrue(acceptsAuthoredAnchor(air, false));
assertFalse(acceptsAuthoredAnchor(air, true));
}
@Test
public void authoredWetAnchorsPreserveReservedPassageAndSolidGuards() throws Exception {
for (SubterrainCell.Kind kind : new SubterrainCell.Kind[]{SubterrainCell.Kind.AIR, SubterrainCell.Kind.WATER, SubterrainCell.Kind.LAVA}) {
SubterrainCell reserved = featureCell(kind, true);
assertFalse(acceptsAuthoredAnchor(reserved, false));
assertFalse(acceptsAuthoredAnchor(reserved, true));
}
SubterrainCell solid = featureCell(SubterrainCell.Kind.SOLID, false);
assertFalse(acceptsAuthoredAnchor(solid, false));
assertFalse(acceptsAuthoredAnchor(solid, true));
}
private static boolean acceptsAuthoredAnchor(SubterrainCell cell, boolean underwater) throws Exception {
Engine engine = mock(Engine.class);
EngineMantle mantle = mock(EngineMantle.class);
when(mantle.getEngine()).thenReturn(engine);
when(engine.getSubterrainCell(17, 4, -9)).thenReturn(cell);
MantleObjectComponent component = new MantleObjectComponent(mantle);
Method method = MantleObjectComponent.class.getDeclaredMethod("acceptsCaveAnchorAt",
int.class, int.class, int.class, boolean.class, MatterCavern.class, HydrologyCaveCell.class);
method.setAccessible(true);
return (boolean) method.invoke(component, 17, 4, -9, underwater, null, null);
}
private static SubterrainCell featureCell(SubterrainCell.Kind kind, boolean reservedPassage) {
SubterrainRoom room = new SubterrainRoom("basin:0:0", "subterrain/basin", IrisSubterrainFamily.CENOTE,
17, 4, -9, 17, 4, -9, 0, 20, 8, 8,
reservedPassage, kind == SubterrainCell.Kind.SOLID, kind);
String material = switch (kind) {
case AIR -> "minecraft:air";
case WATER -> "minecraft:water";
case LAVA -> "minecraft:lava";
case SOLID -> "minecraft:stone";
case OUTSIDE -> "";
};
return new SubterrainCell(kind, material, room);
}
private static IrisBiome biome(String loadKey) {
IrisBiome biome = new IrisBiome();
biome.setLoadKey(loadKey);
@@ -0,0 +1,279 @@
package art.arcane.iris.generation.mantle;
import art.arcane.iris.generation.cave.CarvingMode;
import art.arcane.iris.generation.decoration.IrisProceduralPlacement;
import art.arcane.iris.generation.decoration.coral.IrisCoral;
import art.arcane.iris.generation.decoration.coral.IrisCoralForm;
import art.arcane.iris.generation.decoration.crystal.IrisCrystal;
import art.arcane.iris.generation.decoration.formation.IrisFormation;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.subterrain.IrisSubterrainFeature;
import art.arcane.iris.generation.subterrain.IrisSubterrainFluid;
import art.arcane.iris.generation.subterrain.SubterrainCell;
import art.arcane.iris.generation.subterrain.SubterrainPlan;
import art.arcane.iris.generation.subterrain.SubterrainPlanner;
import art.arcane.iris.generation.subterrain.SubterrainPosition;
import art.arcane.iris.generation.subterrain.SubterrainRoom;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.pack.value.IrisRange;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.structure.object.IObjectPlacer;
import art.arcane.iris.structure.object.IrisObject;
import art.arcane.iris.structure.object.IrisObjectPlacement;
import art.arcane.iris.structure.object.IrisObjectRotation;
import art.arcane.iris.structure.object.IrisObjectTranslate;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.volmlib.nativelib.terrain.BlockStateKey;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.math.RNG;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class NaturalCaveProceduralPlacementTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
private final Map<String, NativeBlockState> states = new HashMap<>();
@Before
public void bindPlatform() {
IrisPlatforms.unbind();
PlatformRegistries registries = mock(PlatformRegistries.class);
when(registries.block(anyString())).thenAnswer(invocation -> state(invocation.getArgument(0)));
when(registries.blockOrNull(anyString())).thenAnswer(invocation -> state(invocation.getArgument(0)));
when(registries.blockOrNull(anyString(), eq(false))).thenAnswer(invocation -> state(invocation.getArgument(0)));
IrisPlatform platform = mock(IrisPlatform.class);
when(platform.registries()).thenReturn(registries);
IrisPlatforms.bind(platform);
}
@After
public void unbindPlatform() {
IrisPlatforms.unbind();
states.clear();
}
@Test
public void roomProportionalFormationRestsOnActualFloorWithoutTranslation() {
Fixture fixture = fixture(new FixtureOptions(IrisSubterrainFluid.WATER, 0));
IrisFormation formation = new IrisFormation().setName("natural-vault").setRoomHeightFraction(0.3)
.setHeightMin(90).setHeightMax(90).setBaseWidthMin(1).setBaseWidthMax(1)
.setRoughness(0).setLean(0).setJitter(0).setCarvingSupport(CarvingMode.CARVING_ONLY);
IrisObject object = variant(formation, fixture);
assertTrue(object.getH() <= Math.floor(fixture.room().vaultHeight() * 0.3) + 2);
assertTrue(object.getH() > 3);
assertTrue(object.getLoadKey().endsWith(fixture.room().featureId()));
assertFloorPlacement(object, formation.asPlacement().setRotation(IrisObjectRotation.of(0, 0, 0)), fixture);
}
@Test
public void crystalBaseAndRotatedShardsRestAboveProtectedCenoteFloor() {
for (double angle : List.of(0D, 30D)) {
Fixture fixture = fixture(new FixtureOptions(IrisSubterrainFluid.WATER, 0));
IrisCrystal crystal = new IrisCrystal().setName("natural-crystal").setVariants(1)
.setBaseRadius(1.5).setBaseNoise(0).setShardCountMin(3).setShardCountMax(3)
.setShardLengthMin(5).setShardLengthMax(5).setShardBaseRadius(0.5)
.setSpreadAngle(10).setJitter(0);
IrisObject object = variant(crystal, fixture);
assertTrue(object.getH() > 3);
IrisObjectPlacement placement = crystal.asPlacement().setRotation(IrisObjectRotation.of(angle, 0, 0));
assertFloorPlacement(object, placement, fixture);
}
}
@Test
public void submergedCoralUsesOwnedFluidHeadAboveGlobalWaterAndWaterlogsOnlyInWater() {
for (IrisSubterrainFluid fluid : IrisSubterrainFluid.values()) {
Fixture fixture = fixture(new FixtureOptions(fluid, 12));
assertTrue(fixture.room().floorY() > fixture.placer().getFluidHeight(fixture.x(), fixture.z()));
IrisCoral coral = coral();
IrisObject object = variant(coral, fixture);
assertFloorPlacement(object, coral.asPlacement().setRotation(IrisObjectRotation.of(0, 0, 0)), fixture);
SubterrainCell.Kind expected = fluid == IrisSubterrainFluid.WATER ? SubterrainCell.Kind.WATER : SubterrainCell.Kind.LAVA;
for (Map.Entry<SubterrainPosition, NativeBlockState> entry : fixture.writes().entrySet()) {
SubterrainPosition position = entry.getKey();
assertEquals(expected, fixture.planner().sample(position.x(), position.y(), position.z()).kind());
assertEquals(entry.getValue().key(), fluid == IrisSubterrainFluid.WATER, entry.getValue().isWaterLogged());
}
}
}
@Test
public void authoredDownwardTranslationStillRejectsWholeObjectAtProtectedFloor() {
Fixture fixture = fixture(new FixtureOptions(IrisSubterrainFluid.WATER, 0));
IrisCrystal crystal = new IrisCrystal().setVariants(1).setBaseRadius(1)
.setShardCountMin(1).setShardCountMax(1).setShardLengthMin(4).setShardLengthMax(4)
.setSpreadAngle(0).setJitter(0);
IrisObject object = variant(crystal, fixture);
IrisObjectPlacement placement = crystal.asPlacement().setRotation(IrisObjectRotation.of(0, 0, 0))
.setTranslate(new IrisObjectTranslate().setY(-1));
CaveObjectPlacementTransaction transaction = transaction(fixture);
int firstAirY = MantleObjectComponent.proceduralFloorY(fixture.placer(), fixture.x(), fixture.floorY() + 3, fixture.z());
int result = object.placeOnFloor(fixture.x(), firstAirY, fixture.z(), transaction, placement, new RNG(17L),
(position, block) -> transaction.setData(position.getX(), position.getY(), position.getZ(), "natural-crystal@1"), fixture.data());
assertTrue(result >= 0);
assertEquals(CaveObjectPlacementTransaction.CommitResult.REJECTED_SUBTERRAIN, transaction.commit());
assertTrue(fixture.writes().isEmpty());
assertTrue(fixture.metadata().isEmpty());
assertTrue(fixture.placer().get(fixture.x(), fixture.floorY(), fixture.z()).isSolid());
}
private void assertFloorPlacement(IrisObject object, IrisObjectPlacement placement, Fixture fixture) {
assertFalse(placement.isBottom());
assertEquals(0, placement.getTranslate().getY());
CaveObjectPlacementTransaction transaction = transaction(fixture);
int firstAirY = MantleObjectComponent.proceduralFloorY(fixture.placer(), fixture.x(), fixture.floorY() + 3, fixture.z());
assertEquals(fixture.floorY() + 1, firstAirY);
int result = object.placeOnFloor(fixture.x(), firstAirY, fixture.z(), transaction, placement, new RNG(17L),
(position, block) -> transaction.setData(position.getX(), position.getY(), position.getZ(), "natural@1"), fixture.data());
assertTrue(result >= 0);
assertTrue(fixture.writes().isEmpty());
assertTrue(fixture.metadata().isEmpty());
assertEquals(CaveObjectPlacementTransaction.CommitResult.COMMITTED, transaction.commit());
assertFalse(fixture.writes().isEmpty());
int lowest = Integer.MAX_VALUE;
for (SubterrainPosition position : fixture.writes().keySet()) {
lowest = Math.min(lowest, position.y());
SubterrainCell cell = fixture.planner().sample(position.x(), position.y(), position.z());
assertTrue(cell.occupied());
assertFalse(cell.room().reservedPassage());
assertTrue(position.y() > cell.room().floorY());
assertTrue(position.y() < cell.room().ceilingY());
}
assertEquals(fixture.floorY() + 1, lowest);
boolean supported = false;
for (SubterrainPosition position : fixture.writes().keySet()) {
if (position.y() == lowest) {
assertTrue(fixture.placer().get(position.x(), lowest - 1, position.z()).isSolid());
assertFalse(fixture.writes().containsKey(new SubterrainPosition(position.x(), lowest - 1, position.z())));
supported = true;
}
}
assertTrue(supported);
assertFalse(fixture.metadata().isEmpty());
assertFalse(placement.isBottom());
}
private CaveObjectPlacementTransaction transaction(Fixture fixture) {
return new CaveObjectPlacementTransaction(fixture.placer(), fixture.floorY() + 3, 6);
}
private IrisObject variant(IrisProceduralPlacement procedural, Fixture fixture) {
IrisObject object = procedural.getVariantObject(fixture.data(), new RNG(819L), fixture.room());
assertNotNull(object);
assertFalse(object.getBlocks().isEmpty());
return object;
}
private IrisCoral coral() {
return new IrisCoral().setName("natural-coral").setVariants(1).setForm(IrisCoralForm.PILLAR)
.setHeightMin(6).setHeightMax(6).setPillarRadius(1).setTipClusterRadius(0)
.setBlock("minecraft:tube_coral[waterlogged=true]").setCarvingSupport(CarvingMode.CARVING_ONLY);
}
private Fixture fixture(FixtureOptions options) {
IrisSubterrainFeature feature = new IrisSubterrainFeature().setId("natural-room").setProbability(1)
.setRadius(32).setHeight(40).setFluid(options.fluid()).setFluidDepth(options.fluidDepth())
.setPillarSpacing(0).setFormationFraction(0).setWorldYRange(new IrisRange(20, 120));
SubterrainPlanner planner = new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 83L, 0, 256));
SubterrainPlan plan = planner.plansForBounds(0, 0, 512, 512).getFirst();
int x = plan.centerX() + 8;
int z = plan.centerZ() + 8;
SubterrainRoom room = plan.sample(x, plan.centerY(), z).room();
assertNotNull(room);
int floorY = room.floorY();
Engine engine = mock(Engine.class);
when(engine.getHeight()).thenReturn(256);
when(engine.getMinHeight()).thenReturn(0);
when(engine.getHeight(anyInt(), anyInt(), eq(true))).thenReturn(220);
when(engine.getSubterrainCell(anyInt(), anyInt(), anyInt())).thenAnswer(invocation ->
planner.sample(invocation.getArgument(0), invocation.getArgument(1), invocation.getArgument(2)));
IrisData data = mock(IrisData.class);
when(data.getEngine()).thenReturn(engine);
Map<SubterrainPosition, NativeBlockState> writes = new HashMap<>();
Map<SubterrainPosition, Object> metadata = new HashMap<>();
IObjectPlacer placer = mock(IObjectPlacer.class);
when(placer.getEngine()).thenReturn(engine);
when(placer.getFluidHeight()).thenReturn(0);
when(placer.getFluidHeight(anyInt(), anyInt())).thenReturn(0);
when(placer.isUnderwater(anyInt(), anyInt())).thenReturn(options.fluidDepth() > 0);
when(placer.get(anyInt(), anyInt(), anyInt())).thenAnswer(invocation -> {
SubterrainPosition position = new SubterrainPosition(invocation.getArgument(0), invocation.getArgument(1), invocation.getArgument(2));
NativeBlockState written = writes.get(position);
if (written != null) {
return written;
}
SubterrainCell cell = planner.sample(position.x(), position.y(), position.z());
return state(cell.owned() ? cell.material() : "minecraft:stone");
});
when(placer.isCarved(anyInt(), anyInt(), anyInt())).thenAnswer(invocation ->
planner.sample(invocation.getArgument(0), invocation.getArgument(1), invocation.getArgument(2)).occupied());
when(placer.isSolid(anyInt(), anyInt(), anyInt())).thenAnswer(invocation ->
placer.get(invocation.getArgument(0), invocation.getArgument(1), invocation.getArgument(2)).isSolid());
when(placer.isSurfaceSolid(anyInt(), anyInt(), anyInt())).thenAnswer(invocation ->
placer.isSolid(invocation.getArgument(0), invocation.getArgument(1), invocation.getArgument(2)));
doAnswer(invocation -> {
writes.put(new SubterrainPosition(invocation.getArgument(0), invocation.getArgument(1), invocation.getArgument(2)), invocation.getArgument(3));
return null;
}).when(placer).set(anyInt(), anyInt(), anyInt(), any());
doAnswer(invocation -> {
metadata.put(new SubterrainPosition(invocation.getArgument(0), invocation.getArgument(1), invocation.getArgument(2)), invocation.getArgument(3));
return null;
}).when(placer).setData(anyInt(), anyInt(), anyInt(), any());
return new Fixture(planner, placer, data, room, x, z, floorY, writes, metadata);
}
private NativeBlockState state(String key) {
NativeBlockState cached = states.get(key);
if (cached != null) {
return cached;
}
NativeBlockState block = mock(NativeBlockState.class);
int properties = key.indexOf('[');
String material = properties < 0 ? key : key.substring(0, properties);
boolean water = material.equals("minecraft:water");
boolean lava = material.equals("minecraft:lava");
boolean air = material.endsWith("air");
when(block.key()).thenReturn(key);
when(block.materialKey()).thenReturn(material);
when(block.isSolid()).thenReturn(!air && !water && !lava);
when(block.isOccluding()).thenReturn(!air && !water && !lava);
when(block.isAir()).thenReturn(air);
when(block.isFluid()).thenReturn(water || lava);
when(block.isWater()).thenReturn(water);
when(block.isAirOrFluid()).thenReturn(air || water || lava);
when(block.isWaterLogged()).thenReturn(key.contains("waterlogged=true"));
when(block.withProperty(anyString(), anyString())).thenAnswer(invocation ->
state(BlockStateKey.withProperty(key, invocation.getArgument(0), invocation.getArgument(1))));
states.put(key, block);
return block;
}
private record FixtureOptions(IrisSubterrainFluid fluid, int fluidDepth) {
}
private record Fixture(SubterrainPlanner planner, IObjectPlacer placer, IrisData data, SubterrainRoom room,
int x, int z, int floorY, Map<SubterrainPosition, NativeBlockState> writes,
Map<SubterrainPosition, Object> metadata) {
}
}
@@ -1,6 +1,9 @@
package art.arcane.iris.generation.mantle;
import art.arcane.iris.integration.Identifier;
import art.arcane.iris.generation.subterrain.SubterrainCell;
import art.arcane.iris.generation.subterrain.SubterrainRoom;
import art.arcane.iris.generation.subterrain.IrisSubterrainFamily;
import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.runtime.Engine;
@@ -48,6 +51,21 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class ObjectDestinationTransactionTest {
@Test
public void sourceCaptureCannotReplaceAuthoredRoomSolids() {
MantleWriter writer = writer();
SubterrainRoom room = new SubterrainRoom("cenote", "wet-cave", IrisSubterrainFamily.CENOTE,
4, 35, 7, 4, 21, 7, 19, 61, 8, 24, false, true, SubterrainCell.Kind.SOLID);
when(writer.getEngine().getSubterrainCell(4, 30, 7))
.thenReturn(new SubterrainCell(SubterrainCell.Kind.SOLID, "minecraft:stone", room));
ObjectDestinationTransaction transaction = new ObjectDestinationTransaction(writer, 0, 0);
int checkpoint = transaction.mutationCheckpoint();
transaction.set(4, 30, 7, mock(NativeBlockState.class));
transaction.setData(4, 30, 7, "object@1");
transaction.setData(4, 30, 7, new MatterCavern(true, "", (byte) 3));
assertEquals(checkpoint, transaction.mutationCheckpoint());
}
@Test
public void commonAndGenericMetadataSurviveBlockReplacementAndSourceReplay() {
MantleWriter writer = writer();
@@ -248,6 +248,7 @@ public class DimensionTerrain3DCompositionTest {
Engine engine = mock(Engine.class, CALLS_REAL_METHODS);
IrisComplex complex = mock(IrisComplex.class);
doReturn(complex).when(engine).getComplex();
doReturn(new IrisDimension()).when(engine).getDimension();
DimensionStackContext stack = mock(DimensionStackContext.class);
doReturn(stack).when(engine).getDimensionStackContext();
DimensionStackLayout.LayerInput bottom = input(24, 0,
@@ -893,7 +893,7 @@ public class IrisEngineGenerationRuntimeScopeTest {
private static IrisBiome carvingBiome(IrisEngine engine, IrisDimensionCarvingResolver.State state) {
IrisDimensionCarvingEntry root = IrisDimensionCarvingResolver.resolveRootEntry(engine, 80, state);
IrisDimensionCarvingEntry child = IrisDimensionCarvingResolver.resolveFromRoot(engine, root, 19, -3, state);
IrisDimensionCarvingEntry child = IrisDimensionCarvingResolver.resolveFromRoot(engine, root, 19, 80, -3, state);
IrisBiome expected = IrisDimensionCarvingResolver.resolveEntryBiome(engine, child, state);
assertSame(expected, IrisDimensionCarvingResolver.resolveBiome(engine, 19, 80, -3, state));
return expected;
@@ -0,0 +1,64 @@
package art.arcane.iris.generation.runtime;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.subterrain.IrisSubterrainFeature;
import art.arcane.iris.generation.subterrain.SubterrainCell;
import art.arcane.iris.generation.subterrain.SubterrainPlan;
import art.arcane.iris.generation.subterrain.SubterrainPlanner;
import art.arcane.iris.generation.subterrain.SubterrainPosition;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.pack.loading.ResourceLoader;
import art.arcane.iris.world.IrisWorld;
import art.arcane.volmlib.util.collection.KList;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.mockito.Answers.CALLS_REAL_METHODS;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
public class SubterrainBiomeOwnershipTest {
@Test
public void occupiedFeatureOwnsExactVolumeAndYWithoutSurfaceDepthGate() {
IrisSubterrainFeature feature = new IrisSubterrainFeature().setId("basin").setBiome("basin-biome").setProbability(1);
SubterrainPlanner planner = new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 1191L, -64, 320));
SubterrainPlan plan = planner.plansForBounds(0, 0, 512, 512).getFirst();
Engine engine = mock(Engine.class, CALLS_REAL_METHODS);
IrisComplex complex = mock(IrisComplex.class);
IrisData data = mock(IrisData.class);
@SuppressWarnings("unchecked")
ResourceLoader<IrisBiome> loader = mock(ResourceLoader.class);
IrisBiome biome = new IrisBiome();
IrisDimension dimension = new IrisDimension().setSubterrainFeatures(new KList<>(feature));
doReturn(dimension).when(engine).getDimension();
doReturn(complex).when(engine).getComplex();
doReturn(planner).when(complex).getSubterrainPlanner();
doReturn(data).when(engine).getData();
doReturn(mock(DimensionStackContext.class)).when(engine).getDimensionStackContext();
doReturn(loader).when(data).getBiomeLoader();
doReturn(biome).when(loader).load("basin-biome");
doReturn(IrisWorld.builder().minHeight(-64).maxHeight(320).build()).when(engine).getWorld();
SubterrainPosition anchor = plan.anchor();
int internalY = anchor.y() + 64;
assertSame(biome, engine.getBiome(anchor.x(), internalY, anchor.z()));
assertSame(biome, engine.getBiomeOrMantle(anchor.x(), internalY, anchor.z()));
assertEquals(planner.sample(anchor.x(), anchor.y(), anchor.z()), engine.getSubterrainCell(anchor.x(), internalY, anchor.z()));
assertNull(engine.getSubterrainBiome(anchor.x(), plan.bounds().maxY() + 65, anchor.z()));
assertNull(engine.getSubterrainBiome(plan.bounds().maxX() + 1, internalY, anchor.z()));
for (int y = plan.bounds().minY(); y <= plan.bounds().maxY(); y++) {
SubterrainCell cell = planner.sample(anchor.x(), y, anchor.z());
if (cell.solid()) {
assertNull(engine.getSubterrainBiome(anchor.x(), y + 64, anchor.z()));
}
}
verify(engine, never()).getMantle();
verify(engine, never()).getDimensionStackContext();
}
}
@@ -9,13 +9,19 @@ import art.arcane.iris.world.entity.IrisEntity;
import art.arcane.iris.world.entity.IrisEntitySpawn;
import art.arcane.iris.world.entity.IrisEntitySpawn.SpawnContext;
import art.arcane.iris.world.entity.IrisSpawner;
import art.arcane.iris.world.entity.IrisSpawnGroup;
import art.arcane.iris.generation.decoration.IrisSurface;
import art.arcane.iris.world.IrisWorld;
import art.arcane.iris.world.task.J;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.GameRules;
import org.bukkit.World;
import org.junit.Test;
import org.junit.BeforeClass;
import org.mockito.MockedStatic;
import java.lang.reflect.Method;
import java.util.HashMap;
@@ -31,6 +37,8 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.mockStatic;
public class WorldEntitySpawnerInitialSeedTest {
@BeforeClass
@@ -52,6 +60,38 @@ public class WorldEntitySpawnerInitialSeedTest {
verify(fixture.lastChunk, never()).getEntities();
}
@Test
public void successfulUndergroundPopulationReducesSurfaceCapacityWithoutCountingFailures() throws Exception {
for (int undergroundSuccesses : new int[]{0, 2, 4}) {
Fixture fixture = new Fixture();
IrisEntitySpawn undergroundEntry = mock(IrisEntitySpawn.class);
IrisEntity entity = mock(IrisEntity.class);
when(entity.getSurface()).thenReturn(IrisSurface.LAND);
when(entity.spawnCategory()).thenReturn("creature");
when(undergroundEntry.getRealEntity(fixture.engine)).thenReturn(entity);
when(undergroundEntry.getRarity()).thenReturn(1);
IrisSpawner cave = new IrisSpawner().setGroup(IrisSpawnGroup.CAVE)
.setMaxEntitiesPerChunk(4).setInitialSpawns(new KList<>(undergroundEntry));
when(fixture.environment.data().getSpawnerLoader().loadAll(fixture.environment.biome().getEntitySpawners()))
.thenReturn(new KList<>(fixture.definition, cave));
when(fixture.environment.dimension().hasUndergroundSpawners(fixture.engine)).thenReturn(true);
when(fixture.engine.getWorld()).thenReturn(IrisWorld.builder().minHeight(-64).maxHeight(320).build());
when(fixture.engine.getBiomeOrMantleEnvironment(anyInt(), anyInt(), anyInt())).thenReturn(fixture.environment);
when(fixture.engine.openBiomeEnvironmentScope(fixture.environment)).thenReturn(mock(BiomeEnvironment.Scope.class));
Location candidate = new Location(fixture.world, 35, -20, -45);
when(undergroundEntry.spawn(eq(fixture.engine), eq(candidate), any(RNG.class), eq(4), any(SpawnContext.class)))
.thenReturn(undergroundSuccesses);
try (MockedStatic<J> scheduler = mockStatic(J.class);
MockedStatic<IrisEntitySpawn> positions = mockStatic(IrisEntitySpawn.class)) {
positions.when(() -> IrisEntitySpawn.findLiveCaveSpawnLocation(any(Chunk.class), any(RNG.class), eq(IrisSurface.LAND)))
.thenReturn(candidate);
fixture.spawn(2, -3);
}
assertEquals(4 - undergroundSuccesses, fixture.observed.get("2,-3").capacity());
verify(fixture.lastChunk, never()).getEntities();
}
}
@Test
public void chunkBatchSeedsAreIndependentOfVisitOrderAndUnrelatedRandomActivity() throws Exception {
Fixture fixture = new Fixture();
@@ -86,7 +126,7 @@ public class WorldEntitySpawnerInitialSeedTest {
private final Engine engine = mock(Engine.class);
private final IrisWorldManager manager = mock(IrisWorldManager.class);
private final World world = mock(World.class);
private final IrisSpawner definition = mock(IrisSpawner.class);
private final IrisSpawner definition = spy(new IrisSpawner().setMaxEntitiesPerChunk(4));
private final IrisEntitySpawn entry = mock(IrisEntitySpawn.class);
private final Map<String, Observation> observed = new HashMap<>();
private final BiomeEnvironment environment;
@@ -114,7 +154,9 @@ public class WorldEntitySpawnerInitialSeedTest {
when(definition.getInitialSpawns()).thenReturn(new KList<>(entry));
when(definition.getMaxEntitiesPerChunk()).thenReturn(4);
when(entry.getRarity()).thenReturn(1);
when(entry.getRealEntity(engine)).thenReturn(mock(IrisEntity.class));
IrisEntity entity = mock(IrisEntity.class);
when(entity.spawnCategory()).thenReturn("creature");
when(entry.getRealEntity(engine)).thenReturn(entity);
when(world.getGameRuleValue(GameRules.SPAWN_MOBS)).thenReturn(true);
when(entry.spawn(eq(engine), any(Chunk.class), any(RNG.class), anyInt(), any(SpawnContext.class)))
.thenAnswer(invocation -> {
@@ -123,7 +165,7 @@ public class WorldEntitySpawnerInitialSeedTest {
int capacity = invocation.getArgument(3);
SpawnContext context = invocation.getArgument(4);
observed.put(chunk.getX() + "," + chunk.getZ(), new Observation(rng.getSeed(), capacity, context.initial()));
return 4;
return capacity;
});
environment = new BiomeEnvironment(1L, biome, region, dimension, data);
spawner = new WorldEntitySpawner(manager);
@@ -0,0 +1,116 @@
package art.arcane.iris.generation.runtime;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.decoration.IrisSurface;
import art.arcane.iris.generation.subterrain.IrisSubterrainFeature;
import art.arcane.iris.generation.subterrain.SubterrainPlan;
import art.arcane.iris.generation.subterrain.SubterrainPlanner;
import art.arcane.iris.generation.subterrain.SubterrainPosition;
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.pack.loading.ResourceLoader;
import art.arcane.iris.world.IrisWorld;
import art.arcane.iris.world.entity.IrisEntity;
import art.arcane.iris.world.entity.IrisEntitySpawn;
import art.arcane.iris.world.entity.IrisSpawnGroup;
import art.arcane.iris.world.entity.IrisSpawner;
import art.arcane.iris.world.task.J;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.stream.ProceduralStream;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.World;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.lang.reflect.Method;
import java.util.List;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.junit.Assert.assertSame;
import static org.mockito.Answers.CALLS_REAL_METHODS;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class WorldEntitySpawnerUndergroundOwnershipTest {
@Test
public void cavePoolUsesActualCandidateBiomeAndPlacesInsideItsVolume() throws Exception {
Engine engine = mock(Engine.class, CALLS_REAL_METHODS);
IrisWorldManager manager = mock(IrisWorldManager.class);
when(manager.getEngine()).thenReturn(engine);
doReturn(IrisWorld.builder().minHeight(-64).maxHeight(320).build()).when(engine).getWorld();
World world = mock(World.class);
Chunk chunk = mock(Chunk.class);
when(chunk.getWorld()).thenReturn(world);
IrisSubterrainFeature feature = new IrisSubterrainFeature().setId("basin").setBiome("basin-biome").setProbability(1);
SubterrainPlanner planner = new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 1191L, -64, 320));
SubterrainPlan plan = planner.plansForBounds(0, 0, 512, 512).getFirst();
SubterrainPosition anchor = plan.anchor();
Location candidate = new Location(world, anchor.x(), anchor.y(), anchor.z());
IrisBiome underground = new IrisBiome();
IrisDimension dimension = new IrisDimension().setSubterrainFeatures(new KList<>(feature));
IrisRegion region = new IrisRegion().setEntitySpawners(new KList<>("cave-residents"));
IrisRegion stackedRegion = new IrisRegion().setEntitySpawners(new KList<>("wrong-pack-residents"));
IrisComplex complex = mock(IrisComplex.class);
DimensionStackContext stack = mock(DimensionStackContext.class);
DimensionStackLayout layout = mock(DimensionStackLayout.class);
DimensionStackLayout.Layer layer = mock(DimensionStackLayout.Layer.class);
@SuppressWarnings("unchecked")
ProceduralStream<IrisRegion> regionStream = mock(ProceduralStream.class);
doReturn(complex).when(engine).getComplex();
doReturn(dimension).when(engine).getDimension();
doReturn(planner).when(complex).getSubterrainPlanner();
doReturn(regionStream).when(complex).getRegionStream();
when(regionStream.get(anchor.x(), anchor.z())).thenReturn(region);
doReturn(stack).when(engine).getDimensionStackContext();
when(stack.getLayout(anchor.x(), anchor.z())).thenReturn(layout);
when(layout.layerAt(anyInt())).thenReturn(layer);
when(layer.region()).thenReturn(stackedRegion);
IrisData data = mock(IrisData.class);
@SuppressWarnings("unchecked")
ResourceLoader<IrisSpawner> loader = mock(ResourceLoader.class);
doReturn(data).when(engine).getData();
@SuppressWarnings("unchecked")
ResourceLoader<IrisBiome> biomeLoader = mock(ResourceLoader.class);
when(data.getBiomeLoader()).thenReturn(biomeLoader);
when(biomeLoader.load("basin-biome")).thenReturn(underground);
when(data.getSpawnerLoader()).thenReturn(loader);
when(loader.loadAll(dimension.getEntitySpawners())).thenReturn(new KList<>());
when(loader.loadAll(underground.getEntitySpawners())).thenReturn(new KList<>());
IrisEntitySpawn entry = mock(IrisEntitySpawn.class);
IrisEntity entity = mock(IrisEntity.class);
when(entity.getSurface()).thenReturn(IrisSurface.LAND);
when(entry.getRealEntity(engine)).thenReturn(entity);
when(entry.getRarity()).thenReturn(1);
IrisSpawner spawner = new IrisSpawner().setGroup(IrisSpawnGroup.CAVE).setInitialSpawns(new KList<>(entry));
when(loader.loadAll(region.getEntitySpawners())).thenReturn(new KList<>(spawner));
BiomeEnvironment environment = engine.getBiomeOrMantleEnvironment(anchor.x(), anchor.y() + 64, anchor.z());
assertSame(underground, environment.biome());
assertSame(region, environment.region());
BiomeEnvironment.Scope scope = mock(BiomeEnvironment.Scope.class);
when(engine.openBiomeEnvironmentScope(environment)).thenReturn(scope);
try (MockedStatic<J> scheduler = mockStatic(J.class);
MockedStatic<IrisEntitySpawn> positions = mockStatic(IrisEntitySpawn.class)) {
positions.when(() -> IrisEntitySpawn.findLiveCaveSpawnLocation(eq(chunk), any(RNG.class), eq(IrisSurface.LAND)))
.thenReturn(candidate);
Method method = WorldEntitySpawner.class.getDeclaredMethod("spawnUnderground",
Chunk.class, boolean.class, WorldEntitySpawner.ChunkCounter.class, RNG.class);
method.setAccessible(true);
method.invoke(new WorldEntitySpawner(manager), chunk, true, new WorldEntitySpawner.ChunkCounter(new org.bukkit.entity.Entity[0]), new RNG(17719L));
}
verify(loader, never()).loadAll(stackedRegion.getEntitySpawners());
verify(engine, never()).getSurfaceBiomeEnvironment(anchor.x(), anchor.z());
assertSame(stackedRegion, engine.getRegion(anchor.x(), plan.bounds().maxY() + 65, anchor.z()));
verify(entry).spawn(eq(engine), eq(candidate), any(RNG.class), eq(spawner.getMaxEntitiesPerChunk()),
any(IrisEntitySpawn.SpawnContext.class));
verify(scope).close();
}
}
@@ -0,0 +1,382 @@
package art.arcane.iris.generation.subterrain;
import art.arcane.iris.pack.value.IrisRange;
import com.google.gson.Gson;
import com.google.gson.JsonParseException;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class SubterrainPlannerTest {
@Test
public void terraceAnchorsUseOccupiedPathsWhenCenterIsPillarOrUpperStep() {
for (IrisSubterrainFluid fluid : IrisSubterrainFluid.values()) {
IrisSubterrainFeature feature = compactFeature(IrisSubterrainFamily.TRAVERTINE_TERRACES).setFluid(fluid);
SubterrainPlan pillar = direct(feature);
for (int y = pillar.bounds().minY(); y <= pillar.bounds().maxY(); y++) {
assertFalse(pillar.sample(0, y, 0).occupied());
}
SubterrainPosition pillarAnchor = pillar.anchor();
assertTrue(pillar.sample(pillarAnchor.x(), pillarAnchor.y(), pillarAnchor.z()).occupied());
assertTrue(pillar.sample(pillarAnchor.x(), pillarAnchor.y(), pillarAnchor.z()).room().reservedPassage());
feature.setHeight(8).setFluidDepth(2).setTerraceCount(16);
for (boolean rotated : List.of(false, true)) {
SubterrainPlan lowVault = new SubterrainPlan(new SubterrainPlan.Options(
SubterrainPlanner.Definition.from(feature), 0, 0, 0, rotated, "terrace:low-vault"));
SubterrainPosition anchor = lowVault.anchor();
SubterrainCell cell = lowVault.sample(anchor.x(), anchor.y(), anchor.z());
assertTrue(cell.occupied());
assertTrue(cell.room().reservedPassage());
assertTrue((rotated ? anchor.z() : anchor.x()) < 0);
assertEquals(anchor, lowVault.anchor());
}
}
}
@Test
public void explicitFluidChangesOnlyLiquidKindAndMaterialForEveryWetFamily() {
for (IrisSubterrainFamily family : List.of(IrisSubterrainFamily.CENOTE,
IrisSubterrainFamily.LAVA_TUBE, IrisSubterrainFamily.TRAVERTINE_TERRACES)) {
IrisSubterrainFeature water = compactFeature(family).setFluid(IrisSubterrainFluid.WATER);
IrisSubterrainFeature lava = compactFeature(family).setFluid(IrisSubterrainFluid.LAVA);
SubterrainPlanner waterPlanner = planner(water);
SubterrainPlanner lavaPlanner = planner(lava);
SubterrainPlan waterPlaced = first(waterPlanner);
SubterrainPlan lavaPlaced = first(lavaPlanner);
assertEquals(waterPlaced.id(), lavaPlaced.id());
assertEquals(waterPlaced.bounds(), lavaPlaced.bounds());
assertEquals(waterPlaced.anchor(), lavaPlaced.anchor());
SubterrainPlan waterPlan = direct(water);
SubterrainPlan lavaPlan = direct(lava);
SubterrainBounds bounds = waterPlan.bounds();
int fluidCells = 0;
for (int x = bounds.minX(); x <= bounds.maxX(); x++) {
for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) {
for (int y = bounds.minY(); y <= bounds.maxY(); y++) {
SubterrainCell original = waterPlanner.sample(List.of(waterPlan), x, y, z);
SubterrainCell changed = lavaPlanner.sample(List.of(lavaPlan), x, y, z);
if (original.fluid()) {
fluidCells++;
assertEquals(SubterrainCell.Kind.WATER, original.kind());
assertEquals("minecraft:water", original.material());
assertEquals(SubterrainCell.Kind.LAVA, changed.kind());
assertEquals("minecraft:lava", changed.material());
SubterrainRoom room = original.room();
assertEquals(new SubterrainRoom(room.featureId(), room.biome(), room.family(),
room.centerX(), room.centerY(), room.centerZ(), room.pathX(), room.pathY(), room.pathZ(),
room.floorY(), room.ceilingY(), room.boundaryDistance(), room.fluidHeadY(),
room.reservedPassage(), room.reservedSolid(), SubterrainCell.Kind.LAVA), changed.room());
} else {
assertEquals(original, changed);
}
}
}
}
assertTrue(family.toString(), fluidCells > 100);
assertEquals(assertRetained(waterPlanner, List.of(waterPlan), SubterrainCell.Kind.WATER),
assertRetained(lavaPlanner, List.of(lavaPlan), SubterrainCell.Kind.LAVA));
}
}
@Test
public void omittedAndNullFluidsPreserveFamilyDefaults() {
Gson gson = new Gson();
for (IrisSubterrainFamily family : IrisSubterrainFamily.values()) {
IrisSubterrainFluid expected = family == IrisSubterrainFamily.LAVA_TUBE
? IrisSubterrainFluid.LAVA : IrisSubterrainFluid.WATER;
IrisSubterrainFeature omitted = gson.fromJson("{\"family\":\"" + family + "\"}", IrisSubterrainFeature.class);
IrisSubterrainFeature nullable = gson.fromJson("{\"family\":\"" + family + "\",\"fluid\":null}", IrisSubterrainFeature.class);
assertNull(omitted.getFluid());
assertNull(nullable.getFluid());
omitted.setId("default");
nullable.setId("default");
assertEquals(expected, SubterrainPlanner.Definition.from(omitted).fluid());
assertEquals(SubterrainPlanner.Definition.from(omitted), SubterrainPlanner.Definition.from(nullable));
SubterrainPlan implicit = direct(feature(family));
SubterrainPlan explicit = direct(feature(family).setFluid(expected));
assertEquals(implicit.bounds(), explicit.bounds());
for (int x = implicit.bounds().minX(); x <= implicit.bounds().maxX(); x += 7) {
for (int z = implicit.bounds().minZ(); z <= implicit.bounds().maxZ(); z += 7) {
for (int y = implicit.bounds().minY(); y <= implicit.bounds().maxY(); y++) {
assertEquals(implicit.sample(x, y, z), explicit.sample(x, y, z));
}
}
}
}
}
@Test
public void faultRemainsDryForEveryConfiguredFluid() {
SubterrainPlan original = direct(feature(IrisSubterrainFamily.TECTONIC_FAULT));
for (IrisSubterrainFluid fluid : IrisSubterrainFluid.values()) {
SubterrainPlan configured = direct(feature(IrisSubterrainFamily.TECTONIC_FAULT).setFluid(fluid));
for (int x = original.bounds().minX(); x <= original.bounds().maxX(); x += 2) {
for (int z = original.bounds().minZ(); z <= original.bounds().maxZ(); z += 2) {
for (int y = original.bounds().minY(); y <= original.bounds().maxY(); y++) {
SubterrainCell cell = configured.sample(x, y, z);
assertFalse(cell.fluid());
assertEquals(original.sample(x, y, z), cell);
}
}
}
}
}
@Test
public void plansSnapshotResolvedFluidAcrossConfigurationMutation() {
IrisSubterrainFeature feature = compactFeature(IrisSubterrainFamily.CENOTE);
SubterrainPlan implicit = direct(feature);
feature.setFluid(IrisSubterrainFluid.LAVA);
SubterrainPlan explicit = direct(feature);
feature.setFluid(IrisSubterrainFluid.WATER);
assertEquals(SubterrainCell.Kind.WATER, implicit.sample(0, -10, 0).kind());
assertEquals(SubterrainCell.Kind.LAVA, explicit.sample(0, -10, 0).kind());
}
@Test
public void fluidJsonOnlyAcceptsTypedWaterOrLava() {
Gson gson = new Gson();
for (IrisSubterrainFluid fluid : IrisSubterrainFluid.values()) {
assertEquals(fluid, gson.fromJson("{\"fluid\":\"" + fluid + "\"}", IrisSubterrainFeature.class).getFluid());
assertEquals("\"" + fluid + "\"", gson.toJson(fluid));
}
for (String invalid : List.of("\"OIL\"", "\"minecraft:water\"", "\"water\"", "5", "true", "{}", "[]")) {
assertThrows(invalid, JsonParseException.class,
() -> gson.fromJson("{\"fluid\":" + invalid + "}", IrisSubterrainFeature.class));
}
}
@Test
public void allFamiliesHaveBoundedOccupiedAnchorsAndAbsoluteOwnership() {
for (IrisSubterrainFamily family : IrisSubterrainFamily.values()) {
SubterrainPlanner planner = planner(feature(family));
SubterrainPlan plan = first(planner);
SubterrainPosition anchor = plan.anchor();
assertTrue(family.toString(), plan.sample(anchor.x(), anchor.y(), anchor.z()).occupied());
assertTrue(plan.bounds().minY() >= -64);
assertTrue(plan.bounds().maxY() <= 96);
assertFalse(planner.sample(anchor.x(), -65, anchor.z()).owned());
assertFalse(planner.sample(anchor.x(), 97, anchor.z()).owned());
assertFalse(plan.sample(plan.bounds().maxX() + 1, anchor.y(), anchor.z()).owned());
assertNotNull(plan.sample(anchor.x(), anchor.y(), anchor.z()).room());
}
}
@Test
public void faultWallsRemainParallelBeyondTwoHundredBlocksAndHaveShelves() {
SubterrainPlan plan = direct(feature(IrisSubterrainFamily.TECTONIC_FAULT));
for (int x = -110; x <= 110; x++) {
assertTrue(plan.sample(x, 0, 0).occupied());
assertTrue(plan.sample(x, 0, 32).solid());
assertTrue(plan.sample(x, 0, -32).solid());
}
int centralFloor = plan.sample(0, 0, 0).room().floorY();
int ledgeFloor = plan.sample(0, 0, 28).room().floorY();
assertTrue(ledgeFloor > centralFloor);
assertTrue(plan.sample(0, ledgeFloor, 28).solid());
}
@Test
public void cenoteDomeAndSealedWaterTableHaveNoExposedFluidSidesOrBottom() {
SubterrainPlan plan = direct(feature(IrisSubterrainFamily.CENOTE));
SubterrainPlanner planner = planner(feature(IrisSubterrainFamily.CENOTE));
assertTrue(plan.sample(0, 0, 0).room().ceilingY() > plan.sample(20, 0, 0).room().ceilingY());
assertTrue(assertRetained(planner, List.of(plan), SubterrainCell.Kind.WATER) > 1000);
assertTrue(plan.sample(0, -16, 0).solid());
}
@Test
public void lavaTubeHasConnectedHornitosAndDryElevatedWalkways() {
SubterrainPlan plan = direct(feature(IrisSubterrainFamily.LAVA_TUBE));
int chimneyX = 64;
int pathZ = 5;
for (int y = 0; y < 32; y++) {
assertEquals("chimney y=" + y, SubterrainCell.Kind.AIR, plan.sample(chimneyX, y, pathZ).kind());
}
assertTrue(plan.sample(chimneyX + 4, 25, pathZ).solid());
SubterrainCell walkway = plan.sample(0, 0, 18);
assertTrue(walkway.room().floorY() > walkway.room().fluidHeadY());
assertTrue(plan.sample(0, walkway.room().floorY(), 18).solid());
SubterrainPlanner planner = planner(feature(IrisSubterrainFamily.LAVA_TUBE));
assertTrue(assertRetained(planner, List.of(plan), SubterrainCell.Kind.LAVA) > 1000);
}
@Test
public void travertineBasinsStepAndRetainFluidBehindRims() {
SubterrainPlan plan = direct(feature(IrisSubterrainFamily.TRAVERTINE_TERRACES));
assertTrue(plan.sample(90, 0, 0).room().floorY() > plan.sample(-90, 0, 0).room().floorY());
SubterrainPlanner planner = planner(feature(IrisSubterrainFamily.TRAVERTINE_TERRACES));
assertTrue(assertRetained(planner, List.of(plan), SubterrainCell.Kind.WATER) > 1000);
int rimX = -128 + 42;
assertTrue(plan.sample(rimX, -10, 0).solid());
assertTrue(plan.sample(0, 0, 28).room().floorY() > plan.sample(0, 0, 28).room().fluidHeadY());
}
@Test
public void pillarsSpanTheWholeVaultAndFormationsScaleWithVault() {
IrisSubterrainFeature feature = feature(IrisSubterrainFamily.CENOTE).setPillarSpacing(12).setFormationFraction(0.3);
SubterrainPlan plan = direct(feature);
SubterrainRoom room = plan.sample(12, 0, 12).room();
for (int y = room.floorY(); y <= room.ceilingY(); y++) {
assertTrue("pillar y=" + y, plan.sample(12, y, 12).solid());
}
assertTrue(plan.sample(0, 0, 0).room().reservedPassage());
assertFalse(plan.sample(0, 0, 0).solid());
int formationCells = 0;
for (int x = -20; x <= 20; x++) {
for (int z = -20; z <= 20; z++) {
SubterrainCell middle = plan.sample(x, 0, z);
if (!middle.occupied()) {
continue;
}
SubterrainRoom context = middle.room();
int quarter = Math.max(1, context.vaultHeight() / 4);
if (plan.sample(x, context.ceilingY() - quarter, z).solid()) {
formationCells++;
}
}
}
assertTrue(formationCells > 0);
}
@Test
public void configOrderAndChunkTraversalDoNotChangeIdentityOrCells() {
IrisSubterrainFeature cenote = feature(IrisSubterrainFamily.CENOTE);
IrisSubterrainFeature fault = feature(IrisSubterrainFamily.TECTONIC_FAULT);
List<IrisSubterrainFeature> features = new ArrayList<>(List.of(cenote, fault));
SubterrainPlanner first = new SubterrainPlanner(new SubterrainPlanner.Options(features, 7123, -64, 97));
Collections.reverse(features);
SubterrainPlanner reordered = new SubterrainPlanner(new SubterrainPlanner.Options(features, 7123, -64, 97));
List<SubterrainPlan> plans = first.plansForBounds(-1024, -1024, 1024, 1024);
assertEquals(plans.stream().map(SubterrainPlan::id).toList(),
reordered.plansForBounds(-1024, -1024, 1024, 1024).stream().map(SubterrainPlan::id).toList());
for (SubterrainPlan plan : plans) {
for (int x = plan.bounds().minX(); x <= plan.bounds().maxX(); x += 13) {
for (int z = plan.bounds().minZ(); z <= plan.bounds().maxZ(); z += 13) {
int chunkX = Math.floorDiv(x, 16) * 16;
int chunkZ = Math.floorDiv(z, 16) * 16;
List<SubterrainPlan> chunk = first.plansForBounds(chunkX - 1, chunkZ - 1, chunkX + 16, chunkZ + 16);
for (int y = plan.bounds().minY(); y <= plan.bounds().maxY(); y += 7) {
assertEquals(first.sample(x, y, z), first.sample(chunk, x, y, z));
assertEquals(first.sample(x, y, z), reordered.sample(x, y, z));
}
}
}
}
}
@Test
public void immutableSnapshotsAndSeedsControlPlans() {
IrisSubterrainFeature feature = feature(IrisSubterrainFamily.CENOTE);
SubterrainPlanner planner = planner(feature);
List<String> before = planner.plansForBounds(-1024, -1024, 1024, 1024).stream().map(SubterrainPlan::id).toList();
feature.setRadius(8).setBiome("changed").setId("changed");
assertEquals(before, planner.plansForBounds(-1024, -1024, 1024, 1024).stream().map(SubterrainPlan::id).toList());
assertEquals("subterranean/example", first(planner).biome());
SubterrainPlanner anotherSeed = new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 999, -64, 97));
assertNotEquals(before, anotherSeed.plansForBounds(-1024, -1024, 1024, 1024).stream().map(SubterrainPlan::id).toList());
}
@Test
public void narrowYBandNeverClipsSealsIntoWorld() {
IrisSubterrainFeature feature = feature(IrisSubterrainFamily.CENOTE).setWorldYRange(new IrisRange(0, 10));
assertTrue(planner(feature).plansForBounds(-2048, -2048, 2048, 2048).isEmpty());
}
@Test
public void solidSealsWinOverHigherPriorityUnrelatedCarving() {
SubterrainPlan lower = direct(feature(IrisSubterrainFamily.CENOTE));
IrisSubterrainFeature feature = feature(IrisSubterrainFamily.TECTONIC_FAULT).setPriority(100);
SubterrainPlanner.Definition definition = SubterrainPlanner.Definition.from(feature);
SubterrainPlan upper = new SubterrainPlan(new SubterrainPlan.Options(definition, 32, 0, 0, false, "fault:other"));
SubterrainPlanner planner = planner(feature);
assertTrue(upper.sample(32, -14, 0).occupied());
assertTrue(lower.sample(32, -14, 0).solid());
assertTrue(planner.sample(List.of(upper, lower), 32, -14, 0).solid());
}
@Test(expected = IllegalArgumentException.class)
public void shortFaultDefinitionsAreRejected() {
planner(feature(IrisSubterrainFamily.TECTONIC_FAULT).setLength(120));
}
@Test
public void footprintAdmissionRejectsTheEntireFeatureAcrossSavedOwnership() {
SubterrainPlanner original = planner(feature(IrisSubterrainFamily.CENOTE));
SubterrainPlan plan = first(original);
SubterrainPosition anchor = plan.anchor();
int protectedX = plan.bounds().maxX();
int protectedZ = plan.bounds().maxZ();
SubterrainPlanner restricted = original.restrictTo(bounds ->
!bounds.intersects(protectedX, protectedZ, protectedX, protectedZ));
assertTrue(original.sample(anchor.x(), anchor.y(), anchor.z()).occupied());
assertFalse(restricted.plansForBounds(plan.bounds().minX(), plan.bounds().minZ(),
plan.bounds().maxX(), plan.bounds().maxZ()).stream().anyMatch(candidate -> candidate.id().equals(plan.id())));
assertFalse(restricted.sample(anchor.x(), anchor.y(), anchor.z()).owned());
assertTrue(original.sample(anchor.x(), anchor.y(), anchor.z()).occupied());
}
@Test(expected = IllegalArgumentException.class)
public void duplicateIdentifiersAreRejected() {
IrisSubterrainFeature feature = feature(IrisSubterrainFamily.CENOTE);
new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature, feature), 1, -64, 97));
}
private int assertRetained(SubterrainPlanner planner, List<SubterrainPlan> candidates, SubterrainCell.Kind kind) {
int count = 0;
SubterrainBounds bounds = candidates.getFirst().bounds();
int[][] offsets = {{-1, 0, 0}, {1, 0, 0}, {0, 0, -1}, {0, 0, 1}, {0, -1, 0}};
for (int x = bounds.minX(); x <= bounds.maxX(); x++) {
for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) {
for (int y = bounds.minY(); y <= bounds.maxY(); y++) {
SubterrainCell cell = planner.sample(candidates, x, y, z);
if (cell.kind() != kind) {
continue;
}
count++;
for (int[] offset : offsets) {
SubterrainCell neighbor = planner.sample(candidates, x + offset[0], y + offset[1], z + offset[2]);
assertTrue("fluid at " + x + "," + y + "," + z, neighbor.solid() || neighbor.kind() == kind);
}
}
}
}
return count;
}
private IrisSubterrainFeature feature(IrisSubterrainFamily family) {
return new IrisSubterrainFeature().setId(family.name().toLowerCase()).setFamily(family)
.setProbability(1).setWorldYRange(new IrisRange(-64, 96)).setBiome("subterranean/example")
.setPillarSpacing(0).setFormationFraction(0);
}
private IrisSubterrainFeature compactFeature(IrisSubterrainFamily family) {
return feature(family).setLength(64).setRadius(12).setHeight(24).setChimneyHeight(8)
.setPillarSpacing(12).setFormationFraction(0.2);
}
private SubterrainPlanner planner(IrisSubterrainFeature feature) {
return new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 7123, -64, 97));
}
private SubterrainPlan first(SubterrainPlanner planner) {
List<SubterrainPlan> plans = planner.plansForBounds(-1024, -1024, 1024, 1024);
assertFalse(plans.isEmpty());
return plans.getFirst();
}
private SubterrainPlan direct(IrisSubterrainFeature feature) {
return new SubterrainPlan(new SubterrainPlan.Options(SubterrainPlanner.Definition.from(feature),
0, 0, 0, false, feature.getId() + ":test"));
}
}
@@ -0,0 +1,129 @@
package art.arcane.iris.generation.subterrain;
import art.arcane.iris.pack.value.IrisRange;
import art.arcane.iris.generation.block.B;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.matter.MatterCavern;
import org.mockito.MockedStatic;
import org.junit.Test;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
public class SubterrainRasterizerTest {
@Test
public void lavaOverridePublishesLavaBlocksAndCavernStateForCenotesAndTerraces() {
NativeBlockState lava = mock(NativeBlockState.class);
when(lava.key()).thenReturn("minecraft:lava");
try (MockedStatic<B> blocks = mockStatic(B.class)) {
blocks.when(() -> B.getState("minecraft:lava")).thenReturn(lava);
for (IrisSubterrainFamily family : List.of(IrisSubterrainFamily.CENOTE, IrisSubterrainFamily.TRAVERTINE_TERRACES)) {
IrisSubterrainFeature feature = new IrisSubterrainFeature().setId("lava-room").setFamily(family)
.setFluid(IrisSubterrainFluid.LAVA).setProbability(1).setLength(64).setRadius(12)
.setHeight(24).setPillarSpacing(0).setFormationFraction(0)
.setBiome("underground/lava").setWorldYRange(new IrisRange(-48, 60));
SubterrainPlanner planner = new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 83L, -64, 128));
SubterrainPlan plan = planner.plansForBounds(-512, -512, 512, 512).getFirst();
int[] fluids = {0};
SubterrainRasterizer.rasterize(planner, plan.centerX() >> 4, plan.centerZ() >> 4, (x, y, z, cell) -> {
if (cell.fluid()) {
fluids[0]++;
assertEquals(SubterrainCell.Kind.LAVA, cell.kind());
assertEquals("minecraft:lava", cell.material());
assertEquals(lava, SubterrainRasterizer.state(cell));
MatterCavern cavern = SubterrainRasterizer.cavern(cell);
assertEquals(2, cavern.getLiquid());
assertTrue(cavern.isLava());
assertFalse(cavern.isWater());
assertEquals("underground/lava", cavern.getCustomBiome());
assertEquals(cell, planner.sample(x, y, z));
}
});
assertTrue(family.toString(), fluids[0] > 0);
}
}
}
@Test
public void sealingMaterialRejectsPartialAndUnresolvedBlocks() {
IrisSubterrainFeature feature = new IrisSubterrainFeature().setId("sealed-room").setSolid("minecraft:oak_slab");
NativeBlockState partial = mock(NativeBlockState.class);
when(partial.isSolid()).thenReturn(true);
try (MockedStatic<B> blocks = mockStatic(B.class)) {
blocks.when(() -> B.getStateOrNull("minecraft:oak_slab", false)).thenReturn(partial);
assertThrows(IllegalArgumentException.class, () -> SubterrainRasterizer.validateMaterials(List.of(feature)));
feature.setSolid("minecraft:missing_block");
assertThrows(IllegalArgumentException.class, () -> SubterrainRasterizer.validateMaterials(List.of(feature)));
NativeBlockState full = mock(NativeBlockState.class);
when(full.key()).thenReturn("minecraft:stone");
when(full.isSolid()).thenReturn(true);
when(full.isOccluding()).thenReturn(true);
feature.setSolid("minecraft:stone");
blocks.when(() -> B.getStateOrNull("minecraft:stone", false)).thenReturn(full);
SubterrainRasterizer.validateMaterials(List.of(feature));
when(full.isWaterLogged()).thenReturn(true);
assertThrows(IllegalArgumentException.class, () -> SubterrainRasterizer.validateMaterials(List.of(feature)));
when(full.isWaterLogged()).thenReturn(false);
when(full.key()).thenReturn("minecraft:gravel");
feature.setSolid("minecraft:gravel");
blocks.when(() -> B.getStateOrNull("minecraft:gravel", false)).thenReturn(full);
assertThrows(IllegalArgumentException.class, () -> SubterrainRasterizer.validateMaterials(List.of(feature)));
}
}
@Test
public void chunksRasterizeTheSameCellsInEitherOrderAndMatchQueries() {
IrisSubterrainFeature feature = new IrisSubterrainFeature().setId("retained-cenote")
.setProbability(1).setRadius(20).setHeight(24).setPillarSpacing(12)
.setWorldYRange(new IrisRange(-48, 60));
SubterrainPlanner planner = new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 83L, -64, 128));
SubterrainPlan plan = planner.plansForBounds(-512, -512, 512, 512).getFirst();
int firstX = plan.centerX() >> 4;
int firstZ = plan.centerZ() >> 4;
Map<SubterrainPosition, SubterrainCell> forward = new HashMap<>();
Map<SubterrainPosition, SubterrainCell> reverse = new HashMap<>();
for (int x = firstX - 1; x <= firstX + 1; x++) {
SubterrainRasterizer.rasterize(planner, x, firstZ, (xx, y, z, cell) -> forward.put(new SubterrainPosition(xx, y, z), cell));
}
for (int x = firstX + 1; x >= firstX - 1; x--) {
SubterrainRasterizer.rasterize(planner, x, firstZ, (xx, y, z, cell) -> reverse.put(new SubterrainPosition(xx, y, z), cell));
}
assertFalse(forward.isEmpty());
assertEquals(forward, reverse);
int wet = 0;
for (Map.Entry<SubterrainPosition, SubterrainCell> entry : forward.entrySet()) {
SubterrainPosition position = entry.getKey();
SubterrainCell cell = entry.getValue();
assertEquals(cell, planner.sample(position.x(), position.y(), position.z()));
if (cell.fluid()) {
wet++;
for (int[] offset : List.of(new int[]{1, 0, 0}, new int[]{-1, 0, 0}, new int[]{0, 0, 1}, new int[]{0, 0, -1}, new int[]{0, -1, 0})) {
SubterrainCell neighbor = planner.sample(position.x() + offset[0], position.y() + offset[1], position.z() + offset[2]);
assertTrue(neighbor.solid() || neighbor.kind() == cell.kind());
}
}
}
assertTrue(wet > 0);
}
@Test
public void protectionKeepsSolidAndPassageWhileAllowingRoomDecoration() {
SubterrainRoom room = new SubterrainRoom("room", "biome", IrisSubterrainFamily.CENOTE,
0, 0, 0, 0, 0, 0, -8, 12, 10, -4, false, false, SubterrainCell.Kind.AIR);
assertFalse(SubterrainRasterizer.protectsPlacement(SubterrainCell.OUTSIDE));
assertFalse(SubterrainRasterizer.protectsPlacement(new SubterrainCell(SubterrainCell.Kind.AIR, "minecraft:cave_air", room)));
assertTrue(SubterrainRasterizer.protectsPlacement(new SubterrainCell(SubterrainCell.Kind.SOLID, "minecraft:stone", room)));
SubterrainRoom passage = new SubterrainRoom("room", "biome", IrisSubterrainFamily.CENOTE,
0, 0, 0, 0, 0, 0, -8, 12, 10, -4, true, false, SubterrainCell.Kind.AIR);
assertTrue(SubterrainRasterizer.protectsPlacement(new SubterrainCell(SubterrainCell.Kind.AIR, "minecraft:cave_air", passage)));
}
}
@@ -0,0 +1,304 @@
package art.arcane.iris.generation.subterrain;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.biome.IrisBiomePaletteLayer;
import art.arcane.iris.generation.block.B;
import art.arcane.iris.generation.block.IrisBlockData;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.stage.IrisCarveModifier;
import art.arcane.iris.world.IrisWorld;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.pack.loading.ResourceLoader;
import art.arcane.iris.pack.value.IrisRange;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.hunk.Hunk;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.times;
public class SubterrainSurfaceMaterialsTest {
@Test
public void everyGeometryUsesOwningBiomeFloorCeilingAndWallWithoutChangingOccupancy() {
for (IrisSubterrainFamily family : IrisSubterrainFamily.values()) {
try (Fixture fixture = new Fixture(family)) {
Map<SubterrainSurfaceMaterials.Surface, SubterrainPosition> surfaces = fixture.surfaces();
for (SubterrainSurfaceMaterials.Surface surface : List.of(SubterrainSurfaceMaterials.Surface.FLOOR,
SubterrainSurfaceMaterials.Surface.CEILING, SubterrainSurfaceMaterials.Surface.WALL)) {
SubterrainPosition position = surfaces.get(surface);
assertTrue(family + " " + surface, position != null);
SubterrainCell before = fixture.cell(position);
NativeBlockState expected = switch (surface) {
case FLOOR -> fixture.floor;
case CEILING -> fixture.ceiling;
case WALL -> fixture.wall;
default -> fixture.fallback;
};
assertSame(expected, fixture.materials.state(position.x(), position.y(), position.z(), before));
assertTrue(SubterrainRasterizer.protectsPlacement(before));
assertEquals(before, fixture.cell(position));
int chunkX = position.x() >> 4;
int chunkZ = position.z() >> 4;
List<SubterrainPlan> local = fixture.planner.plansForBounds((chunkX << 4) - 1, (chunkZ << 4) - 1,
(chunkX << 4) + 16, (chunkZ << 4) + 16);
SubterrainSurfaceMaterials chunk = fixture.materials(local);
assertSame(expected, chunk.state(position.x(), position.y(), position.z(), before));
}
SubterrainPosition anchor = fixture.plan.anchor();
SubterrainCell occupied = fixture.cell(anchor);
assertTrue(occupied.occupied());
assertSame(SubterrainRasterizer.state(occupied), fixture.materials.state(anchor.x(), anchor.y(), anchor.z(), occupied));
}
}
}
@Test
public void finalPreservationRestoresBiomeMaterialRatherThanErasingIt() throws Exception {
try (Fixture fixture = new Fixture(IrisSubterrainFamily.CENOTE)) {
Engine engine = mock(Engine.class);
when(engine.getComplex()).thenReturn(fixture.complex);
when(engine.getData()).thenReturn(fixture.data);
when(engine.getDimension()).thenReturn(fixture.dimension);
when(engine.getWorld()).thenReturn(IrisWorld.builder().minHeight(-64).maxHeight(128).build());
when(fixture.complex.getSubterrainPlanner()).thenReturn(fixture.planner);
IrisCarveModifier modifier = mock(IrisCarveModifier.class, CALLS_REAL_METHODS);
doReturn(engine).when(modifier).getEngine();
Field random = IrisCarveModifier.class.getDeclaredField("rng");
random.setAccessible(true);
random.set(modifier, fixture.random);
for (Map.Entry<SubterrainSurfaceMaterials.Surface, SubterrainPosition> entry : fixture.surfaces().entrySet()) {
SubterrainPosition position = entry.getValue();
Hunk<NativeBlockState> output = Hunk.newArrayHunk(16, 192, 16);
int x = position.x() & 15;
int y = position.y() + 64;
int z = position.z() & 15;
output.setRaw(x, y, z, null);
modifier.preserveSubterrain((position.x() >> 4) << 4, (position.z() >> 4) << 4, output);
NativeBlockState expected = fixture.materials.state(position.x(), position.y(), position.z(), fixture.cell(position));
assertSame(expected, output.getRaw(x, y, z));
output.setRaw(x, y, z, fixture.fallback);
modifier.preserveSubterrain((position.x() >> 4) << 4, (position.z() >> 4) << 4, output);
assertSame(expected, output.getRaw(x, y, z));
}
}
}
@Test
public void realMultiEntryPalettesRestoreDeterministicallyAcrossChunkVisitOrders() throws Exception {
try (Fixture fixture = new Fixture(IrisSubterrainFamily.CENOTE)) {
Map<SubterrainPosition, String> forward = fixture.restoredMaterials(false);
Map<SubterrainPosition, String> reverse = fixture.restoredMaterials(true);
Map<SubterrainPosition, String> repeated = fixture.restoredMaterials(false);
assertEquals(forward, reverse);
assertEquals(forward, repeated);
assertTrue(forward.size() > 100);
assertTrue(forward.values().stream().distinct().count() >= 6);
}
}
@Test
public void missingBiomeRetainsTheShellAndIsLookedUpOncePerResolver() {
try (Fixture fixture = new Fixture(IrisSubterrainFamily.CENOTE)) {
when(fixture.data.getBiomeLoader().load("room-biome")).thenReturn(null);
for (SubterrainPosition position : fixture.surfaces().values()) {
assertSame(fixture.fallback, fixture.materials.state(position.x(), position.y(), position.z(), fixture.cell(position)));
}
verify(fixture.data.getBiomeLoader(), times(1)).load("room-biome");
}
}
@Test
public void unsafeSurfaceChoicesFallBackToTheRetainedSolid() {
try (Fixture fixture = new Fixture(IrisSubterrainFamily.CENOTE)) {
SubterrainPosition position = fixture.surfaces().get(SubterrainSurfaceMaterials.Surface.FLOOR);
NativeBlockState partial = solid("minecraft:oak_slab");
when(partial.isOccluding()).thenReturn(false);
NativeBlockState wet = solid("minecraft:stone[waterlogged=true]");
when(wet.isWaterLogged()).thenReturn(true);
NativeBlockState fluid = solid("minecraft:water");
when(fluid.isFluid()).thenReturn(true);
NativeBlockState custom = solid("minecraft:stone");
when(custom.isCustom()).thenReturn(true);
NativeBlockState gravity = solid("minecraft:gravel");
for (NativeBlockState unsafe : List.of(partial, wet, fluid, custom, gravity)) {
fixture.floorPalette(unsafe);
assertSame(fixture.fallback, fixture.materials.state(position.x(), position.y(), position.z(), fixture.cell(position)));
}
fixture.floorPalette(null);
assertSame(fixture.fallback, fixture.materials.state(position.x(), position.y(), position.z(), fixture.cell(position)));
}
}
private static NativeBlockState solid(String key) {
NativeBlockState state = mock(NativeBlockState.class);
when(state.key()).thenReturn(key);
when(state.isSolid()).thenReturn(true);
when(state.isOccluding()).thenReturn(true);
return state;
}
private static final class Fixture implements AutoCloseable {
private final NativeBlockState fallback = solid("minecraft:stone");
private final NativeBlockState floor = solid("minecraft:clay");
private final NativeBlockState ceiling = solid("minecraft:calcite");
private final NativeBlockState wall = solid("minecraft:andesite");
private final IrisDimension dimension = new IrisDimension();
private final IrisComplex complex = mock(IrisComplex.class);
private final IrisData data = mock(IrisData.class);
private final IrisBiome biome = mock(IrisBiome.class);
private final RNG random = new RNG(44339L);
private final SubterrainPlanner planner;
private final SubterrainPlan plan;
private final List<SubterrainPlan> plans;
private final SubterrainSurfaceMaterials materials;
private final MockedStatic<B> blocks = mockStatic(B.class);
@SuppressWarnings("unchecked")
private Fixture(IrisSubterrainFamily family) {
IrisSubterrainFeature feature = new IrisSubterrainFeature().setId("room").setBiome("room-biome")
.setFamily(family).setProbability(1).setRadius(12).setHeight(16).setLength(200)
.setChimneyHeight(8).setWorldYRange(new IrisRange(-48, 64));
planner = new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 83L, -64, 128));
plan = planner.plansForBounds(-512, -512, 512, 512).getFirst();
plans = planner.plansForBounds(plan.bounds().minX() - 1, plan.bounds().minZ() - 1,
plan.bounds().maxX() + 1, plan.bounds().maxZ() + 1);
ResourceLoader<IrisBiome> loader = mock(ResourceLoader.class);
when(data.getBiomeLoader()).thenReturn(loader);
when(loader.load("room-biome")).thenReturn(biome);
IrisBiomePaletteLayer wallPalette = mock(IrisBiomePaletteLayer.class);
when(biome.getWall()).thenReturn(wallPalette);
when(wallPalette.get(eq(random), anyDouble(), anyDouble(), anyDouble(), eq(data))).thenReturn(wall);
when(biome.generateCeilingLayers(eq(dimension), anyDouble(), anyDouble(), eq(random), eq(1), anyInt(), eq(data), eq(complex)))
.thenReturn(new KList<>(ceiling));
floorPalette(floor);
NativeBlockState air = mock(NativeBlockState.class);
when(air.key()).thenReturn("minecraft:cave_air");
NativeBlockState water = mock(NativeBlockState.class);
when(water.key()).thenReturn("minecraft:water");
NativeBlockState lava = mock(NativeBlockState.class);
when(lava.key()).thenReturn("minecraft:lava");
blocks.when(() -> B.getState(anyString())).thenAnswer(invocation -> switch ((String) invocation.getArgument(0)) {
case "minecraft:water" -> water;
case "minecraft:lava" -> lava;
case "minecraft:cave_air" -> air;
default -> fallback;
});
materials = materials(plans);
}
private Map<SubterrainPosition, String> restoredMaterials(boolean reverse) throws Exception {
IrisBiome authored = new IrisBiome().setLayers(new KList<>(palette("minecraft:clay", "minecraft:terracotta")))
.setCaveCeilingLayers(new KList<>(palette("minecraft:calcite", "minecraft:white_concrete")))
.setWall(palette("minecraft:andesite", "minecraft:tuff"));
when(data.getBiomeLoader().load("room-biome")).thenReturn(authored);
Engine engine = mock(Engine.class);
when(engine.getComplex()).thenReturn(complex);
when(engine.getData()).thenReturn(data);
when(engine.getDimension()).thenReturn(dimension);
when(engine.getWorld()).thenReturn(IrisWorld.builder().minHeight(-64).maxHeight(128).build());
when(complex.getSubterrainPlanner()).thenReturn(planner);
IrisCarveModifier modifier = mock(IrisCarveModifier.class, CALLS_REAL_METHODS);
doReturn(engine).when(modifier).getEngine();
Field randomField = IrisCarveModifier.class.getDeclaredField("rng");
randomField.setAccessible(true);
randomField.set(modifier, random);
List<SubterrainPosition> chunks = new ArrayList<>();
SubterrainBounds bounds = plan.bounds();
for (int x = bounds.minX() >> 4; x <= bounds.maxX() >> 4; x++) {
for (int z = bounds.minZ() >> 4; z <= bounds.maxZ() >> 4; z++) {
chunks.add(new SubterrainPosition(x, 0, z));
}
}
if (reverse) {
Collections.reverse(chunks);
}
Map<SubterrainPosition, String> result = new HashMap<>();
for (SubterrainPosition chunk : chunks) {
Hunk<NativeBlockState> output = Hunk.newArrayHunk(16, 192, 16);
for (int draw = 0; draw < 19; draw++) {
random.nextLong();
}
modifier.preserveSubterrain(chunk.x() << 4, chunk.z() << 4, output);
modifier.preserveSubterrain(chunk.x() << 4, chunk.z() << 4, output);
SubterrainRasterizer.rasterize(planner, chunk.x(), chunk.z(), (x, y, z, cell) -> {
if (cell.solid() && materials.surface(x, y, z, cell) != SubterrainSurfaceMaterials.Surface.INTERIOR) {
result.put(new SubterrainPosition(x, y, z), output.getRaw(x & 15, y + 64, z & 15).key());
}
});
}
return result;
}
private IrisBiomePaletteLayer palette(String first, String second) {
NativeBlockState firstState = solid(first);
NativeBlockState secondState = solid(second);
blocks.when(() -> B.getStateOrNull(first, false)).thenReturn(firstState);
blocks.when(() -> B.getStateOrNull(second, false)).thenReturn(secondState);
return new IrisBiomePaletteLayer().setZoom(1)
.setPalette(new KList<>(new IrisBlockData(first), new IrisBlockData(second)));
}
private void floorPalette(NativeBlockState state) {
when(biome.generateLayers(eq(dimension), anyDouble(), anyDouble(), eq(random), eq(1), anyInt(), eq(data), eq(complex)))
.thenReturn(state == null ? new KList<>() : new KList<>(state));
}
private SubterrainSurfaceMaterials materials(List<SubterrainPlan> candidates) {
return new SubterrainSurfaceMaterials(planner, candidates, dimension, complex, data, random, -64);
}
private SubterrainCell cell(SubterrainPosition position) {
return planner.sample(plans, position.x(), position.y(), position.z());
}
private Map<SubterrainSurfaceMaterials.Surface, SubterrainPosition> surfaces() {
Map<SubterrainSurfaceMaterials.Surface, SubterrainPosition> result = new EnumMap<>(SubterrainSurfaceMaterials.Surface.class);
SubterrainBounds bounds = plan.bounds();
for (int x = bounds.minX(); x <= bounds.maxX(); x++) {
for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) {
for (int y = bounds.minY(); y <= bounds.maxY(); y++) {
SubterrainCell cell = planner.sample(plans, x, y, z);
if (!cell.solid()) {
continue;
}
result.putIfAbsent(materials.surface(x, y, z, cell), new SubterrainPosition(x, y, z));
if (result.size() == SubterrainSurfaceMaterials.Surface.values().length) {
return result;
}
}
}
}
return result;
}
@Override
public void close() {
blocks.close();
}
}
}
@@ -116,7 +116,7 @@ public class IrisDimensionCarvingResolverParityTest {
doReturn(generator).when(parent).getChildrenGenerator(anyLong(), any(IrisData.class));
IrisDimensionCarvingResolver.Snapshot snapshot = IrisDimensionCarvingResolver.snapshot(fixture.engine);
assertSame(fixture.engine.getData().getBiomeLoader().load("child-a"), snapshot.resolveBiome(-17, 40, 23));
assertSame(fixture.engine.getData().getBiomeLoader().load("root-low"), snapshot.resolveBiome(-17, 40, 23));
assertSame(fixture.engine.getData().getBiomeLoader().load("root-low"), snapshot.resolveBiome(-17, 41, 23));
verify(generator, times(2)).noiseFast2D(-17D, 23D);
}
@@ -140,13 +140,30 @@ public class IrisDimensionCarvingResolverParityTest {
doReturn(generator).when(parent).getChildrenGenerator(anyLong(), any(IrisData.class));
IrisDimensionCarvingEntry selected = IrisDimensionCarvingResolver.resolveFromRoot(
engine, parent, -17, 23, new IrisDimensionCarvingResolver.State());
engine, parent, -17, -1500, 23, new IrisDimensionCarvingResolver.State());
assertSame(engine.getDimension().getCarvingEntryIndex().get("child-a"), selected);
verify(generator).noiseFast2D(-17D, 23D);
verify(generator, never()).fit2D(anyInt(), anyInt(), anyDouble(), anyDouble());
}
@Test
public void childWorldYBandIsCheckedAndCachedPerHeight() {
Fixture fixture = createFixture();
IrisDimensionCarvingEntry parent = spy(fixture.engine.getDimension().getCarvingEntryIndex().get("root-low"));
parent.setChildRecursionDepth(1);
parent.setChildren(new KList<>("child-a"));
IrisDimensionCarvingEntry child = fixture.engine.getDimension().getCarvingEntryIndex().get("child-a");
child.setWorldYRange(new IrisRange(-20, 20));
CNG generator = mock(CNG.class);
doReturn(0.5D).when(generator).noiseFast2D(anyDouble(), anyDouble());
doReturn(generator).when(parent).getChildrenGenerator(anyLong(), any(IrisData.class));
IrisDimensionCarvingResolver.State state = new IrisDimensionCarvingResolver.State();
assertSame(child, IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, parent, 7, 0, 9, state));
assertSame(parent, IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, parent, 7, 30, 9, state));
assertSame(child, IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, parent, 7, 0, 9, state));
}
@Test
public void repeatedColumnResolutionSamplesChildrenOnceAcrossHeights() {
Fixture fixture = createFixture();
@@ -161,7 +178,7 @@ public class IrisDimensionCarvingResolverParityTest {
for (int height = 0; height < 16; height++) {
for (int x = -16; x < 0; x++) {
for (int z = 0; z < 16; z++) {
assertSame(child, IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, parent, x, z, state));
assertSame(child, IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, parent, x, -1500, z, state));
}
}
}
@@ -186,8 +203,8 @@ public class IrisDimensionCarvingResolverParityTest {
IrisDimensionCarvingEntry child = fixture.engine.getDimension().getCarvingEntryIndex().get("child-a");
IrisDimensionCarvingResolver.State state = new IrisDimensionCarvingResolver.State();
assertSame(child, IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, parent, -17, 23, state));
assertSame(parent, IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, parent, -17, 23, state));
assertSame(child, IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, parent, -17, -1500, 23, state));
assertSame(parent, IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, parent, -17, -1500, 23, state));
}
}
@@ -204,11 +221,11 @@ public class IrisDimensionCarvingResolverParityTest {
IrisDimensionCarvingEntry firstChild = fixture.engine.getDimension().getCarvingEntryIndex().get("child-a");
IrisDimensionCarvingEntry replacementChild = replacement.engine.getDimension().getCarvingEntryIndex().get("child-a");
assertSame(firstChild, IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, parent, -17, 23, state));
assertSame(firstChild, IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, parent, -17, -1500, 23, state));
doReturn(replacement.engine.getData()).when(fixture.engine).getData();
assertSame(firstChild, IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, parent, -17, 23, state));
assertSame(replacementChild, IrisDimensionCarvingResolver.resolveFromRoot(replacement.engine, parent, -17, 23, state));
assertSame(replacementChild, IrisDimensionCarvingResolver.resolveFromRoot(replacement.engine, parent, -17, 23, state));
assertSame(firstChild, IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, parent, -17, -1500, 23, state));
assertSame(replacementChild, IrisDimensionCarvingResolver.resolveFromRoot(replacement.engine, parent, -17, -1500, 23, state));
assertSame(replacementChild, IrisDimensionCarvingResolver.resolveFromRoot(replacement.engine, parent, -17, -1500, 23, state));
verify(generator, times(3)).noiseFast2D(-17D, 23D);
}
@@ -225,8 +242,8 @@ public class IrisDimensionCarvingResolverParityTest {
for (int worldX = -384; worldX <= 384; worldX += 29) {
for (int worldZ = -384; worldZ <= 384; worldZ += 31) {
IrisDimensionCarvingEntry legacyResolved = legacyResolveFromRoot(fixture.engine, legacyRoot, worldX, worldZ);
IrisDimensionCarvingEntry statefulResolved = IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, statefulRoot, worldX, worldZ, state);
IrisDimensionCarvingEntry legacyResolved = legacyResolveFromRoot(fixture.engine, legacyRoot, worldX, worldY, worldZ);
IrisDimensionCarvingEntry statefulResolved = IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, statefulRoot, worldX, worldY, worldZ, state);
assertSame("entry mismatch at worldY=" + worldY + " worldX=" + worldX + " worldZ=" + worldZ, legacyResolved, statefulResolved);
assertSame(legacyResolveEntryBiome(fixture.engine, legacyResolved),
IrisDimensionCarvingResolver.resolveBiome(fixture.engine, worldX, worldY, worldZ, state));
@@ -248,8 +265,8 @@ public class IrisDimensionCarvingResolverParityTest {
for (int worldX = -640; worldX <= 640; worldX += 79) {
for (int worldZ = -640; worldZ <= 640; worldZ += 83) {
IrisDimensionCarvingEntry legacyResolved = legacyResolveFromRoot(fixture.engine, legacyRoot, worldX, worldZ);
IrisDimensionCarvingEntry statefulResolved = IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, statefulRoot, worldX, worldZ, state);
IrisDimensionCarvingEntry legacyResolved = legacyResolveFromRoot(fixture.engine, legacyRoot, worldX, worldY, worldZ);
IrisDimensionCarvingEntry statefulResolved = IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, statefulRoot, worldX, worldY, worldZ, state);
assertSame("mixed entry mismatch at worldY=" + worldY + " worldX=" + worldX + " worldZ=" + worldZ, legacyResolved, statefulResolved);
assertSame(legacyResolveEntryBiome(fixture.engine, legacyResolved),
IrisDimensionCarvingResolver.resolveBiome(fixture.engine, worldX, worldY, worldZ, state));
@@ -406,7 +423,7 @@ public class IrisDimensionCarvingResolverParityTest {
Fixture replacement = createMixedDepthFixture();
IrisDimensionCarvingEntry expectedEntry = legacyResolveFromRoot(replacement.engine,
legacyResolveRootEntry(replacement.engine, 83), -17, 23);
legacyResolveRootEntry(replacement.engine, 83), -17, 83, 23);
IrisBiome expected = legacyResolveEntryBiome(replacement.engine, expectedEntry);
assertSame(expected, IrisDimensionCarvingResolver.resolveBiome(replacement.engine, -17, 83, 23, state));
assertSame(null, IrisDimensionCarvingResolver.resolveBiome(fixture.engine, -17, 83, 23, state));
@@ -692,7 +709,7 @@ public class IrisDimensionCarvingResolverParityTest {
for (int localZ = 0; localZ < 16; localZ++) {
int worldZ = (chunkZ << 4) + localZ;
int columnIndex = (localX << 4) | localZ;
plan[columnIndex] = IrisDimensionCarvingResolver.resolveFromRoot(engine, rootEntry, worldX, worldZ, state);
plan[columnIndex] = IrisDimensionCarvingResolver.resolveFromRoot(engine, rootEntry, worldX, 40, worldZ, state);
}
}
return plan;
@@ -728,7 +745,7 @@ public class IrisDimensionCarvingResolverParityTest {
return resolved;
}
private IrisDimensionCarvingEntry legacyResolveFromRoot(Engine engine, IrisDimensionCarvingEntry rootEntry, int worldX, int worldZ) {
private IrisDimensionCarvingEntry legacyResolveFromRoot(Engine engine, IrisDimensionCarvingEntry rootEntry, int worldX, int worldY, int worldZ) {
if (rootEntry == null) {
return null;
}
@@ -748,7 +765,8 @@ public class IrisDimensionCarvingResolverParityTest {
int depth = remainingDepth;
while (depth > 0) {
IrisDimensionCarvingEntry selected = legacySelectChild(engine, current, worldX, worldZ, entryIndex);
if (selected == null || selected == current) {
if (selected == null || selected == current || selected.getWorldYRange() != null
&& !selected.getWorldYRange().contains(worldY)) {
break;
}
@@ -78,7 +78,7 @@ public class SchemaBuilderIdentityTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
private static final String EXPECTED_SCHEMA_DIGEST = "d77cc4ea8255cbfdc4f8c49e04a3c422ca3e094399a514799ec703217f7797da";
private static final String EXPECTED_SCHEMA_DIGEST = "6f6dae0cd1077c45fe3f5ba512c1da4b0ab43bc8516f2d759bc8f7a798eb1c58";
private static final List<Class<?>> SCHEMA_ROOTS = List.of(
IrisDimension.class,
@@ -0,0 +1,55 @@
package art.arcane.iris.platform.bukkit.nms;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.runtime.DimensionStackLayout;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisDimensionCarvingResolver;
import art.arcane.iris.world.IrisWorld;
import org.junit.Test;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.mockito.Answers.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
public class BukkitSubterrainBiomePrecedenceTest {
@Test
public void featureOwnsPublishedBiomeAboveDepthGateAndBeforeStackOwnership() throws Exception {
Engine engine = mock(Engine.class);
IrisBiome feature = mock(IrisBiome.class);
IrisDimension host = new IrisDimension();
when(engine.getComplex()).thenReturn(mock(IrisComplex.class));
when(engine.getWorld()).thenReturn(IrisWorld.builder().minHeight(-64).maxHeight(320).build());
when(engine.getDimension()).thenReturn(host);
when(engine.getSubterrainBiome(12, 224, -20)).thenReturn(feature);
@SuppressWarnings("unchecked")
BukkitBiomePolicy<String, String> policy = mock(BukkitBiomePolicy.class, CALLS_REAL_METHODS);
Field owner = BukkitBiomePolicy.class.getDeclaredField("engine");
owner.setAccessible(true);
owner.set(policy, engine);
DimensionStackLayout stack = mock(DimensionStackLayout.class);
Method resolve = BukkitBiomePolicy.class.getDeclaredMethod("resolveBiomeResolution", int.class, int.class,
int.class, IrisDimensionCarvingResolver.State.class, boolean.class, DimensionStackLayout.class);
resolve.setAccessible(true);
Object resolved = resolve.invoke(policy, 3, 40, -5, null, true, stack);
assertSame(feature, field(resolved, "irisBiome"));
assertSame(host, field(resolved, "dimension"));
assertEquals(true, field(resolved, "underground"));
verify(engine).getSubterrainBiome(12, 224, -20);
verifyNoInteractions(stack);
}
private static Object field(Object object, String name) throws Exception {
Field field = object.getClass().getDeclaredField(name);
field.setAccessible(true);
return field.get(object);
}
}
@@ -1,6 +1,15 @@
package art.arcane.iris.world.history;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.DimensionStackContext;
import art.arcane.iris.generation.runtime.DimensionStackLayout;
import art.arcane.iris.generation.subterrain.SubterrainCell;
import art.arcane.iris.generation.subterrain.IrisSubterrainFeature;
import art.arcane.iris.generation.subterrain.SubterrainPlan;
import art.arcane.iris.generation.subterrain.SubterrainPlanner;
import art.arcane.iris.generation.subterrain.SubterrainPosition;
import java.util.List;
import art.arcane.iris.generation.terrain.InferredType;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.terrain.IrisRegion;
@@ -77,6 +86,7 @@ public class SavedBiomeCaptureTest {
@Test
public void retainsFloatingChildIdentityWhenItSharesTheHostDerivative() throws Exception {
Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS);
when(engine.getComplex().getSubterrainPlanner()).thenReturn(null);
GenerationHistory.GenerationStage stage = mock(GenerationHistory.GenerationStage.class);
SavedBiomeRuntime historical = mock(SavedBiomeRuntime.class);
IrisBiome host = biome("host");
@@ -118,6 +128,7 @@ public class SavedBiomeCaptureTest {
@Test
public void recordsExactSurfaceColumnsAndIndependentCaveAndVerticalIdentities() throws Exception {
Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS);
when(engine.getComplex().getSubterrainPlanner()).thenReturn(null);
GenerationHistory.GenerationStage stage = mock(GenerationHistory.GenerationStage.class);
SavedBiomeRuntime historical = mock(SavedBiomeRuntime.class);
IrisBiome left = biome("left");
@@ -155,12 +166,117 @@ public class SavedBiomeCaptureTest {
verifyNoInteractions(historical);
}
@Test
public void preservesHostFeatureBiomeAndRegionAgainstStackAndFloatingOverlay() throws Exception {
Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS);
when(engine.getComplex().getSubterrainPlanner()).thenReturn(null);
IrisBiome upper = biome("upper-biome");
IrisBiome authored = biome("authored-biome");
IrisRegion upperRegion = region("upper-region");
IrisRegion hostRegion = region("host-region");
DimensionStackContext stack = mock(DimensionStackContext.class);
DimensionStackLayout layout = mock(DimensionStackLayout.class);
DimensionStackLayout.Layer layer = mock(DimensionStackLayout.Layer.class);
when(engine.getDimensionStackContext()).thenReturn(stack);
when(stack.getLayout(anyInt(), anyInt())).thenReturn(layout);
when(layout.layerAt(anyInt())).thenReturn(layer);
when(layer.region()).thenReturn(upperRegion);
when(engine.getComplex().getRegionStream().get(anyDouble(), anyDouble())).thenReturn(hostRegion);
when(engine.getComplex().getTransitionGenerationPlan()).thenReturn(null);
when(engine.getMinHeight()).thenReturn(-64);
when(engine.getHeight()).thenReturn(16);
when(engine.getRegion(anyInt(), anyInt())).thenReturn(upperRegion);
doCallRealMethod().when(engine).getRegion(anyInt(), anyInt(), anyInt());
when(engine.getSubterrainCell(anyInt(), anyInt(), anyInt())).thenAnswer(call ->
(int) call.getArgument(1) == 8 ? new SubterrainCell(SubterrainCell.Kind.AIR, "", null) : SubterrainCell.OUTSIDE);
when(engine.getSurfaceBiome(anyInt(), anyInt())).thenReturn(upper);
when(engine.getCaveBiome(anyInt(), anyInt())).thenReturn(upper);
when(engine.getBiomeOrMantle(anyInt(), anyInt(), anyInt())).thenAnswer(call ->
(int) call.getArgument(1) == 8 ? authored : upper);
columnsFromPointLookups(engine);
FloatingBiomeOverlay floating = new FloatingBiomeOverlay(16);
floating.record(0, 8, 0, new FloatingBiomeOverlay.Identity("upper-biome", "upper-region"));
SavedBiomeChunk chunk = SavedBiomeCapture.capture(engine, stage(), mock(SavedBiomeRuntime.class), floating);
assertEquals("authored-biome", chunk.biomeAt(0, -56, 0).biomeKey());
assertEquals("host-region", chunk.biomeAt(0, -56, 0).regionKey());
assertEquals("upper-region", chunk.biomeAt(0, -60, 0).regionKey());
assertEquals("upper-region", chunk.surfaceAt(0, 0).regionKey());
}
@Test
public void capturesFeatureBoundariesAtExactBlockCoordinates() throws Exception {
IrisSubterrainFeature feature = new IrisSubterrainFeature().setId("exact-basin").setBiome("authored-biome")
.setProbability(1).setPillarSpacing(0).setFormationFraction(0);
SubterrainPlanner planner = new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 1191L, -64, 320));
SubterrainPlan plan = planner.plansForBounds(0, 0, 512, 512).getFirst();
SubterrainPosition anchor = plan.anchor();
int edgeX = plan.bounds().minX();
while (!planner.sample(edgeX, anchor.y(), anchor.z()).occupied()) {
edgeX++;
}
int chunkX = Math.floorDiv(edgeX, 16);
int chunkZ = Math.floorDiv(anchor.z(), 16);
Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS);
IrisBiome fallback = biome("base-biome");
IrisBiome authored = biome("authored-biome");
IrisRegion host = region("host-region");
IrisRegion stacked = region("stacked-region");
DimensionStackContext stack = mock(DimensionStackContext.class);
DimensionStackLayout layout = mock(DimensionStackLayout.class);
DimensionStackLayout.Layer layer = mock(DimensionStackLayout.Layer.class);
when(engine.getDimensionStackContext()).thenReturn(stack);
when(stack.getLayout(anyInt(), anyInt())).thenReturn(layout);
when(layout.layerAt(anyInt())).thenReturn(layer);
when(layer.region()).thenReturn(stacked);
when(engine.getComplex().getRegionStream().get(anyDouble(), anyDouble())).thenReturn(host);
when(engine.getComplex().getSubterrainPlanner()).thenReturn(planner);
when(engine.getComplex().getTransitionGenerationPlan()).thenReturn(null);
when(engine.getMinHeight()).thenReturn(-64);
when(engine.getHeight()).thenReturn(384);
when(engine.getRegion(anyInt(), anyInt())).thenReturn(stacked);
when(engine.getSubterrainCell(anyInt(), anyInt(), anyInt())).thenAnswer(call ->
planner.sample(call.getArgument(0), (int) call.getArgument(1) - 64, call.getArgument(2)));
doCallRealMethod().when(engine).getRegion(anyInt(), anyInt(), anyInt());
when(engine.getSurfaceBiome(anyInt(), anyInt())).thenReturn(fallback);
when(engine.getCaveBiome(anyInt(), anyInt())).thenReturn(fallback);
when(engine.getBiomeOrMantle(anyInt(), anyInt(), anyInt())).thenAnswer(call ->
planner.sample(call.getArgument(0), (int) call.getArgument(1) - 64, call.getArgument(2)).occupied()
? authored : fallback);
columnsFromPointLookups(engine);
GenerationHistory.GenerationStage stage = stage();
when(stage.chunkX()).thenReturn(chunkX);
when(stage.chunkZ()).thenReturn(chunkZ);
SavedBiomeChunk chunk = SavedBiomeCapture.capture(engine, stage, mock(SavedBiomeRuntime.class), null);
boolean nonQuartOccupied = false;
boolean outsideOccupiedVolume = false;
for (int localX = 0; localX < 16; localX++) {
for (int localZ = 0; localZ < 16; localZ++) {
for (int y = plan.bounds().minY(); y <= plan.bounds().maxY(); y++) {
boolean occupied = planner.sample(chunkX * 16 + localX, y, chunkZ * 16 + localZ).occupied();
SavedBiomeChunk.Cell recorded = chunk.biomeAt(localX, y, localZ);
assertEquals(occupied ? "authored-biome" : "base-biome", recorded.biomeKey());
assertEquals(occupied ? "host-region" : "stacked-region", recorded.regionKey());
nonQuartOccupied |= occupied && (localX % 4 != 0 || localZ % 4 != 0 || Math.floorMod(y, 4) != 0);
outsideOccupiedVolume |= !occupied;
}
}
}
assertTrue(nonQuartOccupied);
assertTrue(outsideOccupiedVolume);
}
private static Engine caveCaptureEngine(IrisBiome surface, IrisBiome cave) {
Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS);
when(engine.getComplex().getSubterrainPlanner()).thenReturn(null);
IrisRegion region = region("flat-region");
when(engine.getMinHeight()).thenReturn(-64);
when(engine.getHeight()).thenReturn(16);
when(engine.getHeight(anyInt(), anyInt())).thenReturn(8);
when(engine.getSubterrainBiome(anyInt(), anyInt(), anyInt())).thenReturn(null);
when(engine.getDimensionStackContext()).thenReturn(null);
when(engine.getComplex().getTransitionGenerationPlan()).thenReturn(null);
when(engine.getComplex().getCaveBiomeStream().get(anyDouble(), anyDouble())).thenReturn(cave);
@@ -0,0 +1,97 @@
package art.arcane.iris.world.history;
import art.arcane.iris.generation.locator.SubterrainLocator;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.generation.subterrain.IrisSubterrainFeature;
import art.arcane.iris.generation.subterrain.SubterrainPlan;
import art.arcane.iris.generation.subterrain.SubterrainPlanner;
import art.arcane.iris.generation.subterrain.SubterrainPosition;
import org.junit.Test;
import java.util.List;
import java.util.Optional;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class SavedSubterrainQueriesTest {
@Test
public void locateUsesRetainedDefinitionOnlyInsideItsOwnedChunks() throws Exception {
IrisEngine engine = mock(IrisEngine.class);
IrisComplex complex = mock(IrisComplex.class);
GenerationHistory history = mock(GenerationHistory.class);
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
SavedBiomeRuntime saved = mock(SavedBiomeRuntime.class);
GenerationManifest manifest = mock(GenerationManifest.class);
GenerationActivation old = GenerationActivation.initial("a".repeat(64), 1L);
GenerationActivation active = GenerationActivation.next(2L, "b".repeat(64), 1L, 2L, 64);
SubterrainPlanner historical = planner("old-basin");
SubterrainPlan plan = historical.plansForBounds(0, 0, 511, 511).getFirst();
SubterrainPosition anchor = plan.anchor();
when(engine.getComplex()).thenReturn(complex);
when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
when(router.history()).thenReturn(history);
when(router.biomes()).thenReturn(saved);
when(history.manifest()).thenReturn(manifest);
when(manifest.activations()).thenReturn(List.of(old, active));
when(manifest.pendingActivation()).thenReturn(Optional.empty());
when(saved.subterrainPlanner(1L)).thenReturn(historical);
when(saved.subterrainPlanner(2L)).thenReturn(planner("new-basin"));
when(history.resolveActivation(anyInt(), anyInt())).thenReturn(active);
when(history.resolveActivation(Math.floorDiv(anchor.x(), 16), Math.floorDiv(anchor.z(), 16))).thenReturn(old);
SubterrainLocator.Query query = new SubterrainLocator.Query("old-basin", null, "");
SubterrainLocator.Result found = GenerationSemanticQueries.nearestSubterrain(engine, query,
anchor.x(), anchor.y(), anchor.z(), 256).orElseThrow();
assertEquals(plan.id(), found.featureId());
assertEquals(anchor.y(), found.y());
verify(engine, never()).getMantle();
verify(router, never()).openRoute(anyInt(), anyInt());
verify(router, never()).openReadScope(anyInt(), anyInt());
when(history.resolveActivation(anyInt(), anyInt())).thenReturn(active);
assertTrue(GenerationSemanticQueries.nearestSubterrain(engine, query,
anchor.x(), anchor.y(), anchor.z(), 256).isEmpty());
}
@Test
public void newPredictionCannotCrossProtectedGenerationTransition() throws Exception {
IrisEngine engine = mock(IrisEngine.class);
IrisComplex complex = mock(IrisComplex.class);
GenerationHistory history = mock(GenerationHistory.class);
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
SavedBiomeRuntime saved = mock(SavedBiomeRuntime.class);
GenerationManifest manifest = mock(GenerationManifest.class);
GenerationActivation active = GenerationActivation.initial("a".repeat(64), 1L);
SubterrainPlanner planner = planner("basin");
SubterrainPosition anchor = planner.plansForBounds(0, 0, 511, 511).getFirst().anchor();
when(engine.getComplex()).thenReturn(complex);
when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
when(router.history()).thenReturn(history);
when(router.biomes()).thenReturn(saved);
when(history.manifest()).thenReturn(manifest);
when(manifest.activations()).thenReturn(List.of(active));
when(manifest.pendingActivation()).thenReturn(Optional.empty());
when(saved.subterrainPlanner(1L)).thenReturn(planner);
when(history.resolveActivation(anyInt(), anyInt())).thenReturn(active);
when(history.isActiveUnowned(anyInt(), anyInt())).thenReturn(true);
SubterrainLocator.Query query = new SubterrainLocator.Query("basin", null, "");
assertTrue(GenerationSemanticQueries.nearestSubterrain(engine, query,
anchor.x(), anchor.y(), anchor.z(), 64).isEmpty());
when(complex.allowsNewDiscreteContentAt(anchor.x(), anchor.z())).thenReturn(true);
assertTrue(GenerationSemanticQueries.nearestSubterrain(engine, query,
anchor.x(), anchor.y(), anchor.z(), 64).isPresent());
verify(engine, never()).getMantle();
}
private static SubterrainPlanner planner(String id) {
IrisSubterrainFeature feature = new IrisSubterrainFeature().setId(id).setProbability(1D);
return new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 1191L, -64, 320));
}
}