mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 05:50:39 +00:00
Volumetric surface terrain
- Add an optional per-biome `terrain3D` profile with density and fissure noise styles, amplitude, horizontal and vertical feature scales, crack depth, width and scale, slope and fluid-clearance gates, and a seed offset, each range-checked before use - Add a terrain runtime that samples the signed density and fissure fields on a four-block lattice around the biome's generated height and resolves each column into solid spans, so surfaces gain projecting rock, undercuts, ledges and narrow cracks that fade out on flat ground and near the fluid line - Remove unsupported rock by flood-filling span components across neighbouring columns and dropping any fragment of 512 blocks or fewer, while larger connected masses are kept - Report the shaped column top as the natural terrain height, keep the unshaped generator height available as a separate stream, and measure surface slope between neighbouring ledge floors instead of the flat heightmap - Write air through the gaps in the normal terrain actuator, generate a fresh surface palette for every ledge floor, and paint ceiling layers beneath undercuts - Carry each layer's own column through the dimension stack and upper dimension so stack top heights, solidity and surface lookups follow the spans, and each exposed face draws its palette from its own source height and slope - Sample a layer's slope condition once per column at the surface being generated, and gate surface decorators on the slope of the ledge they sit on rather than the column heightmap - Decorate every exposed ledge floor and the ceiling above it during the decorant pass - Treat shaped openings as surface rather than cave when resolving biomes, skip them during cave carving, suppress cave zone markers inside them, and resolve their boundary biome from the surface biome stream - Include shaped openings in mantle carve queries and carved columns, fall back to terrain solidity for surface-solid checks across stack, upper and normal terrain, and answer both queries from the island mask when placing objects on floating islands - Let the hydrology planned surface report whether it owns a column, so the cave voxel view consults natural terrain solidity where hydrology does not own the terrain, and split replayed plan observations into height and ownership records - Validate `terrain3D` on biomes and `snippet/terrain-3d` snippets as a blocking pack error, rejecting unknown fields, wrong types, out-of-range numbers, unknown noise styles, style nesting past 32 levels and snippet references that escape the pack - Copy the whole snippet tree during modded studio pack export through one shared routine instead of only `snippet/style`, so terrain and nested style snippets survive export - Add a terrain probe and Gradle task that generates real pack chunks, compares predicted column geometry against the actual blocks, requires retained overhang coverage for named biomes, reports detached-component topology, and writes cross-section images with a JSON summary - Enumerate image-mapped regions and skip compat-excluded ones when collecting biomes for stacked and upper dimension terrain
This commit is contained in:
1 parent
7841efed54
commit
c39a6403bc
63 files changed
+4761
-145
No files matched your search
@@ -1,12 +1,16 @@
|
||||
package art.arcane.iris.core.pack;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisDimensionStack;
|
||||
import art.arcane.iris.engine.object.IrisObjectMarker;
|
||||
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.engine.object.IrisStaticObject;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@@ -14,9 +18,57 @@ 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;
|
||||
|
||||
public class PackExportClosureTest {
|
||||
@Rule
|
||||
public final TemporaryFolder temporary = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
public void exportedSnippetsLoadTerrainAndNestedStylesFromTheTargetPack() throws Exception {
|
||||
Path source = temporary.newFolder("source").toPath();
|
||||
Path target = temporary.newFolder("target").toPath();
|
||||
Path terrain = source.resolve("snippet/terrain-3d/cliffs/main.json");
|
||||
Path density = source.resolve("snippet/style/cliffs/density.json");
|
||||
Path fracture = source.resolve("snippet/style/cliffs/fracture.json");
|
||||
Path decorator = source.resolve("snippet/decorator/ledge.json");
|
||||
for (Path file : List.of(terrain, density, fracture, decorator)) {
|
||||
Files.createDirectories(file.getParent());
|
||||
}
|
||||
Files.writeString(terrain, "{\"amplitude\":57,\"densityStyle\":\"snippet/style/cliffs/density\"}");
|
||||
Files.writeString(density, "{\"style\":\"SIMPLEX\",\"zoom\":1.5,\"fracture\":\"snippet/style/cliffs/fracture\"}");
|
||||
Files.writeString(fracture, "{\"style\":\"PERLIN\",\"zoom\":2}");
|
||||
Files.writeString(decorator, "{\"chance\":0.2}");
|
||||
Files.writeString(source.resolve("snippet/notes.txt"), "not pack data");
|
||||
|
||||
String hash = PackExportClosure.copySnippets(source.toFile(), target.toFile());
|
||||
|
||||
assertFalse(hash.isEmpty());
|
||||
assertEquals(hash, PackExportClosure.copySnippets(source.toFile(), target.toFile()));
|
||||
assertTrue(Files.isRegularFile(target.resolve("snippet/decorator/ledge.json")));
|
||||
assertFalse(Files.exists(target.resolve("snippet/notes.txt")));
|
||||
IrisData data = IrisData.openDatapackCompiler(target.toFile());
|
||||
try {
|
||||
IrisBiome biome = data.getGson().fromJson(
|
||||
"{\"terrain3D\":\"snippet/terrain-3d/cliffs/main\"}", IrisBiome.class);
|
||||
assertEquals(57D, biome.getTerrain3D().getAmplitude(), 0D);
|
||||
assertEquals(1.5D, biome.getTerrain3D().getDensityStyle().getZoom(), 0D);
|
||||
assertEquals(2D, biome.getTerrain3D().getDensityStyle().getFracture().getZoom(), 0D);
|
||||
} finally {
|
||||
data.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void packsWithoutSnippetsDoNotCreateAnExportFolder() throws Exception {
|
||||
Path source = temporary.newFolder("without-snippets").toPath();
|
||||
Path target = temporary.getRoot().toPath().resolve("absent-export");
|
||||
|
||||
assertEquals("", PackExportClosure.copySnippets(source.toFile(), target.toFile()));
|
||||
assertFalse(Files.exists(target));
|
||||
}
|
||||
|
||||
private static IrisObjectPlacement placement(String objectKey, String... markerKeys) {
|
||||
IrisObjectPlacement placement = new IrisObjectPlacement();
|
||||
placement.setPlace(new KList<>(objectKey));
|
||||
@@ -139,8 +191,8 @@ public class PackExportClosureTest {
|
||||
assertTrue("modded packager must export expression-backed stack blend styles",
|
||||
modded.contains("dm.getExpressionLoader().getPossibleKeys()")
|
||||
&& modded.contains("\"expressions\""));
|
||||
assertTrue("modded packager must retain referenced style snippets",
|
||||
modded.contains("copyStyleSnippets(packFolder, folder)"));
|
||||
assertTrue("modded packager must retain all snippet types and nested references",
|
||||
modded.contains("PackExportClosure.copySnippets(packFolder, folder)"));
|
||||
int moddedValidation = modded.indexOf("PackValidator.validateForPackaging(packFolder)");
|
||||
assertTrue("modded packager must validate before mutating package state",
|
||||
moddedValidation >= 0 && moddedValidation < modded.indexOf("IO.delete(folder)"));
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
package art.arcane.iris.core.pack;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class PackTerrain3DValidatorTest {
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
public void acceptsInlineProfilesAndReferencedProfileAndStyleSnippets() throws Exception {
|
||||
File pack = temporaryFolder.newFolder();
|
||||
write(pack, "biomes/mountains.json", """
|
||||
{"terrain3D":{"amplitude":64,"verticalScale":16,"densityStyle":{"style":"SIMPLEX"}}}
|
||||
""");
|
||||
write(pack, "biomes/cliffs.json", """
|
||||
{"terrain3D":"snippet/terrain-3d/cliffs"}
|
||||
""");
|
||||
write(pack, "snippet/terrain-3d/cliffs.json", """
|
||||
{"$schema":"../../schema.json","crackDepth":32,"densityStyle":"snippet/style/terrain"}
|
||||
""");
|
||||
write(pack, "snippet/style/terrain.json", """
|
||||
{"style":"SIMPLEX","fracture":"snippet/style/warp"}
|
||||
""");
|
||||
write(pack, "snippet/style/warp.json", """
|
||||
{"style":"SIMPLEX","zoom":2,"multiplier":12}
|
||||
""");
|
||||
|
||||
assertTrue(PackTerrain3DValidator.validate(pack).toString(), PackTerrain3DValidator.validate(pack).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptsOmissionAndNullWithoutEnablingProfiles() throws Exception {
|
||||
File pack = temporaryFolder.newFolder();
|
||||
write(pack, "biomes/plain.json", "{}");
|
||||
write(pack, "biomes/ocean.json", "{\"terrain3D\":null}");
|
||||
write(pack, "biomes/disabled.json", "{\"terrain3D\":{\"enabled\":false}}");
|
||||
|
||||
assertTrue(PackTerrain3DValidator.validate(pack).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsUnknownFieldsAndNonBooleanEnableFlags() throws Exception {
|
||||
File pack = temporaryFolder.newFolder();
|
||||
write(pack, "biomes/unknown.json", "{\"terrain3D\":{\"amplitdue\":64}}");
|
||||
write(pack, "biomes/boolean.json", "{\"terrain3D\":{\"enabled\":\"true\"}}");
|
||||
write(pack, "biomes/array.json", "{\"terrain3D\":[]}");
|
||||
write(pack, "biomes/numeric.json", "{\"terrain3D\":{\"amplitude\":\"64\"}}");
|
||||
|
||||
List<String> errors = PackTerrain3DValidator.validate(pack);
|
||||
assertTrue(errors.stream().anyMatch(error -> error.contains("amplitdue is not a terrain3D field")));
|
||||
assertTrue(errors.stream().anyMatch(error -> error.contains("enabled must be a JSON boolean")));
|
||||
assertTrue(errors.stream().anyMatch(error -> error.contains("terrain3D must be an object or snippet reference")));
|
||||
assertTrue(errors.stream().anyMatch(error -> error.contains("amplitude must be a finite JSON number")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void appliesModelBoundsToInlineAndDisabledSnippetProfiles() throws Exception {
|
||||
File pack = temporaryFolder.newFolder();
|
||||
write(pack, "biomes/amplitude.json", "{\"terrain3D\":{\"amplitude\":129}}");
|
||||
write(pack, "snippet/terrain-3d/disabled.json", "{\"enabled\":false,\"verticalScale\":0}");
|
||||
write(pack, "biomes/nonfinite.json", "{\"terrain3D\":{\"crackDepth\":1e999}}");
|
||||
write(pack, "biomes/seed.json", "{\"terrain3D\":{\"seed\":1.5}}");
|
||||
write(pack, "biomes/huge-seed.json", "{\"terrain3D\":{\"seed\":9223372036854775808}}");
|
||||
|
||||
List<String> errors = PackTerrain3DValidator.validate(pack);
|
||||
assertTrue(errors.stream().anyMatch(error -> error.contains("amplitude must be finite and between")));
|
||||
assertTrue(errors.stream().anyMatch(error -> error.contains("verticalScale must be finite and between")));
|
||||
assertTrue(errors.stream().anyMatch(error -> error.contains("crackDepth must be a finite JSON number")));
|
||||
assertTrue(errors.stream().filter(error -> error.contains("seed must be an integer")).count() == 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsUnavailableAndUnreadableCurrentSnippets() throws Exception {
|
||||
File pack = temporaryFolder.newFolder();
|
||||
write(pack, "biomes/missing.json", "{\"terrain3D\":\"snippet/terrain-3d/missing\"}");
|
||||
write(pack, "biomes/style.json", "{\"terrain3D\":{\"densityStyle\":\"snippet/style/missing\"}}");
|
||||
write(pack, "snippet/terrain-3d/broken.json", "{\"amplitude\":");
|
||||
write(pack, "biomes/path.json", "{\"terrain3D\":\"snippet/terrain-3d/../../outside\"}");
|
||||
|
||||
List<String> errors = PackTerrain3DValidator.validate(pack);
|
||||
assertTrue(errors.stream().anyMatch(error -> error.contains("unavailable terrain-3d snippet")));
|
||||
assertTrue(errors.stream().anyMatch(error -> error.contains("unavailable style snippet")));
|
||||
assertTrue(errors.stream().anyMatch(error -> error.contains("contains unreadable JSON")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsMalformedNestedStylesAndCyclicStyleReferences() throws Exception {
|
||||
File pack = temporaryFolder.newFolder();
|
||||
write(pack, "biomes/shape.json", "{\"terrain3D\":{\"densityStyle\":true}}");
|
||||
write(pack, "biomes/style.json", "{\"terrain3D\":{\"densityStyle\":{\"style\":\"UNKNOWN\"}}}");
|
||||
write(pack, "biomes/nested.json", """
|
||||
{"terrain3D":{"densityStyle":{"fracture":{"style":"SIMPLEX","zoom":0,"wrong":1}}}}
|
||||
""");
|
||||
write(pack, "biomes/cycle.json", "{\"terrain3D\":{\"densityStyle\":\"snippet/style/cycle\"}}");
|
||||
write(pack, "snippet/style/cycle.json", "{\"fracture\":\"snippet/style/cycle\"}");
|
||||
|
||||
List<String> errors = PackTerrain3DValidator.validate(pack);
|
||||
assertTrue(errors.stream().anyMatch(error -> error.contains("densityStyle must be an object or snippet reference")));
|
||||
assertTrue(errors.stream().anyMatch(error -> error.contains("is not a known noise style")));
|
||||
assertTrue(errors.stream().anyMatch(error -> error.contains("fracture.zoom must be between")));
|
||||
assertTrue(errors.stream().anyMatch(error -> error.contains("wrong is not a generator style field")));
|
||||
assertTrue(errors.stream().anyMatch(error -> error.contains("maximum style nesting depth")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidTerrainProfileBlocksFullPackValidation() throws Exception {
|
||||
File pack = temporaryFolder.newFolder();
|
||||
write(pack, "dimensions/main.json", "{\"regions\":[\"region\"]}");
|
||||
write(pack, "regions/region.json", "{\"landBiomes\":[\"biome\"]}");
|
||||
write(pack, "biomes/biome.json", "{\"terrain3D\":{\"amplitude\":129}}");
|
||||
|
||||
PackValidationResult result = PackValidator.validate(pack);
|
||||
assertFalse(result.isLoadable());
|
||||
assertTrue(result.getBlockingErrors().stream()
|
||||
.anyMatch(error -> error.contains("terrain3D.amplitude must be finite and between")));
|
||||
}
|
||||
|
||||
private static void write(File root, String relative, String content) throws Exception {
|
||||
Path path = root.toPath().resolve(relative);
|
||||
Files.createDirectories(path.getParent());
|
||||
Files.writeString(path, content, StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioMode;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.framework.BiomeEnvironment;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.history.SavedBiomeUnavailableException;
|
||||
@@ -107,6 +108,7 @@ public class BoardSVCSavedBiomeTest {
|
||||
when(natural.getName()).thenReturn("Natural cave");
|
||||
when(flooded.getName()).thenReturn("Flooded cave");
|
||||
when(fixture.engine.getHeight(8, 8)).thenReturn(200);
|
||||
when(fixture.engine.getComplex()).thenReturn(mock(IrisComplex.class));
|
||||
when(fixture.engine.getCaveBiome(8, 160, 8)).thenReturn(natural);
|
||||
when(fixture.engine.getCaveOrMantleBiome(8, 160, 8)).thenReturn(flooded);
|
||||
when(fixture.engine.getDimension()).thenReturn(fixture.environment.dimension());
|
||||
|
||||
@@ -331,7 +331,8 @@ public class DimensionStackLayoutTest {
|
||||
null,
|
||||
null,
|
||||
terrainHeight,
|
||||
fluidHeight
|
||||
fluidHeight,
|
||||
null
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
package art.arcane.iris.engine;
|
||||
|
||||
import art.arcane.iris.engine.actuator.IrisDimensionStackActuator;
|
||||
import art.arcane.iris.engine.actuator.IrisTerrainNormalActuator;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.SeedManager;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.IrisBiomePaletteLayer;
|
||||
import art.arcane.iris.engine.object.IrisSlopeClip;
|
||||
import art.arcane.iris.engine.image.IrisImageMapRuntime;
|
||||
import art.arcane.iris.engine.terrain.Terrain3DColumn;
|
||||
import art.arcane.iris.engine.terrain.Terrain3DColumnFixtures;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
import art.arcane.iris.util.project.context.ChunkContext;
|
||||
import art.arcane.iris.util.project.context.ChunkedDataCache;
|
||||
import art.arcane.iris.util.project.hunk.Hunk;
|
||||
import art.arcane.iris.util.project.stream.ProceduralStream;
|
||||
import art.arcane.iris.util.project.noise.CNG;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyDouble;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class DimensionTerrain3DCompositionTest {
|
||||
private final Map<String, PlatformBlockState> states = new HashMap<>();
|
||||
private IrisPlatform previousPlatform;
|
||||
|
||||
@Before
|
||||
public void bindPlatform() {
|
||||
previousPlatform = IrisPlatforms.isBound() ? IrisPlatforms.get() : null;
|
||||
IrisPlatforms.unbind();
|
||||
PlatformRegistries registries = mock(PlatformRegistries.class);
|
||||
when(registries.block(anyString())).thenAnswer(invocation -> state(invocation.getArgument(0)));
|
||||
IrisPlatform platform = mock(IrisPlatform.class);
|
||||
when(platform.registries()).thenReturn(registries);
|
||||
IrisPlatforms.bind(platform);
|
||||
}
|
||||
|
||||
@After
|
||||
public void restorePlatform() {
|
||||
IrisPlatforms.unbind();
|
||||
if (previousPlatform != null) {
|
||||
IrisPlatforms.bind(previousPlatform);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void actualStackPassClearsGapsAndPaintsEveryExposedFloor() {
|
||||
Terrain3DColumn terrain = Terrain3DColumnFixtures.spans(6, 0, 3, 7, 9);
|
||||
DimensionStackLayout layout = DimensionStackLayout.create(32, 0,
|
||||
List.of(input(10, 0, null), input(9, 0, terrain)), new int[]{0});
|
||||
Hunk<PlatformBlockState> blocks = render(layout, 32);
|
||||
assertSame(state("surface"), blocks.getRaw(0, 14, 0));
|
||||
assertSame(state("rock"), blocks.getRaw(0, 13, 0));
|
||||
assertTrue(blocks.getRaw(0, 15, 0).isAir());
|
||||
assertTrue(blocks.getRaw(0, 17, 0).isAir());
|
||||
assertSame(state("surface"), blocks.getRaw(0, 20, 0));
|
||||
for (int y = 11; y <= 20; y++) {
|
||||
assertEquals("y=" + y, layout.isSolid(y), !blocks.getRaw(0, y, 0).isAir());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void clippedWorldCeilingFindsTheActualFloorBelowAnUndercut() {
|
||||
Terrain3DColumn terrain = Terrain3DColumnFixtures.spans(6, 0, 3, 7, 9);
|
||||
DimensionStackLayout layout = DimensionStackLayout.create(17, 0,
|
||||
List.of(input(10, 0, null), input(9, 0, terrain)), new int[]{0});
|
||||
assertEquals(14, layout.clippedStackTerrainTopY());
|
||||
assertEquals(14, layout.clippedStackTopY());
|
||||
assertEquals(14, layout.layersBottomToTop().get(1).clippedSurfaceY());
|
||||
assertSame(layout.layersBottomToTop().get(1), layout.topTerrainLayer());
|
||||
assertTrue(render(layout, 17).getRaw(0, 16, 0).isAir());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void laterLayerAirOverwritesLowerTerrainInAnOverlappingSeam() {
|
||||
Terrain3DColumn terrain = Terrain3DColumnFixtures.spans(6, 0, 2, 8, 10);
|
||||
DimensionStackLayout layout = DimensionStackLayout.create(32, 0,
|
||||
List.of(input(20, 0, null), input(10, 0, terrain)), new int[]{-11});
|
||||
Hunk<PlatformBlockState> blocks = render(layout, 32);
|
||||
assertFalse(layout.isSolid(15));
|
||||
assertTrue(blocks.getRaw(0, 15, 0).isAir());
|
||||
assertTrue(layout.isHostFeatureProtectedY(15));
|
||||
assertTrue(layout.isSolid(18));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void referencedStackSelectsEachLedgePaletteFromItsSourceSlope() {
|
||||
Terrain3DColumn terrain = Terrain3DColumnFixtures.spans(15, 0, 5, 10, 21);
|
||||
IrisBiome biome = paletteBiome(false, new IrisSlopeClip(0D, 1D), "grass");
|
||||
DimensionStackLayout.LayerInput upper = materialInput(terrain, biome, 5D);
|
||||
DimensionStackLayout layout = DimensionStackLayout.create(64, 0,
|
||||
List.of(input(10, 0, null), upper), new int[]{0});
|
||||
Hunk<PlatformBlockState> blocks = render(layout, 64);
|
||||
assertSame(state("grass"), blocks.getRaw(0, 16, 0));
|
||||
assertSame(state("rock"), blocks.getRaw(0, 32, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void referencedStackLockedPalettePhaseUsesEachSourceFloorHeight() {
|
||||
Terrain3DColumn terrain = Terrain3DColumnFixtures.spans(15, 0, 5, 10, 21);
|
||||
IrisBiome biome = paletteBiome(true, new IrisSlopeClip(), "a", "b", "c");
|
||||
DimensionStackLayout layout = DimensionStackLayout.create(64, 0,
|
||||
List.of(input(10, 0, null), materialInput(terrain, biome, 0D)), new int[]{0});
|
||||
Hunk<PlatformBlockState> blocks = render(layout, 64);
|
||||
assertSame(state("a"), blocks.getRaw(0, 16, 0));
|
||||
assertSame(state("c"), blocks.getRaw(0, 15, 0));
|
||||
assertSame(state("c"), blocks.getRaw(0, 32, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mirroredUpperPalettesUseSourceSlopesAndLockedLayerPhases() {
|
||||
Terrain3DColumn terrain = Terrain3DColumnFixtures.spans(15, 0, 5, 10, 21);
|
||||
UpperDimensionContext upper = materialUpper(terrain,
|
||||
paletteBiome(false, new IrisSlopeClip(0D, 1D), "grass"), 5D);
|
||||
Hunk<PlatformBlockState> slopeBlocks = renderUpper(upper);
|
||||
assertSame(state("grass"), slopeBlocks.getRaw(0, 58, 0));
|
||||
assertSame(state("rock"), slopeBlocks.getRaw(0, 42, 0));
|
||||
|
||||
upper = materialUpper(terrain, paletteBiome(true, new IrisSlopeClip(), "a", "b", "c"), 0D);
|
||||
Hunk<PlatformBlockState> lockedBlocks = renderUpper(upper);
|
||||
assertSame(state("a"), lockedBlocks.getRaw(0, 58, 0));
|
||||
assertSame(state("c"), lockedBlocks.getRaw(0, 59, 0));
|
||||
assertSame(state("c"), lockedBlocks.getRaw(0, 42, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void actualUpperPassMirrorsGapsAndPaintsEveryInvertedFace() {
|
||||
PlatformBlockState upperRock = state("upper-rock");
|
||||
PlatformBlockState upperSurface = state("upper-surface");
|
||||
Terrain3DColumn terrain = Terrain3DColumnFixtures.spans(15, 0, 5, 10, 20);
|
||||
UpperDimensionContext.Column upperColumn = new UpperDimensionContext.Column(64, 43, terrain);
|
||||
UpperDimensionContext upper = mock(UpperDimensionContext.class);
|
||||
when(upper.sampleColumn(0, 0)).thenReturn(upperColumn);
|
||||
when(upper.getRockBlock(0, 0)).thenReturn(upperRock);
|
||||
when(upper.getSurfaceBlock(0, 0)).thenReturn(upperSurface);
|
||||
Hunk<PlatformBlockState> blocks = renderUpper(upper);
|
||||
|
||||
assertSame(state("upper-surface"), blocks.getRaw(0, 43, 0));
|
||||
assertSame(state("upper-rock"), blocks.getRaw(0, 44, 0));
|
||||
assertTrue(blocks.getRaw(0, 54, 0).isAir());
|
||||
assertTrue(blocks.getRaw(0, 57, 0).isAir());
|
||||
assertSame(state("upper-surface"), blocks.getRaw(0, 58, 0));
|
||||
for (int y = 43; y < 64; y++) {
|
||||
assertEquals("y=" + y, upperColumn.isSolid(y), !blocks.getRaw(0, y, 0).isAir());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void clippedOverlappingStacksMatchIndependentVoxelComposition() {
|
||||
Random random = new Random(718223L);
|
||||
Terrain3DColumn first = Terrain3DColumnFixtures.spans(12, 0, 4, 10, 14, 21, 24);
|
||||
Terrain3DColumn second = Terrain3DColumnFixtures.spans(9, 0, 2, 7, 10, 14, 18);
|
||||
for (int iteration = 0; iteration < 150; iteration++) {
|
||||
int height = 8 + random.nextInt(56);
|
||||
DimensionStackLayout layout = DimensionStackLayout.create(height, random.nextInt(10),
|
||||
List.of(input(24, 0, first), input(18, 22, second), input(24, 0, first)),
|
||||
new int[]{random.nextInt(71) - 50, random.nextInt(71) - 50});
|
||||
boolean[] solid = new boolean[height];
|
||||
boolean[] content = new boolean[height];
|
||||
List<DimensionStackLayout.Layer> layers = layout.layersBottomToTop();
|
||||
for (int index = 0; index < layers.size(); index++) {
|
||||
DimensionStackLayout.Layer layer = layers.get(index);
|
||||
for (int y = 0; y < height; y++) {
|
||||
if (index > 0 && y > layers.get(index - 1).contentTopY() && y < layer.localBaseY()) {
|
||||
solid[y] = false;
|
||||
content[y] = false;
|
||||
}
|
||||
if (y >= layer.renderMinY() && y <= layer.renderMaxY()) {
|
||||
int sourceY = y - layer.localBaseY();
|
||||
solid[y] = layer.terrainColumn().isSolid(sourceY);
|
||||
content[y] = solid[y]
|
||||
|| sourceY > layer.normalTerrainHeight() && sourceY <= layer.fluidHeight();
|
||||
}
|
||||
}
|
||||
}
|
||||
int highestSolid = 0;
|
||||
int highestContent = 0;
|
||||
for (int y = 0; y < height; y++) {
|
||||
assertEquals("iteration=" + iteration + ",y=" + y, solid[y], layout.isSolid(y));
|
||||
if (solid[y]) {
|
||||
highestSolid = y;
|
||||
}
|
||||
if (content[y]) {
|
||||
highestContent = y;
|
||||
}
|
||||
}
|
||||
assertEquals("iteration=" + iteration, highestSolid, layout.clippedStackTerrainTopY());
|
||||
assertEquals("iteration=" + iteration, highestContent, layout.clippedStackTopY());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void composedSupportRespectsUpperAndStackOwnershipAboveRootTerrain() {
|
||||
Engine engine = mock(Engine.class, CALLS_REAL_METHODS);
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
doReturn(complex).when(engine).getComplex();
|
||||
doReturn(64).when(engine).getHeight();
|
||||
when(complex.terrainColumn(0, 0)).thenReturn(Terrain3DColumnFixtures.spans(12, 0, 4, 15, 20));
|
||||
UpperDimensionContext upper = mock(UpperDimensionContext.class);
|
||||
UpperDimensionContext.Column upperColumn = new UpperDimensionContext.Column(64, 40,
|
||||
Terrain3DColumnFixtures.spans(12, 0, 5, 10, 23));
|
||||
doReturn(upper).when(engine).getUpperContext();
|
||||
when(upper.sampleColumn(0, 0)).thenReturn(upperColumn);
|
||||
when(upper.getEffectiveSurfaceY(0, 0)).thenReturn(40);
|
||||
assertTrue(engine.isTerrainSurfaceSolid(0, 50, 0));
|
||||
assertFalse(engine.isTerrainSurfaceSolid(0, 56, 0));
|
||||
assertFalse(engine.isTerrainSurfaceSolid(0, 30, 0));
|
||||
|
||||
DimensionStackLayout layout = DimensionStackLayout.create(64, 0,
|
||||
List.of(input(20, 0, null), input(30, 0,
|
||||
Terrain3DColumnFixtures.spans(10, 0, 3, 28, 30))), new int[]{0});
|
||||
DimensionStackContext stack = mock(DimensionStackContext.class);
|
||||
doReturn(stack).when(engine).getDimensionStackContext();
|
||||
when(stack.getLayout(0, 0)).thenReturn(layout);
|
||||
assertFalse(engine.isTerrainSurfaceSolid(0, 45, 0));
|
||||
assertTrue(engine.isTerrainSurfaceSolid(0, 50, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stackedHostKeepsOutdoorLedgesInTheSurfaceBiome() {
|
||||
Engine engine = mock(Engine.class, CALLS_REAL_METHODS);
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
doReturn(complex).when(engine).getComplex();
|
||||
DimensionStackContext stack = mock(DimensionStackContext.class);
|
||||
doReturn(stack).when(engine).getDimensionStackContext();
|
||||
DimensionStackLayout.LayerInput bottom = input(24, 0,
|
||||
Terrain3DColumnFixtures.spans(12, 0, 4, 20, 24));
|
||||
DimensionStackLayout layout = DimensionStackLayout.create(64, 10,
|
||||
List.of(bottom, input(9, 0, null)), new int[]{0});
|
||||
when(stack.getLayout(0, 0)).thenReturn(layout);
|
||||
when(complex.isTerrain3DSurface(0, 4, 0)).thenReturn(true);
|
||||
IrisBiome cave = new IrisBiome();
|
||||
doReturn(cave).when(engine).getCaveBiome(0, 2, 0);
|
||||
doReturn(cave).when(engine).getCaveOrMantleBiome(0, 2, 0);
|
||||
assertSame(bottom.biome(), engine.getBiome(0, 4, 0));
|
||||
assertSame(bottom.biome(), engine.getBiomeOrMantle(0, 4, 0));
|
||||
assertSame(cave, engine.getBiome(0, 2, 0));
|
||||
assertSame(cave, engine.getBiomeOrMantle(0, 2, 0));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Hunk<PlatformBlockState> renderUpper(UpperDimensionContext upper) {
|
||||
PlatformBlockState rootRock = state("root");
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisDimension dimension = new IrisDimension().setBedrock(false).setHideOresForHiddenOre(true);
|
||||
when(engine.getDimension()).thenReturn(dimension);
|
||||
when(engine.getSeedManager()).thenReturn(mock(SeedManager.class));
|
||||
when(engine.getUpperContext()).thenReturn(upper);
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
when(complex.getRiverWaterSurfaceStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 8D));
|
||||
when(complex.getImageMapRuntime()).thenReturn(mock(IrisImageMapRuntime.class));
|
||||
IrisBiome biome = mock(IrisBiome.class);
|
||||
when(biome.generateLayers(any(), anyDouble(), anyDouble(), any(), anyInt(), anyInt(), any(), any()))
|
||||
.thenReturn(new KList<>());
|
||||
ChunkedDataCache<IrisBiome> biomes = mock(ChunkedDataCache.class);
|
||||
when(biomes.get(0, 0)).thenReturn(biome);
|
||||
ChunkedDataCache<IrisRegion> regions = mock(ChunkedDataCache.class);
|
||||
ChunkedDataCache<PlatformBlockState> rocks = mock(ChunkedDataCache.class);
|
||||
when(rocks.get(0, 0)).thenReturn(rootRock);
|
||||
ChunkContext context = mock(ChunkContext.class);
|
||||
when(context.getBiome()).thenReturn(biomes);
|
||||
when(context.getRegion()).thenReturn(regions);
|
||||
when(context.getRock()).thenReturn(rocks);
|
||||
when(context.getRoundedHeight(0, 0)).thenReturn(8);
|
||||
Hunk<PlatformBlockState> blocks = Hunk.newArrayHunk(1, 64, 1);
|
||||
new IrisTerrainNormalActuator(engine).terrainSliver(0, 0, 0, blocks, context);
|
||||
return blocks;
|
||||
}
|
||||
|
||||
private Hunk<PlatformBlockState> render(DimensionStackLayout layout, int height) {
|
||||
Engine engine = mock(Engine.class);
|
||||
when(engine.getSeedManager()).thenReturn(mock(SeedManager.class));
|
||||
when(engine.getDimensionStackContext()).thenReturn(mock(DimensionStackContext.class));
|
||||
ChunkContext context = mock(ChunkContext.class);
|
||||
when(context.isSpeculativeTerrain()).thenReturn(true);
|
||||
when(context.getDimensionStackLayout(0, 0)).thenReturn(layout);
|
||||
Hunk<PlatformBlockState> output = Hunk.newArrayHunk(1, height, 1);
|
||||
for (int y = 0; y < height; y++) {
|
||||
output.setRaw(0, y, 0, state("root"));
|
||||
}
|
||||
new IrisDimensionStackActuator(engine).onActuate(0, 0, output, false, context);
|
||||
return output;
|
||||
}
|
||||
|
||||
private DimensionStackLayout.LayerInput input(int height, int fluidHeight, Terrain3DColumn terrain) {
|
||||
DimensionTerrainContext context = mock(DimensionTerrainContext.class);
|
||||
when(context.getDimension()).thenReturn(new IrisDimension().setBedrock(false));
|
||||
return new DimensionStackLayout.LayerInput(context, null, null, state("rock"), state("water"),
|
||||
state("surface"), height, fluidHeight, terrain);
|
||||
}
|
||||
|
||||
private DimensionStackLayout.LayerInput materialInput(Terrain3DColumn terrain, IrisBiome biome, double capSlope) {
|
||||
DimensionStackLayout.LayerInput input = input(terrain.topY(), 0, terrain);
|
||||
when(input.terrainContext().getSlopeStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> capSlope));
|
||||
when(input.terrainContext().getSurfaceSlopeStream(anyInt())).thenAnswer(invocation ->
|
||||
ProceduralStream.ofDouble((x, z) -> (int) invocation.getArgument(0) == terrain.topY() ? capSlope : 0D));
|
||||
return new DimensionStackLayout.LayerInput(input.terrainContext(), biome, null,
|
||||
input.rockBlock(), input.fluidBlock(), null, terrain.topY(), 0, terrain);
|
||||
}
|
||||
|
||||
private UpperDimensionContext materialUpper(Terrain3DColumn terrain, IrisBiome biome, double capSlope) {
|
||||
PlatformBlockState rock = state("rock");
|
||||
UpperDimensionContext upper = mock(UpperDimensionContext.class);
|
||||
when(upper.sampleColumn(0, 0)).thenReturn(new UpperDimensionContext.Column(64, 63 - terrain.topY(), terrain));
|
||||
when(upper.getRockBlock(0, 0)).thenReturn(rock);
|
||||
when(upper.getUpperBiome(0, 0)).thenReturn(biome);
|
||||
when(upper.getDimension()).thenReturn(new IrisDimension().setBedrock(false));
|
||||
when(upper.getSurfaceSlopeStream(anyInt())).thenAnswer(invocation ->
|
||||
ProceduralStream.ofDouble((x, z) -> (int) invocation.getArgument(0) == terrain.topY() ? capSlope : 0D));
|
||||
return upper;
|
||||
}
|
||||
|
||||
private IrisBiome paletteBiome(boolean locked, IrisSlopeClip slope, String... keys) {
|
||||
KList<IrisBiomePaletteLayer> layers = new KList<>();
|
||||
for (String key : keys) {
|
||||
PlatformBlockState block = state(key);
|
||||
IrisBiomePaletteLayer layer = mock(IrisBiomePaletteLayer.class);
|
||||
CNG height = mock(CNG.class);
|
||||
when(layer.getZoom()).thenReturn(1D);
|
||||
when(layer.getMinHeight()).thenReturn(1);
|
||||
when(layer.getMaxHeight()).thenReturn(1);
|
||||
when(layer.getSlopeCondition()).thenReturn(slope);
|
||||
when(layer.getHeightGenerator(any(), any())).thenReturn(height);
|
||||
when(height.fit(anyInt(), anyInt(), anyDouble(), anyDouble())).thenReturn(1);
|
||||
when(layer.get(any(), anyInt(), anyDouble(), anyDouble(), anyDouble(), any())).thenReturn(block);
|
||||
layers.add(layer);
|
||||
}
|
||||
return new IrisBiome().setLockLayers(locked).setLayers(layers);
|
||||
}
|
||||
|
||||
private PlatformBlockState state(String key) {
|
||||
return states.computeIfAbsent(key, value -> {
|
||||
PlatformBlockState block = mock(PlatformBlockState.class);
|
||||
when(block.key()).thenReturn(value);
|
||||
when(block.isAir()).thenReturn(value.equalsIgnoreCase("air") || value.equals("minecraft:air"));
|
||||
return block;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -7,14 +7,64 @@ import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.project.stream.ProceduralStream;
|
||||
import art.arcane.iris.engine.terrain.Terrain3DColumn;
|
||||
import art.arcane.iris.engine.terrain.Terrain3DColumnFixtures;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.Collections;
|
||||
import java.util.IdentityHashMap;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class DimensionTerrainContextFallbackTest {
|
||||
@Test
|
||||
public void regionMapOnlyTargetsJoinPreparedRegionsWithoutDuplicateIdentities() {
|
||||
IrisRegion declared = new IrisRegion();
|
||||
IrisRegion mapped = new IrisRegion();
|
||||
declared.setLoadKey("declared");
|
||||
mapped.setLoadKey("mapped");
|
||||
IrisRegion excluded = mock(IrisRegion.class);
|
||||
when(excluded.isCompatExcluded()).thenReturn(true);
|
||||
IrisImageMapRuntime imageMaps = mock(IrisImageMapRuntime.class);
|
||||
Set<IrisRegion> mappedRegions = Collections.newSetFromMap(new IdentityHashMap<>());
|
||||
mappedRegions.add(declared);
|
||||
mappedRegions.add(mapped);
|
||||
mappedRegions.add(excluded);
|
||||
when(imageMaps.getMappedRegions()).thenReturn(mappedRegions);
|
||||
KList<IrisRegion> regions = DimensionTerrainContext.terrainRegions(List.of(declared), imageMaps);
|
||||
|
||||
assertEquals(2, regions.size());
|
||||
assertSame(declared, regions.get(0));
|
||||
assertTrue(regions.stream().anyMatch(region -> region == mapped));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sourceLedgeSlopeUsesNeighboringSourceSpansRatherThanTheHighestCap() {
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisDimension dimension = mock(IrisDimension.class);
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
when(engine.getDimension()).thenReturn(dimension);
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
when(engine.getHeight()).thenReturn(128);
|
||||
when(dimension.getLoadKey()).thenReturn("root");
|
||||
when(complex.hasTerrain3D()).thenReturn(true);
|
||||
when(complex.getSlopeStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 40D));
|
||||
when(complex.terrainColumn(-17, 8)).thenReturn(Terrain3DColumnFixtures.spans(20, 0, 5, 20, 40));
|
||||
when(complex.terrainColumn(-14, 8)).thenReturn(Terrain3DColumnFixtures.spans(20, 0, 8, 20, 60));
|
||||
when(complex.terrainColumn(-17, 11)).thenReturn(Terrain3DColumnFixtures.spans(20, 0, 9, 20, 80));
|
||||
DimensionTerrainContext context = DimensionTerrainContext.forStack(engine, dimension);
|
||||
|
||||
assertEquals(5D, context.getSurfaceSlopeStream(5).getDouble(-17, 8), 0D);
|
||||
assertEquals(40D, context.getSurfaceSlopeStream(40).getDouble(-17, 8), 0D);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void selfSamplerUsesNaturalValuesOnlyForNonblockingColumns() {
|
||||
@@ -70,4 +120,26 @@ public class DimensionTerrainContextFallbackTest {
|
||||
assertSame(resolvedBiomeValue, context.getBiome(12D, -8D));
|
||||
assertSame(resolvedFluid, context.getFluidBlock(12D, -8D));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void selfStackReusesResolvedColumnsAndNaturalFallbackWithoutShapingAgain() {
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisDimension dimension = mock(IrisDimension.class);
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
when(engine.getDimension()).thenReturn(dimension);
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
when(engine.getHeight()).thenReturn(128);
|
||||
when(dimension.getLoadKey()).thenReturn("root");
|
||||
Terrain3DColumn natural = Terrain3DColumnFixtures.spans(30, 0, 15, 25, 40);
|
||||
Terrain3DColumn resolved = Terrain3DColumnFixtures.spans(30, 0, 12, 28, 35);
|
||||
when(complex.naturalTerrainColumn(-17, 8)).thenReturn(natural);
|
||||
when(complex.terrainColumn(-17, 8)).thenReturn(resolved);
|
||||
DimensionTerrainContext context = DimensionTerrainContext.forStack(engine, dimension);
|
||||
|
||||
assertSame(resolved, context.terrainColumn(-17, 8));
|
||||
when(engine.answersFromNaturalTerrain(-17, 8)).thenReturn(true);
|
||||
assertSame(natural, context.terrainColumn(-17, 8));
|
||||
when(engine.answersFromNaturalTerrain(-17, 8)).thenReturn(false);
|
||||
assertSame(resolved, context.terrainColumn(-17, 8));
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,15 @@ import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.history.TransitionGenerationPlan;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.util.project.stream.ProceduralStream;
|
||||
import art.arcane.iris.engine.terrain.Terrain3DColumn;
|
||||
import art.arcane.iris.engine.terrain.Terrain3DColumnFixtures;
|
||||
import org.junit.Test;
|
||||
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
@@ -16,6 +20,43 @@ import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class UpperDimensionTransitionTest {
|
||||
@Test
|
||||
public void mirroredDensityPreservesEverySolidSpanAndFace() {
|
||||
Terrain3DColumn terrain = Terrain3DColumnFixtures.spans(80, 0, 30, 40, 60, 100, 110);
|
||||
UpperDimensionContext.Column column = new UpperDimensionContext.Column(128, 17, terrain);
|
||||
assertTrue(column.isSolid(17));
|
||||
assertTrue(column.isSolid(27));
|
||||
assertFalse(column.isSolid(28));
|
||||
assertFalse(column.isSolid(66));
|
||||
assertTrue(column.isSolid(67));
|
||||
assertEquals(67, column.faceY(70));
|
||||
assertEquals(97, column.faceY(110));
|
||||
assertFalse(column.isSolid(128));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void upperGapClipsToTheNextRealMirroredFace() {
|
||||
IrisDimension dimension = mock(IrisDimension.class);
|
||||
when(dimension.getLoadKey()).thenReturn("main");
|
||||
when(dimension.getUpperDimensionGap()).thenReturn(28);
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
Terrain3DColumn terrain = Terrain3DColumnFixtures.spans(80, 0, 30, 40, 60, 100, 110);
|
||||
when(complex.naturalTerrainColumn(-17, 8)).thenReturn(terrain);
|
||||
when(complex.getUnblendedNaturalHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 110D));
|
||||
when(complex.getHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 64D));
|
||||
Engine engine = mock(Engine.class);
|
||||
when(engine.getDimension()).thenReturn(dimension);
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
when(engine.getHeight()).thenReturn(128);
|
||||
UpperDimensionContext context = UpperDimensionContext.create(engine, dimension);
|
||||
|
||||
assertEquals(97, context.getEffectiveSurfaceY(-17, 8));
|
||||
UpperDimensionContext.Column column = context.sampleColumn(-17, 8);
|
||||
assertFalse(column.isSolid(92));
|
||||
assertTrue(column.isSolid(97));
|
||||
assertEquals(97, column.faceY(110));
|
||||
verify(complex, never()).terrainColumn(anyInt(), anyInt());
|
||||
}
|
||||
@Test
|
||||
public void currentCeilingInputRespectsCurrentLowerTerrainGap() {
|
||||
IrisDimension dimension = mock(IrisDimension.class);
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package art.arcane.iris.engine.decorator;
|
||||
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.object.IrisDecorator;
|
||||
import art.arcane.iris.engine.object.IrisSlopeClip;
|
||||
import art.arcane.iris.util.project.stream.ProceduralStream;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class IrisSurfaceDecoratorSlopeTest {
|
||||
@Test
|
||||
public void lowerLedgeUsesItsOwnSlopeForVegetation() {
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
IrisSurfaceDecorator surface = mock(IrisSurfaceDecorator.class, CALLS_REAL_METHODS);
|
||||
doReturn(complex).when(surface).getComplex();
|
||||
IrisDecorator vegetation = new IrisDecorator().setSlopeCondition(new IrisSlopeClip(0D, 2.6D));
|
||||
when(complex.hasTerrain3D()).thenReturn(true);
|
||||
when(complex.terrainSurfaceSlope(12, 32, -8)).thenReturn(0D);
|
||||
when(complex.terrainSurfaceSlope(12, 96, -8)).thenReturn(5D);
|
||||
|
||||
assertTrue(surface.isSlopeValid(vegetation, 12, 32, -8));
|
||||
assertFalse(surface.isSlopeValid(vegetation, 12, 96, -8));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void disabledTerrainKeepsItsExistingSlopeGate() {
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
IrisSurfaceDecorator surface = mock(IrisSurfaceDecorator.class, CALLS_REAL_METHODS);
|
||||
doReturn(complex).when(surface).getComplex();
|
||||
IrisDecorator vegetation = new IrisDecorator().setSlopeCondition(new IrisSlopeClip(0D, 2.6D));
|
||||
when(complex.getSlopeStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 5D));
|
||||
|
||||
assertFalse(surface.isSlopeValid(vegetation, 12, 32, -8));
|
||||
vegetation.setForcePlace(true);
|
||||
assertTrue(surface.isSlopeValid(vegetation, 12, 32, -8));
|
||||
}
|
||||
}
|
||||
+6
@@ -1255,6 +1255,10 @@ public class HydrologyFootprintCompilerTest {
|
||||
).compile(courses);
|
||||
|
||||
for (HydrologyColumnSample sample : full.columns().values()) {
|
||||
assertEquals(sample.primarySurfaceLayer().isPresent(),
|
||||
validation.plannedSurface().ownsTerrain(sample.x(), sample.z()));
|
||||
assertEquals(sample.primarySurfaceLayer().isPresent(),
|
||||
validation.withMaterializedSurface(full).plannedSurface().ownsTerrain(sample.x(), sample.z()));
|
||||
assertEquals(
|
||||
"planned surface at " + sample.x() + "," + sample.z(),
|
||||
sample.terrainHeight(),
|
||||
@@ -1294,6 +1298,8 @@ public class HydrologyFootprintCompilerTest {
|
||||
}
|
||||
assertEquals(91, validation.plannedSurface().resolve(80, 80, 91));
|
||||
assertEquals(92, validation.plannedSurface().resolve(80, 80, 92));
|
||||
assertFalse(validation.plannedSurface().ownsTerrain(80, 80));
|
||||
assertFalse(validation.withMaterializedSurface(full).plannedSurface().ownsTerrain(80, 80));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+12
-4
@@ -146,10 +146,18 @@ public class HydrologyPlannerCrossTileCaveAdmissionIntegrationTest {
|
||||
}
|
||||
|
||||
private void assertMaterializedPreconditions(HydrologyTile tile, HydrologyTerrainSampler terrain) {
|
||||
HydrologyCaveVoxelViewFactory.PlannedSurface surface = (int x, int z, int naturalHeight) ->
|
||||
tile.footprint().sample(x, z)
|
||||
.map(HydrologyColumnSample::terrainHeight)
|
||||
.orElse(naturalHeight);
|
||||
HydrologyCaveVoxelViewFactory.PlannedSurface surface = new HydrologyCaveVoxelViewFactory.PlannedSurface() {
|
||||
@Override
|
||||
public int resolve(int x, int z, int naturalHeight) {
|
||||
return tile.footprint().sample(x, z).map(HydrologyColumnSample::terrainHeight).orElse(naturalHeight);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean ownsTerrain(int x, int z) {
|
||||
HydrologyColumnSample sample = tile.footprint().sample(x, z).orElse(null);
|
||||
return sample != null && sample.primarySurfaceLayer().isPresent();
|
||||
}
|
||||
};
|
||||
CaveVoxelView view = new PlannedSurfaceCaveView(terrain, surface);
|
||||
for (HydrologyCavePlan plan : tile.cavePlans()) {
|
||||
for (Map.Entry<CavePosition, CaveVoxelPrecondition> entry
|
||||
|
||||
+42
-7
@@ -788,9 +788,9 @@ public class HydrologyCaveContainmentPlannerTest {
|
||||
);
|
||||
HydrologyCaveContainmentPlanner.ValidationCache cache =
|
||||
new HydrologyCaveContainmentPlanner.ValidationCache();
|
||||
HydrologyObservedPlannedSurface initialSurface = observedSurface(32);
|
||||
HydrologyObservedPlannedSurface matchingSurface = observedSurface(32);
|
||||
HydrologyObservedPlannedSurface changedSurface = observedSurface(9);
|
||||
HydrologyObservedPlannedSurface initialSurface = observedSurface(32, true);
|
||||
HydrologyObservedPlannedSurface matchingSurface = observedSurface(32, true);
|
||||
HydrologyObservedPlannedSurface changedSurface = observedSurface(9, true);
|
||||
|
||||
HydrologyCavePlan initial = planner.validateAll(
|
||||
plannedSurfaceView(initialSurface),
|
||||
@@ -822,6 +822,31 @@ public class HydrologyCaveContainmentPlannerTest {
|
||||
assertEquals(2L, cache.misses());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void plannedSurfaceCacheInvalidatesEqualHeightWhenTerrainOwnershipChanges() {
|
||||
CavePosition center = position(0, 10, 0);
|
||||
HydrologyCaveCandidate candidate = candidate(
|
||||
204L, 10, "water", Map.of(center, HydrologyCaveAction.WET_SOURCE), settings());
|
||||
HydrologyCaveContainmentPlanner.ValidationCache cache =
|
||||
new HydrologyCaveContainmentPlanner.ValidationCache();
|
||||
HydrologyObservedPlannedSurface initialSurface = observedSurface(32, true);
|
||||
HydrologyObservedPlannedSurface matchingSurface = observedSurface(32, true);
|
||||
HydrologyObservedPlannedSurface changedSurface = observedSurface(32, false);
|
||||
|
||||
HydrologyCavePlan initial = planner.validateAll(plannedSurfaceView(initialSurface),
|
||||
List.of(candidate), cache, initialSurface).plans().getFirst();
|
||||
HydrologyCavePlan cached = planner.validateAll(plannedSurfaceView(matchingSurface),
|
||||
List.of(candidate), cache, matchingSurface).plans().getFirst();
|
||||
HydrologyCavePlan changed = planner.validateAll(plannedSurfaceView(changedSurface),
|
||||
List.of(candidate), cache, changedSurface).plans().getFirst();
|
||||
|
||||
assertTrue(initial.accepted());
|
||||
assertEquals(initial, cached);
|
||||
assertEquals(HydrologyCaveRejection.OPEN_SURFACE, changed.rejection());
|
||||
assertEquals(1L, cache.hits());
|
||||
assertEquals(2L, cache.misses());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validationCacheEvictsItsPreviousWorkingSetWithoutResettingStatistics() {
|
||||
CavePosition center = position(0, 10, 0);
|
||||
@@ -1228,9 +1253,18 @@ public class HydrologyCaveContainmentPlannerTest {
|
||||
return new CavePosition(x, y, z);
|
||||
}
|
||||
|
||||
private HydrologyObservedPlannedSurface observedSurface(int resolvedHeight) {
|
||||
HydrologyCaveVoxelViewFactory.PlannedSurface surface =
|
||||
(int x, int z, int naturalHeight) -> resolvedHeight;
|
||||
private HydrologyObservedPlannedSurface observedSurface(int resolvedHeight, boolean terrainOwned) {
|
||||
HydrologyCaveVoxelViewFactory.PlannedSurface surface = new HydrologyCaveVoxelViewFactory.PlannedSurface() {
|
||||
@Override
|
||||
public int resolve(int x, int z, int naturalHeight) {
|
||||
return resolvedHeight;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean ownsTerrain(int x, int z) {
|
||||
return terrainOwned;
|
||||
}
|
||||
};
|
||||
return new HydrologyObservedPlannedSurface(surface);
|
||||
}
|
||||
|
||||
@@ -1253,7 +1287,8 @@ public class HydrologyCaveContainmentPlannerTest {
|
||||
|
||||
@Override
|
||||
public boolean isAboveTerrainSurface(CavePosition position) {
|
||||
return position.y() > surface.resolve(position.x(), position.z(), 32);
|
||||
return !surface.ownsTerrain(position.x(), position.z())
|
||||
|| position.y() > surface.resolve(position.x(), position.z(), 32);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ public class MantleWriterOverlayTest {
|
||||
IrisPlatforms.bind(platform);
|
||||
|
||||
engineMantle = mock(EngineMantle.class);
|
||||
when(engineMantle.getComplex()).thenReturn(mock(IrisComplex.class));
|
||||
Engine engine = mock(Engine.class);
|
||||
dimension = mock(IrisDimension.class);
|
||||
mantle = mock(Mantle.class);
|
||||
|
||||
@@ -2,6 +2,7 @@ package art.arcane.iris.engine.mantle;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveAction;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveCell;
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
@@ -63,6 +64,7 @@ public class MantleWriterPreObjectJournalTest {
|
||||
IrisMatterSupport.ensureRegistered();
|
||||
|
||||
EngineMantle engineMantle = mock(EngineMantle.class);
|
||||
when(engineMantle.getComplex()).thenReturn(mock(IrisComplex.class));
|
||||
Mantle<Matter> mantle = mock(Mantle.class);
|
||||
MantleChunk<Matter> chunk = mock(MantleChunk.class);
|
||||
matter = new IrisMatter(16, 16, 16);
|
||||
|
||||
+112
-4
@@ -1,8 +1,17 @@
|
||||
package art.arcane.iris.engine.mantle.components;
|
||||
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyCaveVoxelViewFactory;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
|
||||
import art.arcane.iris.engine.hydrology.RiverFootprint;
|
||||
import art.arcane.iris.engine.hydrology.cave.CavePosition;
|
||||
import art.arcane.iris.engine.hydrology.cave.CaveVoxel;
|
||||
import art.arcane.iris.engine.mantle.EngineMantle;
|
||||
import art.arcane.iris.engine.mantle.MantleComponent;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.project.stream.ProceduralStream;
|
||||
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
@@ -12,15 +21,19 @@ import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyDouble;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@@ -65,8 +78,9 @@ public class MantleHydrologyCaveVoxelViewTest {
|
||||
MantleHydrologyCaveVoxelView view = new MantleHydrologyCaveVoxelView(
|
||||
mantle,
|
||||
128,
|
||||
(x, z) -> 60,
|
||||
(chunkX, chunkZ) -> loaded.add(chunkX + "," + chunkZ)
|
||||
new MantleHydrologyCaveVoxelView.TerrainSources((x, z) -> new MantleHydrologyCaveVoxelView.TerrainColumn(60, false),
|
||||
(x, y, z) -> y <= 60,
|
||||
(chunkX, chunkZ) -> loaded.add(chunkX + "," + chunkZ))
|
||||
);
|
||||
|
||||
assertEquals(CaveVoxel.CAVE_AIR, view.voxelAt(new CavePosition(0, 20, 0)));
|
||||
@@ -100,12 +114,106 @@ public class MantleHydrologyCaveVoxelViewTest {
|
||||
MantleHydrologyCaveVoxelView view = new MantleHydrologyCaveVoxelView(
|
||||
mantle,
|
||||
128,
|
||||
(x, z) -> 60,
|
||||
(chunkX, chunkZ) -> loaded.add(chunkX + "," + chunkZ)
|
||||
new MantleHydrologyCaveVoxelView.TerrainSources((x, z) -> new MantleHydrologyCaveVoxelView.TerrainColumn(60, false),
|
||||
(x, y, z) -> y <= 60,
|
||||
(chunkX, chunkZ) -> loaded.add(chunkX + "," + chunkZ))
|
||||
);
|
||||
|
||||
assertTrue(view.isOpenToSurface(new CavePosition(4, 20, 4)));
|
||||
assertFalse(view.isOpenToSurface(new CavePosition(5, 20, 4)));
|
||||
assertEquals(List.of("0,0"), loaded);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void terrainUndercutsRemainOpenWithoutCaveMatter() {
|
||||
Mantle<Matter> mantle = mock(Mantle.class);
|
||||
doReturn(mock(MantleChunk.class)).when(mantle).getChunk(anyInt(), anyInt());
|
||||
MantleHydrologyCaveVoxelView view = new MantleHydrologyCaveVoxelView(mantle, 128,
|
||||
new MantleHydrologyCaveVoxelView.TerrainSources((x, z) -> new MantleHydrologyCaveVoxelView.TerrainColumn(80, false),
|
||||
(x, y, z) -> y <= 40 || y >= 65 && y <= 80,
|
||||
(chunkX, chunkZ) -> { }));
|
||||
|
||||
assertEquals(CaveVoxel.SOLID, view.voxelAt(new CavePosition(0, 40, 0)));
|
||||
assertEquals(CaveVoxel.CAVE_AIR, view.voxelAt(new CavePosition(0, 41, 0)));
|
||||
assertEquals(CaveVoxel.CAVE_AIR, view.voxelAt(new CavePosition(0, 64, 0)));
|
||||
assertEquals(CaveVoxel.SOLID, view.voxelAt(new CavePosition(0, 65, 0)));
|
||||
assertFalse(view.isOpenToSurface(new CavePosition(0, 50, 0)));
|
||||
assertTrue(view.isOpenToSurface(new CavePosition(0, 81, 0)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void footprintOwnedBedsFillNaturalGapsEvenWhenTheirHeightIsUnchanged() {
|
||||
Mantle<Matter> mantle = mock(Mantle.class);
|
||||
Engine engine = configuredEngine(mantle);
|
||||
IrisComplex complex = undercutTerrain();
|
||||
HydrologyColumnSample sample = mock(HydrologyColumnSample.class);
|
||||
when(sample.x()).thenReturn(0);
|
||||
when(sample.z()).thenReturn(0);
|
||||
when(sample.terrainHeight()).thenReturn(80);
|
||||
when(sample.primarySurfaceLayer()).thenReturn(Optional.of(mock(HydrologyColumnLayer.class)));
|
||||
RiverFootprint footprint = new RiverFootprint(Map.of(RiverFootprint.pack(0, 0), sample));
|
||||
MantleHydrologyCaveVoxelView view = new MantleHydrologyCaveVoxelView(engine, complex, footprint);
|
||||
|
||||
assertEquals(CaveVoxel.SOLID, view.voxelAt(new CavePosition(0, 50, 0)));
|
||||
assertEquals(CaveVoxel.CAVE_AIR, view.voxelAt(new CavePosition(1, 50, 0)));
|
||||
assertEquals(CaveVoxel.CAVE_AIR, view.voxelAt(new CavePosition(0, 81, 0)));
|
||||
assertFalse(view.isOpenToSurface(new CavePosition(0, 50, 0)));
|
||||
verify(complex, never()).isNaturalTerrainSolid(0, 50, 0);
|
||||
verify(complex, never()).sampleHydrologyColumn(anyDouble(), anyDouble());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void plannedOwnershipPreservesOwnedBedsAndUnownedUndercutsWithoutHydrologyRecursion() {
|
||||
Mantle<Matter> mantle = mock(Mantle.class);
|
||||
Engine engine = configuredEngine(mantle);
|
||||
IrisComplex complex = undercutTerrain();
|
||||
HydrologyCaveVoxelViewFactory.PlannedSurface planned = new HydrologyCaveVoxelViewFactory.PlannedSurface() {
|
||||
@Override
|
||||
public int resolve(int x, int z, int naturalHeight) {
|
||||
return x == 0 ? 55 : naturalHeight;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean ownsTerrain(int x, int z) {
|
||||
return x == 0;
|
||||
}
|
||||
};
|
||||
doReturn(new MatterCavern(true, "", (byte) 0)).when(mantle).get(0, 20, 0, MatterCavern.class);
|
||||
MantleHydrologyCaveVoxelView view = new MantleHydrologyCaveVoxelView(engine, complex, planned);
|
||||
|
||||
assertEquals(CaveVoxel.SOLID, view.voxelAt(new CavePosition(0, 50, 0)));
|
||||
assertEquals(CaveVoxel.CAVE_AIR, view.voxelAt(new CavePosition(1, 50, 0)));
|
||||
assertEquals(CaveVoxel.CAVE_AIR, view.voxelAt(new CavePosition(0, 20, 0)));
|
||||
assertEquals(CaveVoxel.SOLID, view.voxelAt(new CavePosition(0, 55, 0)));
|
||||
assertEquals(CaveVoxel.CAVE_AIR, view.voxelAt(new CavePosition(0, 56, 0)));
|
||||
assertEquals(CaveVoxel.CAVE_AIR, view.voxelAt(new CavePosition(0, 80, 0)));
|
||||
verify(complex, never()).sampleHydrologyColumn(anyDouble(), anyDouble());
|
||||
}
|
||||
|
||||
private static Engine configuredEngine(Mantle<Matter> mantle) {
|
||||
doReturn(mock(MantleChunk.class)).when(mantle).getChunk(anyInt(), anyInt());
|
||||
when(mantle.getWorldHeight()).thenReturn(128);
|
||||
when(mantle.hasFlag(anyInt(), anyInt(), eq(ReservedFlag.CARVED))).thenReturn(true);
|
||||
Engine engine = mock(Engine.class);
|
||||
EngineMantle engineMantle = mock(EngineMantle.class);
|
||||
when(engine.getMantle()).thenReturn(engineMantle);
|
||||
when(engineMantle.getMantle()).thenReturn(mantle);
|
||||
MantleComponent carving = mock(MantleComponent.class);
|
||||
when(carving.isEnabled()).thenReturn(true);
|
||||
when(engineMantle.getRegisteredComponents()).thenReturn(Map.of(ReservedFlag.CARVED, carving));
|
||||
return engine;
|
||||
}
|
||||
|
||||
private static IrisComplex undercutTerrain() {
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
when(complex.getNaturalHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 80D));
|
||||
when(complex.isNaturalTerrainSolid(anyInt(), anyInt(), anyInt())).thenAnswer(invocation -> {
|
||||
int y = invocation.getArgument(1);
|
||||
return y <= 40 || y >= 65 && y <= 80;
|
||||
});
|
||||
return complex;
|
||||
}
|
||||
}
|
||||
+155
@@ -1,18 +1,35 @@
|
||||
package art.arcane.iris.engine.mantle.components;
|
||||
|
||||
import art.arcane.iris.core.link.Identifier;
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.history.BoundaryColumnGeometry;
|
||||
import art.arcane.iris.engine.history.TerrainBoundarySignature;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveAction;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveCell;
|
||||
import art.arcane.iris.engine.mantle.EngineMantle;
|
||||
import art.arcane.iris.engine.mantle.MantleWriter;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.terrain.Terrain3DColumnFixtures;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
import art.arcane.iris.testsupport.BukkitTestServer;
|
||||
import art.arcane.iris.util.project.matter.IrisMatterSupport;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.matter.IrisMatter;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.matter.MatterCavern;
|
||||
import org.junit.Test;
|
||||
import org.mockito.InOrder;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
@@ -22,6 +39,7 @@ import static org.junit.Assert.assertThrows;
|
||||
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.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
@@ -106,6 +124,90 @@ public class ObjectDestinationTransactionTest {
|
||||
assertArrayEquals(new byte[]{0, 0}, transaction.getCarvedColumn(0, 0, 2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void densityOpeningsReachTransactionsBeforeMantleCarving() {
|
||||
try (TerrainFixture fixture = new TerrainFixture()) {
|
||||
ObjectDestinationTransaction transaction = new ObjectDestinationTransaction(fixture.writer, 0, 0);
|
||||
|
||||
assertArrayEquals(new byte[]{0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0},
|
||||
transaction.getCarvedColumn(0, 0, 16));
|
||||
assertFalse(transaction.isCarved(0, 4, 0));
|
||||
assertTrue(transaction.isCarved(0, 5, 0));
|
||||
assertTrue(transaction.isCarved(0, 9, 0));
|
||||
assertFalse(transaction.isCarved(0, 10, 0));
|
||||
assertArrayEquals(new byte[]{0, 0, 0, 0, 0, 1}, transaction.getCarvedColumn(0, 0, 6));
|
||||
assertArrayEquals(new byte[0], transaction.getCarvedColumn(0, 0, -1));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void densityMaskPreservesPrerequisiteJournalAndHydrologyPrecedence() {
|
||||
try (TerrainFixture fixture = new TerrainFixture()) {
|
||||
MatterCavern cavern = new MatterCavern(true, "", (byte) 0);
|
||||
fixture.matter.<MatterCavern>slice(MatterCavern.class).set(0, 3, 0, cavern);
|
||||
fixture.writer.withComponentPriority(2, () -> {
|
||||
fixture.writer.clearData(0, 3, 0, MatterCavern.class);
|
||||
fixture.writer.setData(0, 4, 0, cavern);
|
||||
});
|
||||
fixture.matter.<HydrologyCaveCell>slice(HydrologyCaveCell.class)
|
||||
.set(0, 6, 0, HydrologyCaveCell.of(HydrologyCaveAction.SEAL_GUARD));
|
||||
fixture.matter.<HydrologyCaveCell>slice(HydrologyCaveCell.class)
|
||||
.set(0, 12, 0, HydrologyCaveCell.of(HydrologyCaveAction.DRY_AIR));
|
||||
ObjectDestinationTransaction transaction = new ObjectDestinationTransaction(fixture.writer, 0, 0);
|
||||
|
||||
assertArrayEquals(new byte[]{0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0},
|
||||
transaction.getCarvedColumn(0, 0, 16));
|
||||
assertTrue(transaction.isCarved(0, 3, 0));
|
||||
assertFalse(transaction.isCarved(0, 4, 0));
|
||||
assertFalse(transaction.isCarved(0, 6, 0));
|
||||
assertTrue(transaction.isCarved(0, 12, 0));
|
||||
|
||||
transaction.setData(0, 5, 0, mock(PlatformBlockState.class));
|
||||
|
||||
assertFalse(transaction.isCarved(0, 5, 0));
|
||||
assertEquals(0, transaction.getCarvedColumn(0, 0, 16)[5]);
|
||||
assertTrue(fixture.writer.isPrerequisiteCarved(0, 5, 0));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void additionalTerrainOwnershipExcludesRootDensityOpenings() {
|
||||
try (TerrainFixture fixture = new TerrainFixture()) {
|
||||
when(fixture.engine.isAdditionalTerrainOwned(0, 6, 0)).thenReturn(true);
|
||||
when(fixture.complex.isTerrain3DOpening(0, 6, 0)).thenReturn(false);
|
||||
ObjectDestinationTransaction transaction = new ObjectDestinationTransaction(fixture.writer, 0, 0);
|
||||
|
||||
assertArrayEquals(new byte[]{0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0},
|
||||
transaction.getCarvedColumn(0, 0, 16));
|
||||
assertFalse(transaction.isCarved(0, 6, 0));
|
||||
assertTrue(transaction.isCarved(0, 7, 0));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void historicalGeometryOwnsTransactionalCarvingOverCurrentDensity() {
|
||||
try (TerrainFixture fixture = new TerrainFixture()) {
|
||||
BoundaryColumnGeometry.Voxel solid = new BoundaryColumnGeometry.Voxel(
|
||||
"minecraft:stone", BoundaryColumnGeometry.Phase.SOLID, "", false);
|
||||
BoundaryColumnGeometry.Voxel air = new BoundaryColumnGeometry.Voxel(
|
||||
"minecraft:air", BoundaryColumnGeometry.Phase.AIR, "", false);
|
||||
List<BoundaryColumnGeometry.Voxel> voxels = new ArrayList<>(Collections.nCopies(16, solid));
|
||||
voxels.set(2, air);
|
||||
TerrainBoundarySignature historical = mock(TerrainBoundarySignature.class);
|
||||
when(historical.geometry()).thenReturn(BoundaryColumnGeometry.fromVoxels(-64, voxels));
|
||||
when(fixture.engine.getMinHeight()).thenReturn(-64);
|
||||
when(fixture.complex.resolvedTerrainColumn(0, 0)).thenReturn(Optional.of(historical));
|
||||
fixture.matter.<HydrologyCaveCell>slice(HydrologyCaveCell.class)
|
||||
.set(0, 2, 0, HydrologyCaveCell.of(HydrologyCaveAction.SEAL_GUARD));
|
||||
ObjectDestinationTransaction transaction = new ObjectDestinationTransaction(fixture.writer, 0, 0);
|
||||
|
||||
assertArrayEquals(new byte[]{0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
||||
transaction.getCarvedColumn(0, 0, 16));
|
||||
assertTrue(transaction.isCarved(0, 2, 0));
|
||||
assertFalse(transaction.isCarved(0, 5, 0));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failedCommitRestoresEveryTouchedDestinationCell() {
|
||||
MantleWriter writer = writer();
|
||||
@@ -239,6 +341,59 @@ public class ObjectDestinationTransactionTest {
|
||||
return writer;
|
||||
}
|
||||
|
||||
private static final class TerrainFixture implements AutoCloseable {
|
||||
private final IrisPlatform previousPlatform;
|
||||
private final IrisComplex complex = mock(IrisComplex.class);
|
||||
private final Engine engine = mock(Engine.class);
|
||||
private final Matter matter;
|
||||
private final MantleWriter writer;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private TerrainFixture() {
|
||||
BukkitTestServer.install();
|
||||
previousPlatform = IrisPlatforms.isBound() ? IrisPlatforms.get() : null;
|
||||
IrisPlatforms.unbind();
|
||||
PlatformBlockState air = mock(PlatformBlockState.class);
|
||||
PlatformRegistries registries = mock(PlatformRegistries.class);
|
||||
IrisPlatform platform = mock(IrisPlatform.class);
|
||||
when(air.isAir()).thenReturn(true);
|
||||
when(registries.air()).thenReturn(air);
|
||||
when(registries.block(anyString())).thenReturn(air);
|
||||
when(platform.registries()).thenReturn(registries);
|
||||
IrisPlatforms.bind(platform);
|
||||
IrisMatterSupport.ensureRegistered();
|
||||
|
||||
EngineMantle engineMantle = mock(EngineMantle.class);
|
||||
Mantle<Matter> mantle = mock(Mantle.class);
|
||||
MantleChunk<Matter> chunk = mock(MantleChunk.class);
|
||||
matter = new IrisMatter(16, 16, 16);
|
||||
when(engineMantle.getEngine()).thenReturn(engine);
|
||||
when(engineMantle.getComplex()).thenReturn(complex);
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
when(engine.getDimension()).thenReturn(mock(IrisDimension.class));
|
||||
when(mantle.getWorldHeight()).thenReturn(16);
|
||||
when(mantle.getChunk(0, 0)).thenReturn(chunk);
|
||||
when(chunk.use()).thenReturn(chunk);
|
||||
when(chunk.exists(0)).thenReturn(true);
|
||||
when(chunk.getOrCreate(0)).thenReturn(matter);
|
||||
when(chunk.get(0)).thenReturn(matter);
|
||||
when(complex.terrainColumn(0, 0)).thenReturn(Terrain3DColumnFixtures.spans(8, 0, 4, 10, 11));
|
||||
for (int y = 5; y <= 9; y++) {
|
||||
when(complex.isTerrain3DOpening(0, y, 0)).thenReturn(true);
|
||||
}
|
||||
writer = new MantleWriter(engineMantle, mantle, 0, 0, 0, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
writer.close();
|
||||
IrisPlatforms.unbind();
|
||||
if (previousPlatform != null) {
|
||||
IrisPlatforms.bind(previousPlatform);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record Marker(String value) {
|
||||
}
|
||||
}
|
||||
+2
@@ -1,6 +1,7 @@
|
||||
package art.arcane.iris.engine.modifier;
|
||||
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisDimensionCarvingResolver;
|
||||
import art.arcane.volmlib.util.math.BlockPosition;
|
||||
@@ -127,6 +128,7 @@ public class IrisCarveModifierBiomeBlendTest {
|
||||
private static IrisCarveModifier modifier(Engine engine, RNG rng) throws ReflectiveOperationException {
|
||||
IrisCarveModifier modifier = mock(IrisCarveModifier.class, CALLS_REAL_METHODS);
|
||||
doReturn(engine).when(modifier).getEngine();
|
||||
doReturn(mock(IrisComplex.class)).when(modifier).getComplex();
|
||||
Field field = IrisCarveModifier.class.getDeclaredField("rng");
|
||||
field.setAccessible(true);
|
||||
field.set(modifier, rng);
|
||||
|
||||
+22
@@ -3,12 +3,14 @@ package art.arcane.iris.engine.modifier;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.loader.ResourceLoader;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisDimensionCarvingResolver;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveAction;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveCell;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.project.hunk.Hunk;
|
||||
import art.arcane.iris.util.project.stream.ProceduralStream;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.matter.MatterCavern;
|
||||
@@ -48,6 +50,7 @@ public class IrisCarveModifierBoundarySupportTest {
|
||||
|
||||
IrisCarveModifier modifier = mock(IrisCarveModifier.class, CALLS_REAL_METHODS);
|
||||
doReturn(engine).when(modifier).getEngine();
|
||||
doReturn(mock(IrisComplex.class)).when(modifier).getComplex();
|
||||
Field rng = IrisCarveModifier.class.getDeclaredField("rng");
|
||||
rng.setAccessible(true);
|
||||
rng.set(modifier, new RNG(71L));
|
||||
@@ -81,6 +84,25 @@ public class IrisCarveModifierBoundarySupportTest {
|
||||
assertSame(resolvedCeiling, ceiling);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void terrainOpeningKeepsItsSurfaceBiomeOverCaveMatter() {
|
||||
IrisBiome surface = new IrisBiome();
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
ProceduralStream<IrisBiome> biomes = mock(ProceduralStream.class);
|
||||
doReturn(true).when(complex).isTerrain3DOpening(40, 42, 44);
|
||||
doReturn(biomes).when(complex).getTrueBiomeStream();
|
||||
doReturn(surface).when(biomes).get(40, 44);
|
||||
IrisCarveModifier modifier = mock(IrisCarveModifier.class, CALLS_REAL_METHODS);
|
||||
doReturn(complex).when(modifier).getComplex();
|
||||
|
||||
IrisBiome biome = modifier.resolveCaveBoundaryBiome(
|
||||
new MatterCavern(true, "carving/deep", (byte) 0), 40, 42, 44,
|
||||
new IrisDimensionCarvingResolver.State(), new Long2ObjectOpenHashMap<>(), new HashMap<>());
|
||||
|
||||
assertSame(surface, biome);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void gravityFloorLayerRequiresSolidSupportBelowItsTarget() {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package art.arcane.iris.engine.modifier;
|
||||
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.actuator.IrisDecorantActuator;
|
||||
import art.arcane.iris.engine.decorator.IrisCeilingDecorator;
|
||||
import art.arcane.iris.engine.decorator.IrisSurfaceDecorator;
|
||||
@@ -176,6 +177,7 @@ public class IrisNaturalCaveScanTest {
|
||||
Mantle<Matter> mantle = mock(Mantle.class);
|
||||
chunk = mock(MantleChunk.class);
|
||||
doReturn(engine).when(modifier).getEngine();
|
||||
doReturn(mock(IrisComplex.class)).when(modifier).getComplex();
|
||||
doReturn(32).when(engine).getHeight();
|
||||
doReturn(new SeedManager(1337L)).when(engine).getSeedManager();
|
||||
doReturn(engineMantle).when(engine).getMantle();
|
||||
|
||||
@@ -28,6 +28,45 @@ public final class IrisBiomeLayerSlopeSourceTest {
|
||||
assertExplicitSlopeStreamControlsSurfacePaletteSelection(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lowerLedgeSlopeControlsSurfacePaletteSelection() {
|
||||
assertLowerLedgeSlopeControlsSurfacePaletteSelection(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lowerLedgeSlopeControlsLockedSurfacePaletteSelection() {
|
||||
assertLowerLedgeSlopeControlsSurfacePaletteSelection(true);
|
||||
}
|
||||
|
||||
private static void assertLowerLedgeSlopeControlsSurfacePaletteSelection(boolean lockLayers) {
|
||||
IrisData data = mock(IrisData.class);
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
IrisBiomePaletteLayer grass = mock(IrisBiomePaletteLayer.class);
|
||||
CNG heightGenerator = mock(CNG.class);
|
||||
PlatformBlockState block = mock(PlatformBlockState.class);
|
||||
RNG rng = new RNG(19L);
|
||||
IrisBiome biome = new IrisBiome().setLockLayers(lockLayers).setLayers(new KList<>(grass));
|
||||
when(complex.hasTerrain3D()).thenReturn(true);
|
||||
when(complex.terrainSurfaceSlope(12, 32, -8)).thenReturn(0D);
|
||||
when(complex.terrainSurfaceSlope(12, 96, -8)).thenReturn(5D);
|
||||
when(grass.getZoom()).thenReturn(1D);
|
||||
when(grass.getMinHeight()).thenReturn(1);
|
||||
when(grass.getMaxHeight()).thenReturn(1);
|
||||
when(grass.getSlopeCondition()).thenReturn(new IrisSlopeClip(0D, 2.6D));
|
||||
when(grass.getHeightGenerator(any(RNG.class), same(data))).thenReturn(heightGenerator);
|
||||
when(heightGenerator.fit(1, 1, 12D, -8D)).thenReturn(1);
|
||||
when(grass.get(rng, 0, 12D, 0D, -8D, data)).thenReturn(block);
|
||||
|
||||
KList<PlatformBlockState> ledge = biome.generateLayers(
|
||||
new IrisDimension(), 12D, -8D, rng, 1, 32, data, complex);
|
||||
KList<PlatformBlockState> cap = biome.generateLayers(
|
||||
new IrisDimension(), 12D, -8D, rng, 1, 96, data, complex);
|
||||
|
||||
assertEquals(1, ledge.size());
|
||||
assertSame(block, ledge.get(0));
|
||||
assertTrue(cap.isEmpty());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static void assertExplicitSlopeStreamControlsSurfacePaletteSelection(boolean lockLayers) {
|
||||
IrisData data = mock(IrisData.class);
|
||||
|
||||
+20
@@ -1,12 +1,32 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.mantle.components.IslandObjectPlacer;
|
||||
import art.arcane.iris.engine.mantle.MantleWriter;
|
||||
import org.junit.Test;
|
||||
|
||||
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.when;
|
||||
|
||||
public class IslandObjectPlacerAnchorFaceTest {
|
||||
|
||||
@Test
|
||||
public void islandMaskOwnsSurfaceSupportAboveRootTerrainAndInsideRootOpenings() {
|
||||
FloatingIslandSample sample = sampleWithBottomAt(100, 0);
|
||||
MantleWriter writer = mock(MantleWriter.class);
|
||||
when(writer.isSurfaceSolid(0, 109, 0)).thenReturn(false);
|
||||
when(writer.isCarved(0, 109, 0)).thenReturn(true);
|
||||
IslandObjectPlacer placer = IslandObjectPlacer.top(writer, (x, z) -> sample, null, 109);
|
||||
|
||||
assertTrue(placer.isSurfaceSolid(0, 109, 0));
|
||||
assertFalse(placer.isCarved(0, 109, 0));
|
||||
assertFalse(placer.isSurfaceSolid(0, 108, 0));
|
||||
assertTrue(placer.isCarved(0, 108, 0));
|
||||
assertFalse(placer.isSurfaceSolid(0, 110, 0));
|
||||
}
|
||||
|
||||
private FloatingIslandSample sampleWithBottomAt(int baseY, int bottomOffset) {
|
||||
boolean[] mask = new boolean[10];
|
||||
mask[bottomOffset] = true;
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package art.arcane.iris.engine.terrain;
|
||||
|
||||
public final class Terrain3DColumnFixtures {
|
||||
private Terrain3DColumnFixtures() {
|
||||
}
|
||||
|
||||
public static Terrain3DColumn spans(double baseHeight, int... boundaries) {
|
||||
return new Terrain3DColumn(baseHeight, boundaries[1] + 1, true, boundaries);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
package art.arcane.iris.engine.terrain;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class Terrain3DFragmentFilterTest {
|
||||
@Test
|
||||
public void removesAnEntire512BlockIslandAcrossChunkBoundaries() {
|
||||
Set<Voxel> blocks = new HashSet<>();
|
||||
for (int x = 12; x <= 19; x++) {
|
||||
for (int z = -20; z <= -13; z++) {
|
||||
for (int y = 24; y <= 31; y++) {
|
||||
blocks.add(new Voxel(x, y, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
Fixture fixture = new Fixture(blocks, true);
|
||||
|
||||
for (Voxel block : blocks) {
|
||||
Terrain3DColumn column = fixture.filter.column(block.x(), block.z());
|
||||
assertFalse(column.isSolid(block.y()));
|
||||
assertEquals(8, column.topY());
|
||||
assertEquals(1, column.spanCount());
|
||||
assertTrue(column.isSolid(8));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preservesThinShelvesConnectedSidewaysAroundACorner() {
|
||||
Set<Voxel> blocks = new HashSet<>();
|
||||
for (int x = 0; x <= 15; x++) {
|
||||
blocks.add(new Voxel(x, 24, 0));
|
||||
}
|
||||
for (int z = 1; z <= 15; z++) {
|
||||
blocks.add(new Voxel(15, 24, z));
|
||||
}
|
||||
for (int y = 9; y < 24; y++) {
|
||||
blocks.add(new Voxel(15, y, 15));
|
||||
}
|
||||
Fixture fixture = new Fixture(blocks, true);
|
||||
|
||||
for (Voxel block : blocks) {
|
||||
assertTrue(fixture.filter.column(block.x(), block.z()).isSolid(block.y()));
|
||||
}
|
||||
assertFalse(fixture.filter.column(0, 0).isSolid(23));
|
||||
assertEquals(24, fixture.filter.column(0, 0).surfaceY(24));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aDiagonalContactDoesNotKeepAnOtherwiseDetachedBlock() {
|
||||
Set<Voxel> blocks = new HashSet<>();
|
||||
for (int y = 9; y <= 24; y++) {
|
||||
blocks.add(new Voxel(0, y, 0));
|
||||
}
|
||||
blocks.add(new Voxel(1, 24, 1));
|
||||
Fixture fixture = new Fixture(blocks, true);
|
||||
|
||||
assertTrue(fixture.filter.column(0, 0).isSolid(24));
|
||||
assertFalse(fixture.filter.column(1, 1).isSolid(24));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preservesAll513BlocksAfterAPartialComponentClassification() {
|
||||
Set<Voxel> blocks = new HashSet<>();
|
||||
for (int x = 0; x < 513; x++) {
|
||||
blocks.add(new Voxel(x, 24, 0));
|
||||
}
|
||||
Fixture fixture = new Fixture(blocks, true);
|
||||
|
||||
assertTrue(fixture.filter.column(0, 0).isSolid(24));
|
||||
assertTrue(fixture.lookups.get() <= 1 + 4 * 512);
|
||||
for (int x = 512; x >= 0; x--) {
|
||||
assertTrue(fixture.filter.column(x, 0).isSolid(24));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aLargeVerticalIslandNeedsNoNeighbourSampling() {
|
||||
Set<Voxel> blocks = new HashSet<>();
|
||||
for (int y = 24; y < 537; y++) {
|
||||
blocks.add(new Voxel(0, y, 0));
|
||||
}
|
||||
Fixture fixture = new Fixture(blocks, true);
|
||||
|
||||
assertEquals(536, fixture.filter.column(0, 0).topY());
|
||||
assertEquals(1, fixture.lookups.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void completeCacheEvictionAndConcurrentQueriesPreserveDecisions() throws Exception {
|
||||
Set<Voxel> blocks = new HashSet<>();
|
||||
for (int x = -18; x <= -15; x++) {
|
||||
for (int z = 14; z <= 17; z++) {
|
||||
for (int y = 24; y <= 27; y++) {
|
||||
blocks.add(new Voxel(x, y, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int x = 14; x <= 526; x++) {
|
||||
blocks.add(new Voxel(x, 24, 16));
|
||||
}
|
||||
Fixture evicted = new Fixture(blocks, false);
|
||||
Fixture cached = new Fixture(blocks, true);
|
||||
int[] coordinates = {-18, -17, -16, -15, 14, 15, 16, 526};
|
||||
try (ExecutorService executor = Executors.newFixedThreadPool(4)) {
|
||||
List<Future<Terrain3DColumn>> results = new ArrayList<>();
|
||||
for (int x : coordinates) {
|
||||
results.add(executor.submit(() -> evicted.filter.column(x, 16)));
|
||||
}
|
||||
for (int index = results.size() - 1; index >= 0; index--) {
|
||||
assertEquals(cached.filter.column(coordinates[index], 16),
|
||||
results.get(index).get(10, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sharedConcurrentQueriesPublishWholeRemovedAndKeptComponents() throws Exception {
|
||||
Set<Voxel> blocks = new HashSet<>();
|
||||
for (int x = 12; x <= 19; x++) {
|
||||
for (int z = -20; z <= -13; z++) {
|
||||
for (int y = 24; y <= 31; y++) {
|
||||
blocks.add(new Voxel(x, y, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int x = 32; x <= 544; x++) {
|
||||
blocks.add(new Voxel(x, 24, 16));
|
||||
}
|
||||
Fixture fixture = new Fixture(blocks, true);
|
||||
CountDownLatch start = new CountDownLatch(1);
|
||||
try (ExecutorService executor = Executors.newFixedThreadPool(8)) {
|
||||
List<Future<Terrain3DColumn>> removed = new ArrayList<>();
|
||||
List<Future<Terrain3DColumn>> kept = new ArrayList<>();
|
||||
for (int index = 0; index < 64; index++) {
|
||||
int removedX = 12 + index % 8;
|
||||
int removedZ = -20 + index / 8;
|
||||
int keptX = 32 + index * 8;
|
||||
removed.add(executor.submit(() -> {
|
||||
assertTrue(start.await(10, TimeUnit.SECONDS));
|
||||
return fixture.filter.column(removedX, removedZ);
|
||||
}));
|
||||
kept.add(executor.submit(() -> {
|
||||
assertTrue(start.await(10, TimeUnit.SECONDS));
|
||||
return fixture.filter.column(keptX, 16);
|
||||
}));
|
||||
}
|
||||
start.countDown();
|
||||
for (int index = 63; index >= 0; index--) {
|
||||
assertEquals(8, removed.get(index).get(10, TimeUnit.SECONDS).topY());
|
||||
assertTrue(kept.get(index).get(10, TimeUnit.SECONDS).isSolid(24));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void spanFilteringMatchesAnIndependentVoxelComponentOracle() {
|
||||
Random random = new Random(74921);
|
||||
for (int sample = 0; sample < 18; sample++) {
|
||||
Set<Voxel> blocks = new HashSet<>();
|
||||
double probability = 0.12D + sample % 6 * 0.14D;
|
||||
for (int x = -3; x <= 3; x++) {
|
||||
for (int z = -3; z <= 3; z++) {
|
||||
for (int y = 9; y <= 20; y++) {
|
||||
if (random.nextDouble() < probability) {
|
||||
blocks.add(new Voxel(x, y, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Set<Voxel> retained = retainedBlocks(blocks);
|
||||
Fixture fixture = new Fixture(blocks, true);
|
||||
for (int x = 3; x >= -3; x--) {
|
||||
for (int z = -3; z <= 3; z++) {
|
||||
Terrain3DColumn column = fixture.filter.column(x, z);
|
||||
for (int y = 9; y <= 20; y++) {
|
||||
Voxel block = new Voxel(x, y, z);
|
||||
assertEquals("Sample " + sample + " at " + block,
|
||||
retained.contains(block), column.isSolid(y));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Set<Voxel> retainedBlocks(Set<Voxel> blocks) {
|
||||
Set<Voxel> remaining = new HashSet<>(blocks);
|
||||
Set<Voxel> retained = new HashSet<>();
|
||||
while (!remaining.isEmpty()) {
|
||||
Voxel start = remaining.iterator().next();
|
||||
remaining.remove(start);
|
||||
Set<Voxel> component = new HashSet<>();
|
||||
ArrayDeque<Voxel> pending = new ArrayDeque<>();
|
||||
pending.add(start);
|
||||
boolean grounded = false;
|
||||
while (!pending.isEmpty()) {
|
||||
Voxel block = pending.removeFirst();
|
||||
component.add(block);
|
||||
grounded |= block.y() == 9;
|
||||
for (Voxel neighbour : List.of(new Voxel(block.x() - 1, block.y(), block.z()),
|
||||
new Voxel(block.x() + 1, block.y(), block.z()),
|
||||
new Voxel(block.x(), block.y() - 1, block.z()),
|
||||
new Voxel(block.x(), block.y() + 1, block.z()),
|
||||
new Voxel(block.x(), block.y(), block.z() - 1),
|
||||
new Voxel(block.x(), block.y(), block.z() + 1))) {
|
||||
if (remaining.remove(neighbour)) {
|
||||
pending.addLast(neighbour);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (grounded || component.size() > 512) {
|
||||
retained.addAll(component);
|
||||
}
|
||||
}
|
||||
return retained;
|
||||
}
|
||||
|
||||
private record Voxel(int x, int y, int z) {
|
||||
}
|
||||
|
||||
private static final class Fixture {
|
||||
private final Set<Voxel> blocks;
|
||||
private final boolean cache;
|
||||
private final int maximumY;
|
||||
private final Map<Long, Terrain3DFragmentFilter.DensityColumn> columns = new ConcurrentHashMap<>();
|
||||
private final AtomicInteger lookups = new AtomicInteger();
|
||||
private final Terrain3DFragmentFilter filter = new Terrain3DFragmentFilter(this::column);
|
||||
|
||||
private Fixture(Set<Voxel> blocks, boolean cache) {
|
||||
this.blocks = Set.copyOf(blocks);
|
||||
this.cache = cache;
|
||||
int highest = 9;
|
||||
for (Voxel block : blocks) {
|
||||
highest = Math.max(highest, block.y());
|
||||
}
|
||||
maximumY = highest + 1;
|
||||
}
|
||||
|
||||
private Terrain3DFragmentFilter.DensityColumn column(int x, int z) {
|
||||
lookups.incrementAndGet();
|
||||
long key = (long) x << 32 ^ z & 0xffffffffL;
|
||||
return cache ? columns.computeIfAbsent(key, ignored -> createColumn(x, z)) : createColumn(x, z);
|
||||
}
|
||||
|
||||
private Terrain3DFragmentFilter.DensityColumn createColumn(int x, int z) {
|
||||
int[] boundaries = new int[maximumY * 2];
|
||||
boundaries[0] = 0;
|
||||
int count = 1;
|
||||
boolean previousSolid = true;
|
||||
for (int y = 9; y <= maximumY; y++) {
|
||||
boolean solid = blocks.contains(new Voxel(x, y, z));
|
||||
if (solid != previousSolid) {
|
||||
boundaries[count++] = solid ? y : y - 1;
|
||||
previousSolid = solid;
|
||||
}
|
||||
}
|
||||
return new Terrain3DFragmentFilter.DensityColumn(
|
||||
new Terrain3DColumn(8, 9, true, Arrays.copyOf(boundaries, count)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
package art.arcane.iris.engine.terrain;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisTerrain3D;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class Terrain3DRuntimeTest {
|
||||
@Test
|
||||
public void omittedProfilesPreserveExactHeightsWithoutCreatingNoise() {
|
||||
AtomicInteger compilations = new AtomicInteger();
|
||||
IrisBiome biome = new IrisBiome();
|
||||
Terrain3DRuntime runtime = new Terrain3DRuntime(
|
||||
new Terrain3DRuntime.Sources((x, z) -> 100.25D + x * 0.125D, (x, z) -> biome),
|
||||
options(true),
|
||||
(style, seed) -> {
|
||||
compilations.incrementAndGet();
|
||||
return (x, y, z) -> 1D;
|
||||
});
|
||||
|
||||
for (int x = -20; x < 20; x++) {
|
||||
Terrain3DColumn column = runtime.column(x, 3);
|
||||
assertEquals(100.25D + x * 0.125D, column.baseHeight(), 0D);
|
||||
assertEquals(Math.round(column.baseHeight()), column.topY());
|
||||
assertEquals(1, column.spanCount());
|
||||
assertFalse(column.shaped());
|
||||
}
|
||||
assertEquals(0, compilations.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inactiveRuntimeDoesNotResolveBiomesOrNoise() {
|
||||
Terrain3DRuntime runtime = new Terrain3DRuntime(
|
||||
new Terrain3DRuntime.Sources((x, z) -> 91.5D, (x, z) -> {
|
||||
throw new AssertionError("Unexpected biome resolution");
|
||||
}), options(false), (style, seed) -> {
|
||||
throw new AssertionError("Unexpected noise compilation");
|
||||
});
|
||||
|
||||
assertFalse(runtime.active());
|
||||
assertEquals(92, runtime.column(0, 0).topY());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unchangedColumnsRetainTheOriginalHeightOutsideVoxelBounds() {
|
||||
Terrain3DRuntime runtime = new Terrain3DRuntime(
|
||||
new Terrain3DRuntime.Sources((x, z) -> 300.25D, (x, z) -> null),
|
||||
options(false), (style, seed) -> (x, y, z) -> 0D);
|
||||
|
||||
assertEquals(300.25D, runtime.column(0, 0).baseHeight(), 0D);
|
||||
assertEquals(255, runtime.column(0, 0).topY());
|
||||
assertFalse(runtime.column(0, 0).isSolid(256));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void densityAddsRockAndLeavesAirBelowProjectingLedges() {
|
||||
Terrain3DRuntime runtime = runtime(profile(), (x, y, z) -> StrictMath.sin(y * StrictMath.PI / 16D));
|
||||
Terrain3DColumn column = runtime.column(0, 0);
|
||||
|
||||
assertTrue(column.shaped());
|
||||
assertTrue(column.topY() > Math.round(column.baseHeight()));
|
||||
assertTrue(column.spanCount() > 1);
|
||||
assertTrue(column.isSolid(104));
|
||||
assertFalse(column.isSolid(84));
|
||||
assertTrue(column.isSolid(60));
|
||||
assertEquals(-1, column.surfaceY(84));
|
||||
for (int span = 0; span < column.spanCount(); span++) {
|
||||
assertEquals(column.floor(span), column.surfaceY(column.ceiling(span)));
|
||||
assertFalse(column.isSolid(column.floor(span) + 1));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isolatedDensityPeakIsRemovedFromEveryColumnQuery() {
|
||||
Terrain3DRuntime runtime = runtime(profile(),
|
||||
(x, y, z) -> x == 16D && y == 112D && z == -16D ? 1D : -1D);
|
||||
|
||||
for (int x = 14; x <= 18; x++) {
|
||||
for (int z = -18; z <= -14; z++) {
|
||||
Terrain3DColumn column = runtime.column(x, z);
|
||||
assertEquals(64, column.topY());
|
||||
assertEquals(1, column.spanCount());
|
||||
assertFalse(column.isSolid(112));
|
||||
assertEquals(-1, column.surfaceY(112));
|
||||
assertEquals(64, column.highestSolidY(112));
|
||||
assertEquals(64, column.nearestSurfaceY(112));
|
||||
assertTrue(column.isSolid(64));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void latticeMatchesTheSameFieldAcrossPositiveAndNegativeChunkBoundaries() {
|
||||
Terrain3DRuntime.NoiseSource noise = (x, y, z) -> (StrictMath.sin(x * StrictMath.PI / 23D)
|
||||
+ StrictMath.sin(y * StrictMath.PI / 16D)
|
||||
+ StrictMath.cos(z * StrictMath.PI / 29D)) / 3D;
|
||||
Terrain3DRuntime forward = runtime(profile(), noise);
|
||||
Terrain3DRuntime reverse = runtime(profile(), noise);
|
||||
|
||||
for (int x = 20; x >= -20; x--) {
|
||||
reverse.column(x, -17);
|
||||
}
|
||||
for (int x = -20; x <= 20; x++) {
|
||||
Terrain3DColumn column = forward.column(x, -17);
|
||||
assertEquals(column, reverse.column(x, -17));
|
||||
for (int y = 1; y < 160; y++) {
|
||||
double density = 96.5D - y + 32D * interpolated(noise, x, y, -17);
|
||||
assertEquals("at " + x + "," + y + ",-17", density >= 0D, column.isSolid(y));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void profileContributionsFadeIntoAnOmittedNeighbour() {
|
||||
IrisBiome shaped = new IrisBiome().setTerrain3D(profile());
|
||||
IrisBiome unchanged = new IrisBiome();
|
||||
Terrain3DRuntime runtime = new Terrain3DRuntime(
|
||||
new Terrain3DRuntime.Sources((x, z) -> 96D, (x, z) -> x < 16 ? shaped : unchanged),
|
||||
options(true), (style, seed) -> (x, y, z) -> 1D);
|
||||
|
||||
assertEquals(128, runtime.column(12, 0).topY());
|
||||
assertEquals(120, runtime.column(13, 0).topY());
|
||||
assertEquals(112, runtime.column(14, 0).topY());
|
||||
assertEquals(104, runtime.column(15, 0).topY());
|
||||
assertEquals(96, runtime.column(16, 0).topY());
|
||||
assertFalse(runtime.column(16, 0).shaped());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void signedFissureInterpolationPreservesNarrowCracksBetweenLatticeNodes() {
|
||||
IrisTerrain3D config = profile().setAmplitude(0).setCrackDepth(24).setCrackWidth(1)
|
||||
.setCrackScale(64);
|
||||
Terrain3DRuntime runtime = runtime(config, (x, y, z) -> (x - 2D) / 32D);
|
||||
|
||||
assertEquals(72, runtime.column(2, 0).topY());
|
||||
assertEquals(96, runtime.column(0, 0).topY());
|
||||
assertEquals(96, runtime.column(4, 0).topY());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void waterAndSlopeGatesKeepProtectedTerrainUnchanged() {
|
||||
IrisBiome biome = new IrisBiome().setTerrain3D(profile().setMinimumSlope(0.25D));
|
||||
Terrain3DRuntime flat = new Terrain3DRuntime(
|
||||
new Terrain3DRuntime.Sources((x, z) -> 96D, (x, z) -> biome),
|
||||
options(true), (style, seed) -> (x, y, z) -> -1D);
|
||||
assertFalse(flat.column(0, 0).shaped());
|
||||
|
||||
biome.setTerrain3D(profile().setAmplitude(128).setCrackDepth(128).setFluidClearance(8));
|
||||
Terrain3DRuntime water = new Terrain3DRuntime(
|
||||
new Terrain3DRuntime.Sources((x, z) -> 40D, (x, z) -> biome),
|
||||
new Terrain3DRuntime.Options(12, 256, 32, null, true, 4096),
|
||||
(style, seed) -> (x, y, z) -> -1D);
|
||||
assertFalse(water.column(0, 0).shaped());
|
||||
assertEquals(40, water.column(0, 0).topY());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void columnCacheReusesGeometryAndAnchorNoise() {
|
||||
AtomicInteger samples = new AtomicInteger();
|
||||
Terrain3DRuntime runtime = runtime(profile(), (x, y, z) -> {
|
||||
samples.incrementAndGet();
|
||||
return StrictMath.sin(y * StrictMath.PI / 16D);
|
||||
});
|
||||
Terrain3DColumn first = runtime.column(0, 0);
|
||||
int initialSamples = samples.get();
|
||||
assertSame(first, runtime.column(0, 0));
|
||||
assertEquals(initialSamples, samples.get());
|
||||
for (int x = 0; x < 16; x++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
runtime.column(x, z);
|
||||
}
|
||||
}
|
||||
assertTrue(samples.get() <= 49 * 18);
|
||||
int samplesBeforeClear = samples.get();
|
||||
runtime.clear();
|
||||
assertEquals(first, runtime.column(0, 0));
|
||||
assertTrue(samples.get() > samplesBeforeClear);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void concurrentAndEvictedColumnsRemainReproducible() throws Exception {
|
||||
Terrain3DRuntime.NoiseSource noise = (x, y, z) -> StrictMath.sin((x + y + z) * StrictMath.PI / 16D);
|
||||
IrisBiome biome = new IrisBiome().setTerrain3D(profile());
|
||||
Terrain3DRuntime runtime = new Terrain3DRuntime(
|
||||
new Terrain3DRuntime.Sources((x, z) -> 96D, (x, z) -> biome),
|
||||
new Terrain3DRuntime.Options(12, 256, 0, null, true, 16),
|
||||
(style, seed) -> noise);
|
||||
Terrain3DRuntime reference = runtime(profile(), noise);
|
||||
try (ExecutorService executor = Executors.newFixedThreadPool(8)) {
|
||||
List<Future<Terrain3DColumn>> results = new ArrayList<>();
|
||||
for (int index = 0; index < 128; index++) {
|
||||
int x = index % 32 - 16;
|
||||
results.add(executor.submit(() -> runtime.column(x, 0)));
|
||||
}
|
||||
for (int index = 0; index < results.size(); index++) {
|
||||
assertEquals(reference.column(index % 32 - 16, 0), results.get(index).get(10, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void repeatedColumnsKeepCoordinateAndRuntimeGeometrySeparate() {
|
||||
Terrain3DRuntime first = new Terrain3DRuntime(
|
||||
new Terrain3DRuntime.Sources((x, z) -> 96D + x * 0.25D + z * 0.5D, (x, z) -> null),
|
||||
options(true), (style, seed) -> (x, y, z) -> 0D);
|
||||
Terrain3DRuntime second = runtime(profile(), (x, y, z) -> 1D);
|
||||
|
||||
assertEquals(96D, first.column(0, 0).baseHeight(), 0D);
|
||||
assertEquals(128, second.column(0, 0).topY());
|
||||
assertEquals(96, first.column(0, 0).topY());
|
||||
assertEquals(95.75D, first.column(-1, 0).baseHeight(), 0D);
|
||||
assertEquals(95.5D, first.column(0, -1).baseHeight(), 0D);
|
||||
assertEquals(95.5D, first.column(0, -1).baseHeight(), 0D);
|
||||
assertEquals(96D, first.column(0, 0).baseHeight(), 0D);
|
||||
assertEquals(128, second.column(0, 0).topY());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void clearingRuntimeRefreshesTheLastColumnOnItsCallingThread() {
|
||||
AtomicInteger height = new AtomicInteger(96);
|
||||
IrisBiome biome = new IrisBiome().setTerrain3D(profile());
|
||||
Terrain3DRuntime runtime = new Terrain3DRuntime(
|
||||
new Terrain3DRuntime.Sources((x, z) -> height.get(), (x, z) -> biome),
|
||||
options(true), (style, seed) -> (x, y, z) -> 0D);
|
||||
|
||||
assertEquals(96, runtime.column(0, 0).topY());
|
||||
height.set(120);
|
||||
runtime.clear();
|
||||
|
||||
assertEquals(120, runtime.column(0, 0).topY());
|
||||
assertTrue(runtime.column(0, 0).isSolid(110));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void clearingRuntimeRefreshesLastColumnsOnOtherThreads() throws Exception {
|
||||
AtomicInteger height = new AtomicInteger(96);
|
||||
IrisBiome biome = new IrisBiome().setTerrain3D(profile());
|
||||
Terrain3DRuntime runtime = new Terrain3DRuntime(
|
||||
new Terrain3DRuntime.Sources((x, z) -> height.get(), (x, z) -> biome),
|
||||
options(true), (style, seed) -> (x, y, z) -> 0D);
|
||||
CountDownLatch sampled = new CountDownLatch(2);
|
||||
CountDownLatch cleared = new CountDownLatch(1);
|
||||
try (ExecutorService executor = Executors.newFixedThreadPool(2)) {
|
||||
List<Future<Terrain3DColumn>> results = new ArrayList<>();
|
||||
for (int index = 0; index < 2; index++) {
|
||||
results.add(executor.submit(() -> {
|
||||
Terrain3DColumn before = runtime.column(0, 0);
|
||||
sampled.countDown();
|
||||
assertTrue(cleared.await(10, TimeUnit.SECONDS));
|
||||
assertEquals(96, before.topY());
|
||||
return runtime.column(0, 0);
|
||||
}));
|
||||
}
|
||||
try {
|
||||
assertTrue(sampled.await(10, TimeUnit.SECONDS));
|
||||
height.set(120);
|
||||
runtime.clear();
|
||||
} finally {
|
||||
cleared.countDown();
|
||||
}
|
||||
for (Future<Terrain3DColumn> result : results) {
|
||||
assertEquals(120, result.get(10, TimeUnit.SECONDS).topY());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidCurrentFieldsAndNonfiniteNoiseFailBeforeGeometryEscapes() {
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> runtime(profile().setAmplitude(129), (x, y, z) -> 0D).column(0, 0));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> runtime(profile().setVerticalScale(Double.NaN), (x, y, z) -> 0D).column(0, 0));
|
||||
assertThrows(IllegalStateException.class,
|
||||
() -> runtime(profile(), (x, y, z) -> Double.NaN).column(0, 0));
|
||||
}
|
||||
|
||||
private static Terrain3DRuntime runtime(IrisTerrain3D profile, Terrain3DRuntime.NoiseSource noise) {
|
||||
IrisBiome biome = new IrisBiome().setTerrain3D(profile);
|
||||
return new Terrain3DRuntime(
|
||||
new Terrain3DRuntime.Sources((x, z) -> 96D, (x, z) -> biome),
|
||||
options(true), (style, seed) -> noise);
|
||||
}
|
||||
|
||||
private static Terrain3DRuntime.Options options(boolean enabled) {
|
||||
return new Terrain3DRuntime.Options(12, 256, 0, null, enabled, 4096);
|
||||
}
|
||||
|
||||
private static IrisTerrain3D profile() {
|
||||
return new IrisTerrain3D().setAmplitude(32).setHorizontalScale(64).setVerticalScale(64)
|
||||
.setMinimumSlope(0).setFluidClearance(0).setFluidFade(1);
|
||||
}
|
||||
|
||||
private static double interpolated(Terrain3DRuntime.NoiseSource noise, int x, int y, int z) {
|
||||
int startX = Math.floorDiv(x, 4) * 4;
|
||||
int startY = Math.floorDiv(y, 4) * 4;
|
||||
int startZ = Math.floorDiv(z, 4) * 4;
|
||||
double density = 0D;
|
||||
for (int dx = 0; dx <= 1; dx++) {
|
||||
for (int dy = 0; dy <= 1; dy++) {
|
||||
for (int dz = 0; dz <= 1; dz++) {
|
||||
double weightX = dx == 0 ? 1D - (x - startX) / 4D : (x - startX) / 4D;
|
||||
double weightY = dy == 0 ? 1D - (y - startY) / 4D : (y - startY) / 4D;
|
||||
double weightZ = dz == 0 ? 1D - (z - startZ) / 4D : (z - startZ) / 4D;
|
||||
density += weightX * weightY * weightZ
|
||||
* noise.sample(startX + dx * 4, startY + dy * 4, startZ + dz * 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
return density;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package art.arcane.iris.engine.terrain;
|
||||
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.util.project.stream.ProceduralStream;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.doCallRealMethod;
|
||||
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 Terrain3DSurfaceSlopeTest {
|
||||
@Test
|
||||
public void flatLowerLedgeIgnoresTheVaryingUpperCap() {
|
||||
IrisComplex complex = complex();
|
||||
when(complex.terrainColumn(0, 0)).thenReturn(column(60, 100, 120));
|
||||
when(complex.terrainColumn(3, 0)).thenReturn(column(60, 150, 200));
|
||||
when(complex.terrainColumn(0, 3)).thenReturn(column(60, 90, 100));
|
||||
|
||||
assertEquals(0D, complex.terrainSurfaceSlope(0, 60, 0), 0D);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lowerLedgeUsesTheExistingThreeBlockSlopeMetric() {
|
||||
IrisComplex complex = complex();
|
||||
when(complex.terrainColumn(0, 0)).thenReturn(column(60, 100, 120));
|
||||
when(complex.terrainColumn(3, 0)).thenReturn(column(63, 100, 120));
|
||||
when(complex.terrainColumn(0, 3)).thenReturn(column(64, 100, 120));
|
||||
|
||||
assertEquals(5D, complex.terrainSurfaceSlope(0, 60, 0), 0D);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void neighboringContinuousRiverTerrainUsesItsPlacementHeight() {
|
||||
IrisComplex complex = complex();
|
||||
when(complex.terrainColumn(0, 0)).thenReturn(column(60, 100, 120));
|
||||
when(complex.getPlacementHeightStream()).thenReturn(
|
||||
ProceduralStream.ofDouble((x, z) -> x == 3D ? 63D : 64D));
|
||||
|
||||
assertEquals(5D, complex.terrainSurfaceSlope(0, 60, 0), 0D);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void topSurfaceAndCaveInteriorsKeepTheExistingSlope() {
|
||||
IrisComplex complex = complex();
|
||||
when(complex.terrainColumn(0, 0)).thenReturn(column(60, 100, 120));
|
||||
when(complex.getSlopeStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 7.25D));
|
||||
|
||||
assertEquals(7.25D, complex.terrainSurfaceSlope(0, 120, 0), 0D);
|
||||
assertEquals(7.25D, complex.terrainSurfaceSlope(0, 50, 0), 0D);
|
||||
verify(complex, never()).terrainColumn(3, 0);
|
||||
verify(complex, never()).terrainColumn(0, 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unshapedTerrainKeepsTheExistingSlope() {
|
||||
IrisComplex complex = complex();
|
||||
when(complex.getSlopeStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 3.5D));
|
||||
|
||||
assertEquals(3.5D, complex.terrainSurfaceSlope(0, 60, 0), 0D);
|
||||
verify(complex, never()).terrainColumn(3, 0);
|
||||
verify(complex, never()).terrainColumn(0, 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nearestSurfaceUsesTheLowerFloorOnAnEqualDistance() {
|
||||
Terrain3DColumn column = column(60, 80, 100);
|
||||
|
||||
assertEquals(60, column.nearestSurfaceY(80));
|
||||
assertEquals(100, column.nearestSurfaceY(81));
|
||||
assertEquals(60, column.nearestSurfaceY(Integer.MIN_VALUE));
|
||||
assertEquals(100, column.nearestSurfaceY(Integer.MAX_VALUE));
|
||||
}
|
||||
|
||||
private static IrisComplex complex() {
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
doCallRealMethod().when(complex).terrainSurfaceSlope(anyInt(), anyInt(), anyInt());
|
||||
return complex;
|
||||
}
|
||||
|
||||
private static Terrain3DColumn column(int floor, int ceiling, int top) {
|
||||
return new Terrain3DColumn(floor, floor, true, new int[]{0, floor, ceiling, top});
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user