mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-25 21:40:05 +00:00
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:
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 {
|
||||
|
||||
+44
@@ -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;
|
||||
}
|
||||
}
|
||||
+62
@@ -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));
|
||||
}
|
||||
}
|
||||
+71
@@ -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);
|
||||
|
||||
+60
@@ -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);
|
||||
|
||||
+279
@@ -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) {
|
||||
}
|
||||
}
|
||||
+18
@@ -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();
|
||||
|
||||
+1
@@ -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,
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+64
@@ -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();
|
||||
}
|
||||
}
|
||||
+45
-3
@@ -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);
|
||||
|
||||
+116
@@ -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"));
|
||||
}
|
||||
}
|
||||
+129
@@ -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)));
|
||||
}
|
||||
}
|
||||
+304
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+35
-17
@@ -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,
|
||||
|
||||
+55
@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user