mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-21 15:57:38 +00:00
Terrain-first hydrology
- Replace the river package with the hydrology planner, footprint compiler, cave containment and runtime - Add the hydrology pack schema, validator, probes and tests - Wire hydrology into IrisComplex, actuators, mantle, Vision and commands
This commit is contained in:
1 parent
bdaa84975a
commit
48afa0d93f
352 files changed
+57955
-19113
No files matched your search
@@ -1,5 +1,6 @@
|
||||
package art.arcane.iris.core.lifecycle;
|
||||
|
||||
import art.arcane.iris.engine.IrisEngineMantle;
|
||||
import art.arcane.iris.core.WorldSlotKey;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.junit.Rule;
|
||||
@@ -223,8 +224,10 @@ public class BukkitWorldConfigurationTest {
|
||||
public void auditFailsClosedForMantleDataWithoutAPackSnapshot() throws Exception {
|
||||
File configuration = temporaryFolder.newFile("audit-mantle.yml");
|
||||
Path levelRoot = temporaryFolder.newFolder("audit-mantle-level-root").toPath();
|
||||
Files.createDirectories(levelRoot.resolve("dimensions/iris/mantled/mantle"));
|
||||
Files.writeString(levelRoot.resolve("dimensions/iris/mantled/mantle/0.0.ttp"), "mantle");
|
||||
Path mantleRoot = levelRoot.resolve("dimensions/iris/mantled")
|
||||
.resolve(IrisEngineMantle.STORAGE_FOLDER_NAME);
|
||||
Files.createDirectories(mantleRoot);
|
||||
Files.writeString(mantleRoot.resolve("0.0.ttp"), "mantle");
|
||||
YamlConfiguration yaml = new YamlConfiguration();
|
||||
yaml.set("worlds.world_iris_mantled.generator", "Iris:overworld");
|
||||
yaml.save(configuration);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package art.arcane.iris.core.lifecycle;
|
||||
|
||||
import art.arcane.iris.engine.IrisEngineMantle;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
@@ -83,7 +84,7 @@ public class HuskWorldQuarantineTest {
|
||||
public void aWorldWithMantleDataIsLeftForTheFailClosedGuard() throws IOException {
|
||||
Path levelRoot = level("mantle");
|
||||
Path world = dimensionRoot(levelRoot, "irisworld");
|
||||
write(world.resolve("mantle/pv.0.ttp.lz4b"));
|
||||
write(world.resolve(IrisEngineMantle.STORAGE_FOLDER_NAME).resolve("pv.0.ttp.lz4b"));
|
||||
|
||||
assertQuarantinesNothing(levelRoot, world);
|
||||
}
|
||||
|
||||
+1
-1
@@ -458,7 +458,7 @@ public class DefaultPackBootstrapProvisionerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void betaPacksUpdateIndependentlyAndRecompileOneAggregateDatapack() throws Exception {
|
||||
public void releasePacksUpdateIndependentlyAndRecompileOneAggregateDatapack() throws Exception {
|
||||
byte[] overworld = packArchive("overworld", "overworld_first");
|
||||
byte[] underworldFirst = underworldArchive("underworld_first");
|
||||
AtomicInteger requests = new AtomicInteger();
|
||||
|
||||
@@ -105,9 +105,9 @@ public class PackDownloaderTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolvesDefaultOverworldBetaRelease() {
|
||||
public void resolvesDefaultOverworldStableRelease() {
|
||||
assertEquals(
|
||||
"https://github.com/IrisDimensions/overworld/releases/download/beta/overworld.zip",
|
||||
"https://github.com/IrisDimensions/overworld/releases/download/4002/overworld.zip",
|
||||
PackDownloader.builtInPackUrl("overworld")
|
||||
);
|
||||
assertTrue(PackDownloader.isDefaultOverworld("overworld"));
|
||||
@@ -116,9 +116,9 @@ public class PackDownloaderTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolvesUnderworldBetaRelease() {
|
||||
public void resolvesUnderworldStableRelease() {
|
||||
assertEquals(
|
||||
"https://github.com/IrisDimensions/underworld/releases/download/beta/underworld.zip",
|
||||
"https://github.com/IrisDimensions/underworld/releases/download/1005/underworld.zip",
|
||||
PackDownloader.builtInPackUrl("underworld")
|
||||
);
|
||||
assertTrue(PackDownloader.isBuiltInPack("underworld"));
|
||||
@@ -659,8 +659,8 @@ public class PackDownloaderTest {
|
||||
Files.writeString(
|
||||
extracted.toPath().resolve("dimensions/overworld.json"),
|
||||
"{\"name\":\"Overworld\",\"regions\":[\"local\"],\"logicalHeight\":256,"
|
||||
+ "\"dimensionHeight\":{\"min\":-64,\"max\":320},\"rivers\":{\"enabled\":true,"
|
||||
+ "\"terrain\":{},\"water\":{\"mode\":\"SEA_LEVEL\"}}}",
|
||||
+ "\"dimensionHeight\":{\"min\":-64,\"max\":320},\"hydrology\":{\"rivers\":{"
|
||||
+ "\"enabled\":true,\"routing\":{\"tileSize\":255}}}}",
|
||||
StandardCharsets.UTF_8
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,891 @@
|
||||
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 PackHydrologyValidatorTest {
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
public void acceptsCanonicalHydrologyAndRecursivePolicyClosure() throws Exception {
|
||||
File pack = pack(canonicalDimension());
|
||||
write(pack, "regions/region.json", """
|
||||
{
|
||||
"landBiomes": ["biome"],
|
||||
"seaBiomes": [],
|
||||
"shoreBiomes": [],
|
||||
"caveBiomes": [],
|
||||
"riverPolicy": {
|
||||
"placement": "NATURAL",
|
||||
"profiles": ["default"],
|
||||
"surfaceBiomes": ["river_surface"]
|
||||
}
|
||||
}
|
||||
""");
|
||||
write(pack, "biomes/biome.json", """
|
||||
{
|
||||
"name": "Biome",
|
||||
"derivative": "minecraft:plains",
|
||||
"riverPolicy": {
|
||||
"routing": "PREFER",
|
||||
"outletAdmission": null,
|
||||
"shoreBiomes": ["river_shore"]
|
||||
}
|
||||
}
|
||||
""");
|
||||
write(pack, "biomes/river_surface.json", """
|
||||
{
|
||||
"name": "Surface",
|
||||
"derivative": "minecraft:river",
|
||||
"riverPolicy": {
|
||||
"profiles": ["default"],
|
||||
"dryBiomes": ["river_dry"]
|
||||
}
|
||||
}
|
||||
""");
|
||||
write(pack, "biomes/river_shore.json",
|
||||
"{\"name\":\"Shore\",\"derivative\":\"minecraft:river\"}");
|
||||
write(pack, "biomes/river_dry.json",
|
||||
"{\"name\":\"Dry\",\"derivative\":\"minecraft:plains\"}");
|
||||
|
||||
PackHydrologyValidator.Validation result = validate(pack);
|
||||
|
||||
assertTrue(result.errors().toString(), result.errors().isEmpty());
|
||||
assertTrue(result.warnings().toString(), result.warnings().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsRoutingGeometryHydraulicComplexityAndCapabilityFailures() throws Exception {
|
||||
File pack = pack("""
|
||||
{
|
||||
"regions": ["region"],
|
||||
"dimensionHeight": {"min": -64, "max": 128},
|
||||
"useMantle": false,
|
||||
"carvingEnabled": false,
|
||||
"disabledComponents": ["CARVED", "RIVER_HYDROLOGY"],
|
||||
"hydrology": {
|
||||
"rivers": {
|
||||
"enabled": true,
|
||||
"routing": {
|
||||
"tileSize": 8192,
|
||||
"sampleSpacing": 8,
|
||||
"refinementSpacing": 3,
|
||||
"branching": {
|
||||
"minimumSurfaceCourseLength": 32769,
|
||||
"minimumUndergroundCourseLength": 32769
|
||||
},
|
||||
"maximumRouteLength": 32768,
|
||||
"oceanOutlets": false,
|
||||
"inlandOutlets": []
|
||||
},
|
||||
"geometry": {
|
||||
"meanders": {
|
||||
"primaryWavelength": 7,
|
||||
"detailWavelength": 129,
|
||||
"maximumTurnDegrees": 151
|
||||
},
|
||||
"surface": {
|
||||
"bedRoundness": 7,
|
||||
"bedRoughness": -0.1,
|
||||
"wallRoughness": 1.1,
|
||||
"roughnessWavelength": 2
|
||||
},
|
||||
"drops": {
|
||||
"cascadeRunPerBlock": 17,
|
||||
"cascadeExponent": 0.2,
|
||||
"maximumCascadeStep": 5,
|
||||
"flowWidthRatio": 0.2,
|
||||
"maximumFlowDepth": 17,
|
||||
"basinWidthRatio": 4.1,
|
||||
"maximumBasinDepth": 33
|
||||
}
|
||||
},
|
||||
"surface": {
|
||||
"sources": {"density": 64, "minimumElevation": 128, "minimumPerTile": 64,
|
||||
"minimumSpacing": 8193},
|
||||
"channel": {
|
||||
"width": {"min": 20, "max": 2},
|
||||
"depth": {"min": 2, "max": 5},
|
||||
"surfaceInset": {"min": 0, "max": 65},
|
||||
"maximumIncision": 65,
|
||||
"shoreWidth": 3,
|
||||
"terrainBlendWidth": {"min": 3, "max": 65}
|
||||
},
|
||||
"hydraulics": {
|
||||
"targetPoolLength": {"min": 8, "max": 32},
|
||||
"riffleDrop": 5,
|
||||
"maximumGradualDrop": 3,
|
||||
"maximumGradualLength": 2,
|
||||
"waterfallMinimumDrop": 8
|
||||
},
|
||||
"ridgeTunnels": {"maximumLength": 4097, "headroom": 4},
|
||||
"mouths": {"levelingDistance": 4, "maximumOceanApron": 8}
|
||||
},
|
||||
"underground": {
|
||||
"sources": {"density": 64, "minimumPerTile": 64},
|
||||
"fluidLevel": {"min": -64, "max": 127},
|
||||
"channelWidth": {"min": 4, "max": 18},
|
||||
"depth": {"min": 2, "max": 5},
|
||||
"headroom": {"min": 6, "max": 14}
|
||||
},
|
||||
"profiles": [
|
||||
{
|
||||
"id": "default",
|
||||
"fluidPalette": {"palette": [{"block": "minecraft:water"}]}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
PackHydrologyValidator.Validation result = validate(pack);
|
||||
|
||||
assertContains(result.errors(), "sampleSpacing must be divisible by refinementSpacing");
|
||||
assertContains(result.errors(), "primaryWavelength must be at least 8");
|
||||
assertContains(result.errors(), "detailWavelength must be at most 128");
|
||||
assertContains(result.errors(), "detailWavelength must not exceed primaryWavelength");
|
||||
assertContains(result.errors(), "maximumTurnDegrees must be at most 150");
|
||||
assertContains(result.errors(), "bedRoundness must be at most 6");
|
||||
assertContains(result.errors(), "bedRoughness must be at least 0");
|
||||
assertContains(result.errors(), "wallRoughness must be at most 1");
|
||||
assertContains(result.errors(), "roughnessWavelength must be at least 3");
|
||||
assertContains(result.errors(), "minimumSurfaceCourseLength must be at most 32768");
|
||||
assertContains(result.errors(), "minimumUndergroundCourseLength must be at most 32768");
|
||||
assertContains(result.errors(), "cascadeRunPerBlock must be at most 16");
|
||||
assertContains(result.errors(), "cascadeExponent must be at least 0.25");
|
||||
assertContains(result.errors(), "maximumCascadeStep must be at most 4");
|
||||
assertContains(result.errors(), "flowWidthRatio must be at least 0.25");
|
||||
assertContains(result.errors(), "maximumFlowDepth must be at most 16");
|
||||
assertContains(result.errors(), "basinWidthRatio must be at most 4");
|
||||
assertContains(result.errors(), "maximumBasinDepth must be at most 32");
|
||||
assertContains(result.errors(), "minimumSpacing must be at most 8192");
|
||||
assertContains(result.errors(), "must enable oceanOutlets or select at least one");
|
||||
assertContains(result.errors(), "shoreWidth must be at most 2");
|
||||
assertContains(result.errors(), "terrainBlendWidth.min must be at least 4");
|
||||
assertContains(result.errors(), "terrainBlendWidth.max must be at most 64");
|
||||
assertContains(result.errors(), "width.min must not exceed max");
|
||||
assertContains(result.errors(), "surfaceInset.min must be at least 1");
|
||||
assertContains(result.errors(), "surfaceInset.max must be at most 64");
|
||||
assertContains(result.errors(), "maximumIncision must be at most 64");
|
||||
assertContains(result.errors(), "riffleDrop must not exceed maximumGradualDrop");
|
||||
assertContains(result.errors(), "waterfallMinimumDrop must equal maximumGradualDrop plus one");
|
||||
assertContains(result.errors(), "maximumGradualLength must be at least maximumGradualDrop");
|
||||
assertContains(result.errors(), "maximumLength must be at most 4096");
|
||||
assertContains(result.errors(), "coarse lattice nodes");
|
||||
assertContains(result.errors(), "route samples per tile");
|
||||
assertContains(result.errors(), "requires useMantle to be true");
|
||||
assertContains(result.errors(), "requires carvingEnabled to be true");
|
||||
assertContains(result.errors(), "requires CARVED to remain enabled");
|
||||
assertContains(result.errors(), "requires RIVER_HYDROLOGY to remain enabled");
|
||||
assertContains(result.errors(), "minimumElevation must be below dimensionHeight.max");
|
||||
assertContains(result.errors(), "fluidLevel.min and depth.max");
|
||||
assertContains(result.errors(), "fluidLevel.max and headroom.max");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsInvalidProfilesPalettesAndPolicyProfileReferences() throws Exception {
|
||||
File pack = pack("""
|
||||
{
|
||||
"regions": ["region"],
|
||||
"hydrology": {
|
||||
"rivers": {
|
||||
"enabled": true,
|
||||
"profiles": [
|
||||
{
|
||||
"id": "water",
|
||||
"fluidPalette": {"palette": [{"block": "minecraft:water"}]}
|
||||
},
|
||||
{
|
||||
"id": "water",
|
||||
"fluidPalette": {"palette": []}
|
||||
},
|
||||
{"id": "Bad Profile"},
|
||||
{
|
||||
"id": "stone",
|
||||
"fluidPalette": {"palette": [{"block": "minecraft:stone"}]}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"riverPolicy": {
|
||||
"placement": "REQUIRED_HEADWATER",
|
||||
"routing": "ALLOW",
|
||||
"outletAdmission": true,
|
||||
"profiles": ["missing"]
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
PackHydrologyValidator.Validation result = validate(pack);
|
||||
|
||||
assertContains(result.errors(), "id must be unique inside hydrology.rivers.profiles");
|
||||
assertContains(result.errors(), "fluidPalette.palette must contain at least one fluid block");
|
||||
assertContains(result.errors(), "id must use 1 to 64 lowercase");
|
||||
assertContains(result.errors(), "fluidPalette must be configured");
|
||||
assertContains(result.errors(), "may contain only fluid blocks");
|
||||
assertContains(result.errors(), "references unknown river profile 'missing'");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validatesAllCurrentPolicyFieldsAndBiomeExistence() throws Exception {
|
||||
File pack = pack("""
|
||||
{
|
||||
"regions": ["region"],
|
||||
"hydrology": {
|
||||
"rivers": {
|
||||
"enabled": true,
|
||||
"profiles": [
|
||||
{
|
||||
"id": "default",
|
||||
"fluidPalette": {"palette": [{"block": "minecraft:water"}]}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
""");
|
||||
write(pack, "regions/region.json", """
|
||||
{
|
||||
"landBiomes": ["biome"],
|
||||
"riverPolicy": {
|
||||
"placement": "SOURCE",
|
||||
"routing": "CHEAP",
|
||||
"outletAdmission": "yes",
|
||||
"profiles": "default",
|
||||
"surfaceBiomes": ["missing"],
|
||||
"mouthBiomes": "river",
|
||||
"shoreBiomes": ["biome", "biome"],
|
||||
"dryBiomes": [17],
|
||||
"floodedCaveBiomes": ["../escape"],
|
||||
"widthMultiplier": 0,
|
||||
"depthMultiplier": 17,
|
||||
"incisionMultiplier": -1,
|
||||
"routingMultiplier": -1
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
PackHydrologyValidator.Validation result = validate(pack);
|
||||
|
||||
assertContains(result.errors(), ".placement must be one of");
|
||||
assertContains(result.errors(), ".routing must be one of");
|
||||
assertContains(result.errors(), ".outletAdmission must be a boolean or null");
|
||||
assertContains(result.errors(), ".profiles must be an array or null");
|
||||
assertContains(result.errors(), "surfaceBiomes[0] references missing biome 'missing'");
|
||||
assertContains(result.errors(), ".mouthBiomes must be an array or null");
|
||||
assertContains(result.errors(), "shoreBiomes[1] duplicates biome 'biome'");
|
||||
assertContains(result.errors(), "dryBiomes[0] must name a biome resource");
|
||||
assertContains(result.errors(), "floodedCaveBiomes[0] must name a biome resource");
|
||||
assertContains(result.errors(), "widthMultiplier must be at least");
|
||||
assertContains(result.errors(), "depthMultiplier must be at most");
|
||||
assertContains(result.errors(), "incisionMultiplier must be at least");
|
||||
assertContains(result.errors(), "routingMultiplier must be at least");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validatesProfileReferencesReachedOnlyThroughAnotherPolicy() throws Exception {
|
||||
File pack = pack("""
|
||||
{
|
||||
"regions": ["region"],
|
||||
"hydrology": {
|
||||
"rivers": {
|
||||
"enabled": true,
|
||||
"profiles": [
|
||||
{
|
||||
"id": "default",
|
||||
"fluidPalette": {"palette": [{"block": "minecraft:water"}]}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
""");
|
||||
write(pack, "biomes/biome.json", """
|
||||
{
|
||||
"name": "Root",
|
||||
"derivative": "minecraft:plains",
|
||||
"riverPolicy": {"surfaceBiomes": ["indirect"]}
|
||||
}
|
||||
""");
|
||||
write(pack, "biomes/indirect.json", """
|
||||
{
|
||||
"name": "Indirect",
|
||||
"derivative": "minecraft:river",
|
||||
"riverPolicy": {"profiles": ["not_in_dimension"]}
|
||||
}
|
||||
""");
|
||||
|
||||
PackHydrologyValidator.Validation result = validate(pack);
|
||||
|
||||
assertContains(
|
||||
result.errors(),
|
||||
"Biome 'indirect'.riverPolicy.profiles[0] references unknown river profile 'not_in_dimension'"
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validatesProfileReferencesReachedOnlyThroughBiomeImageMapTargets() throws Exception {
|
||||
File pack = pack("""
|
||||
{
|
||||
"regions": ["region"],
|
||||
"imageMaps": [
|
||||
{"key": "biome", "map": "policy-map", "application": "BIOME"}
|
||||
],
|
||||
"hydrology": {
|
||||
"rivers": {
|
||||
"enabled": true,
|
||||
"profiles": [
|
||||
{
|
||||
"id": "default",
|
||||
"fluidPalette": {"palette": [{"block": "minecraft:water"}]}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
""");
|
||||
write(pack, "image-maps/policy-map.json", """
|
||||
{
|
||||
"colors": {"#123456": "iris:mapped_color"},
|
||||
"fallbackTarget": "mapped_fallback"
|
||||
}
|
||||
""");
|
||||
write(pack, "biomes/mapped_color.json", """
|
||||
{
|
||||
"name": "Mapped Color",
|
||||
"derivative": "minecraft:river",
|
||||
"riverPolicy": {"profiles": ["missing_color_profile"]}
|
||||
}
|
||||
""");
|
||||
write(pack, "biomes/mapped_fallback.json", """
|
||||
{
|
||||
"name": "Mapped Fallback",
|
||||
"derivative": "minecraft:river",
|
||||
"riverPolicy": {"profiles": ["missing_fallback_profile"]}
|
||||
}
|
||||
""");
|
||||
|
||||
PackHydrologyValidator.Validation result = validate(pack);
|
||||
|
||||
assertContains(
|
||||
result.errors(),
|
||||
"Biome 'mapped_color'.riverPolicy.profiles[0] references unknown river profile 'missing_color_profile'"
|
||||
);
|
||||
assertContains(
|
||||
result.errors(),
|
||||
"Biome 'mapped_fallback'.riverPolicy.profiles[0] references unknown river profile 'missing_fallback_profile'"
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsUnsafeDeepFluidConfiguration() throws Exception {
|
||||
File pack = pack("""
|
||||
{
|
||||
"regions": ["region"],
|
||||
"dimensionHeight": {"min": -64, "max": 128},
|
||||
"hydrology": {
|
||||
"deepFluids": [
|
||||
{
|
||||
"id": "deep_lava",
|
||||
"fluidPalette": {"palette": [{"block": "minecraft:lava"}]},
|
||||
"height": {"min": 40, "max": -40},
|
||||
"density": 1,
|
||||
"spacing": 16,
|
||||
"horizontalRadius": 20,
|
||||
"verticalRadius": 3,
|
||||
"channelWidth": 8,
|
||||
"depth": 4,
|
||||
"headroom": 4,
|
||||
"containedPools": "yes",
|
||||
"shortChannels": 1
|
||||
},
|
||||
{
|
||||
"id": "deep_lava",
|
||||
"fluidPalette": {"palette": []},
|
||||
"height": {"min": -63, "max": 127}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
PackHydrologyValidator.Validation result = validate(pack);
|
||||
|
||||
assertContains(result.errors(), "height.min must not exceed max");
|
||||
assertContains(result.errors(), "spacing must be at least");
|
||||
assertContains(result.errors(), "depth and headroom must fit inside verticalRadius");
|
||||
assertContains(result.errors(), "containedPools must be a boolean");
|
||||
assertContains(result.errors(), "shortChannels must be a boolean");
|
||||
assertContains(result.errors(), "id must be unique inside hydrology.deepFluids");
|
||||
assertContains(result.errors(), "fluidPalette.palette must contain at least one fluid block");
|
||||
assertContains(result.errors(), "height.min and lower footprint envelope must remain above dimensionHeight.min");
|
||||
assertContains(result.errors(), "height.max and headroom must remain below dimensionHeight.max");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsDeepPoolWhoseVerticalRadiusCrossesTheDimensionFloor() throws Exception {
|
||||
File pack = pack("""
|
||||
{
|
||||
"regions": ["region"],
|
||||
"dimensionHeight": {"min": -64, "max": 128},
|
||||
"hydrology": {
|
||||
"deepFluids": [
|
||||
{
|
||||
"id": "deep_lava",
|
||||
"fluidPalette": {"palette": [{"block": "minecraft:lava"}]},
|
||||
"height": {"min": -60, "max": -40},
|
||||
"density": 1,
|
||||
"spacing": 64,
|
||||
"horizontalRadius": 8,
|
||||
"verticalRadius": 8,
|
||||
"channelWidth": 4,
|
||||
"depth": 2,
|
||||
"headroom": 4,
|
||||
"containedPools": true,
|
||||
"shortChannels": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
PackHydrologyValidator.Validation result = validate(pack);
|
||||
|
||||
assertContains(
|
||||
result.errors(),
|
||||
"height.min and lower footprint envelope must remain above dimensionHeight.min"
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsFluidIdsSharedByRiverAndDeepProfiles() throws Exception {
|
||||
File pack = pack("""
|
||||
{
|
||||
"regions": ["region"],
|
||||
"dimensionHeight": {"min": -256, "max": 512},
|
||||
"hydrology": {
|
||||
"rivers": {
|
||||
"enabled": true,
|
||||
"profiles": [
|
||||
{
|
||||
"id": "shared",
|
||||
"fluidPalette": {"palette": [{"block": "minecraft:water"}]}
|
||||
}
|
||||
]
|
||||
},
|
||||
"deepFluids": [
|
||||
{
|
||||
"id": "shared",
|
||||
"fluidPalette": {"palette": [{"block": "minecraft:lava"}]},
|
||||
"height": {"min": -192, "max": 32},
|
||||
"density": 0.1,
|
||||
"spacing": 1024,
|
||||
"horizontalRadius": 18,
|
||||
"verticalRadius": 8,
|
||||
"channelWidth": 4,
|
||||
"depth": 2,
|
||||
"headroom": 4,
|
||||
"containedPools": true,
|
||||
"shortChannels": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
PackHydrologyValidator.Validation result = validate(pack);
|
||||
|
||||
assertContains(result.errors(), "must not duplicate a hydrology.rivers profile id");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsDeepOnlyIdSharedByTheImplicitDefaultRiverProfile() throws Exception {
|
||||
File pack = pack("""
|
||||
{
|
||||
"regions": ["region"],
|
||||
"dimensionHeight": {"min": -256, "max": 512},
|
||||
"hydrology": {
|
||||
"deepFluids": [
|
||||
{
|
||||
"id": "default",
|
||||
"fluidPalette": {"palette": [{"block": "minecraft:lava"}]},
|
||||
"density": 0.5
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
PackHydrologyValidator.Validation result = validate(pack);
|
||||
|
||||
assertContains(result.errors(), "must not duplicate a hydrology.rivers profile id");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void omittedDeepGeometryUsesRuntimeDefaults() throws Exception {
|
||||
File pack = pack("""
|
||||
{
|
||||
"regions": ["region"],
|
||||
"dimensionHeight": {"min": -256, "max": 512},
|
||||
"hydrology": {
|
||||
"deepFluids": [
|
||||
{
|
||||
"id": "deep_lava",
|
||||
"fluidPalette": {"palette": [{"block": "minecraft:lava"}]},
|
||||
"density": 0.5
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
PackHydrologyValidator.Validation result = validate(pack);
|
||||
|
||||
assertTrue(result.errors().toString(), result.errors().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void omittedGrottoGeometryUsesRuntimeDefaults() throws Exception {
|
||||
File pack = pack("""
|
||||
{
|
||||
"regions": ["region"],
|
||||
"dimensionHeight": {"min": -256, "max": 512},
|
||||
"hydrology": {
|
||||
"rivers": {
|
||||
"enabled": true,
|
||||
"grottos": {
|
||||
"coastal": {"verticalRadius": 6},
|
||||
"inland": {"verticalRadius": 6}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
PackHydrologyValidator.Validation result = validate(pack);
|
||||
|
||||
assertTrue(result.errors().toString(), result.errors().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validatesSurfaceSinkholeConfigurationAsACanonicalInlandOutlet() throws Exception {
|
||||
File missingOutlet = pack("""
|
||||
{
|
||||
"regions": ["region"],
|
||||
"hydrology": {
|
||||
"rivers": {
|
||||
"grottos": {
|
||||
"inland": {
|
||||
"enabled": false,
|
||||
"connectSurfaceRivers": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""");
|
||||
File invalidType = pack("""
|
||||
{
|
||||
"regions": ["region"],
|
||||
"hydrology": {
|
||||
"rivers": {
|
||||
"grottos": {
|
||||
"inland": {
|
||||
"enabled": true,
|
||||
"connectSurfaceRivers": "yes"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
PackHydrologyValidator.Validation missingOutletResult = validate(missingOutlet);
|
||||
PackHydrologyValidator.Validation invalidTypeResult = validate(invalidType);
|
||||
|
||||
assertContains(missingOutletResult.errors(), "connectSurfaceRivers requires the inland grotto to be enabled");
|
||||
assertContains(missingOutletResult.errors(), "requires routing.inlandOutlets to select SINKHOLE_GROTTO");
|
||||
assertContains(invalidTypeResult.errors(), "connectSurfaceRivers must be a boolean");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shapeDisabledDeepEntryDoesNotRequireMantleCapabilities() throws Exception {
|
||||
File pack = pack("""
|
||||
{
|
||||
"regions": ["region"],
|
||||
"dimensionHeight": {"min": -256, "max": 512},
|
||||
"useMantle": false,
|
||||
"carvingEnabled": false,
|
||||
"disabledComponents": ["CARVED", "RIVER_HYDROLOGY"],
|
||||
"hydrology": {
|
||||
"deepFluids": [
|
||||
{
|
||||
"id": "deep_lava",
|
||||
"fluidPalette": {"palette": [{"block": "minecraft:lava"}]},
|
||||
"density": 1,
|
||||
"containedPools": false,
|
||||
"shortChannels": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
PackHydrologyValidator.Validation result = validate(pack);
|
||||
|
||||
assertTrue(result.errors().toString(), result.errors().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsEveryReservedHydrologyFeatureKeywordAsADeepFluidId() throws Exception {
|
||||
for (String id : List.of(
|
||||
"surface",
|
||||
"waterfall",
|
||||
"sinkhole",
|
||||
"underground",
|
||||
"grotto",
|
||||
"coastal_grotto",
|
||||
"coastal-grotto",
|
||||
"inland_grotto",
|
||||
"inland-grotto",
|
||||
"mouth",
|
||||
"ridge_tunnel",
|
||||
"ridge-tunnel",
|
||||
"deep"
|
||||
)) {
|
||||
File pack = pack("""
|
||||
{
|
||||
"regions": ["region"],
|
||||
"dimensionHeight": {"min": -256, "max": 512},
|
||||
"hydrology": {
|
||||
"deepFluids": [
|
||||
{
|
||||
"id": "%s",
|
||||
"fluidPalette": {"palette": [{"block": "minecraft:lava"}]},
|
||||
"height": {"min": -192, "max": 32},
|
||||
"density": 0.1,
|
||||
"spacing": 1024,
|
||||
"horizontalRadius": 18,
|
||||
"verticalRadius": 8,
|
||||
"channelWidth": 4,
|
||||
"depth": 2,
|
||||
"headroom": 4,
|
||||
"containedPools": true,
|
||||
"shortChannels": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""".formatted(id));
|
||||
|
||||
PackHydrologyValidator.Validation result = validate(pack);
|
||||
|
||||
assertContains(result.errors(), "id must not use a reserved hydrology feature keyword");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void packValidatorRegistersCanonicalHydrologyValidator() throws Exception {
|
||||
File pack = pack("""
|
||||
{
|
||||
"regions": ["region"],
|
||||
"logicalHeight": 256,
|
||||
"hydrology": {
|
||||
"rivers": {
|
||||
"enabled": true,
|
||||
"routing": {
|
||||
"tileSize": 2048,
|
||||
"sampleSpacing": 63,
|
||||
"refinementSpacing": 8
|
||||
},
|
||||
"profiles": [
|
||||
{
|
||||
"id": "default",
|
||||
"fluidPalette": {"palette": [{"block": "minecraft:water"}]}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
PackValidationResult result = PackValidator.validateForPackaging(pack);
|
||||
|
||||
assertFalse(result.getBlockingErrors().toString(), result.isLoadable());
|
||||
assertContains(result.getBlockingErrors(), "tileSize must be divisible by sampleSpacing");
|
||||
}
|
||||
|
||||
private PackHydrologyValidator.Validation validate(File pack) {
|
||||
File[] dimensions = new File(pack, "dimensions").listFiles(
|
||||
file -> file.isFile() && file.getName().endsWith(".json"));
|
||||
return PackHydrologyValidator.validate(pack, dimensions);
|
||||
}
|
||||
|
||||
private File pack(String dimensionJson) throws Exception {
|
||||
File pack = temporaryFolder.newFolder("pack-" + System.nanoTime());
|
||||
write(pack, "dimensions/main.json", dimensionJson);
|
||||
write(pack, "regions/region.json", """
|
||||
{
|
||||
"landBiomes": ["biome"],
|
||||
"seaBiomes": [],
|
||||
"shoreBiomes": [],
|
||||
"caveBiomes": []
|
||||
}
|
||||
""");
|
||||
write(pack, "biomes/biome.json",
|
||||
"{\"name\":\"Biome\",\"derivative\":\"minecraft:plains\"}");
|
||||
return pack;
|
||||
}
|
||||
|
||||
private String canonicalDimension() {
|
||||
return """
|
||||
{
|
||||
"regions": ["region"],
|
||||
"dimensionHeight": {"min": -256, "max": 512},
|
||||
"hydrology": {
|
||||
"rivers": {
|
||||
"enabled": true,
|
||||
"routing": {
|
||||
"tileSize": 2048,
|
||||
"sampleSpacing": 64,
|
||||
"refinementSpacing": 8,
|
||||
"maximumRouteLength": 8192,
|
||||
"oceanOutlets": true,
|
||||
"inlandOutlets": ["SINKHOLE_GROTTO"]
|
||||
},
|
||||
"surface": {
|
||||
"enabled": true,
|
||||
"sources": {
|
||||
"density": 0.5,
|
||||
"minimumElevation": 88,
|
||||
"minimumPerTile": 1
|
||||
},
|
||||
"channel": {
|
||||
"width": {"min": 4, "max": 32, "style": {"style": "STATIC"}},
|
||||
"depth": {"min": 2, "max": 5},
|
||||
"maximumIncision": 12,
|
||||
"shoreWidth": 1.5,
|
||||
"terrainBlendWidth": {"min": 4, "max": 10}
|
||||
},
|
||||
"hydraulics": {
|
||||
"targetPoolLength": {"min": 48, "max": 160},
|
||||
"riffleDrop": 1,
|
||||
"maximumGradualDrop": 3,
|
||||
"maximumGradualLength": 10,
|
||||
"waterfallMinimumDrop": 4
|
||||
},
|
||||
"ridgeTunnels": {
|
||||
"enabled": true,
|
||||
"maximumLength": 192,
|
||||
"headroom": 10
|
||||
},
|
||||
"mouths": {
|
||||
"levelingDistance": 64,
|
||||
"maximumOceanApron": 2
|
||||
}
|
||||
},
|
||||
"underground": {
|
||||
"enabled": true,
|
||||
"sources": {"density": 0.5, "minimumPerTile": 1},
|
||||
"fluidLevel": {"min": -48, "max": 50},
|
||||
"channelWidth": {"min": 4, "max": 18},
|
||||
"depth": {"min": 2, "max": 5},
|
||||
"headroom": {"min": 6, "max": 14},
|
||||
"connectToExistingCaves": true
|
||||
},
|
||||
"grottos": {
|
||||
"coastal": {
|
||||
"enabled": true,
|
||||
"poolLevel": "SEA_LEVEL",
|
||||
"horizontalRadius": 12,
|
||||
"verticalRadius": 7,
|
||||
"headroom": 4,
|
||||
"maximumVolume": 8192
|
||||
},
|
||||
"inland": {
|
||||
"enabled": true,
|
||||
"connectSurfaceRivers": true,
|
||||
"horizontalRadius": 10,
|
||||
"verticalRadius": 6,
|
||||
"headroom": 4,
|
||||
"maximumVolume": 8192
|
||||
}
|
||||
},
|
||||
"profiles": [
|
||||
{
|
||||
"id": "default",
|
||||
"fluidPalette": {
|
||||
"palette": [{"block": "minecraft:water", "weight": 1}]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"deepFluids": [
|
||||
{
|
||||
"id": "deep_lava",
|
||||
"fluidPalette": {
|
||||
"palette": [{"block": "minecraft:lava"}]
|
||||
},
|
||||
"height": {"min": -192, "max": 32},
|
||||
"density": 0.5,
|
||||
"spacing": 384,
|
||||
"horizontalRadius": 18,
|
||||
"verticalRadius": 8,
|
||||
"channelWidth": 4,
|
||||
"depth": 2,
|
||||
"headroom": 4,
|
||||
"containedPools": true,
|
||||
"shortChannels": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"riverPolicy": {
|
||||
"placement": "NATURAL",
|
||||
"routing": "ALLOW",
|
||||
"outletAdmission": true,
|
||||
"profiles": ["default"],
|
||||
"surfaceBiomes": ["river_surface"],
|
||||
"mouthBiomes": ["river_surface"],
|
||||
"shoreBiomes": ["river_shore"],
|
||||
"dryBiomes": ["river_dry"],
|
||||
"floodedCaveBiomes": ["river_dry"],
|
||||
"widthMultiplier": 1,
|
||||
"depthMultiplier": 1,
|
||||
"incisionMultiplier": 1,
|
||||
"routingMultiplier": 1
|
||||
}
|
||||
}
|
||||
""";
|
||||
}
|
||||
|
||||
private void assertContains(List<String> messages, String fragment) {
|
||||
assertTrue(messages.toString(), contains(messages, fragment));
|
||||
}
|
||||
|
||||
private boolean contains(List<String> messages, String fragment) {
|
||||
for (String message : messages) {
|
||||
if (message.contains(fragment)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void write(File root, String relative, String content) throws Exception {
|
||||
Path target = new File(root, relative).toPath();
|
||||
Files.createDirectories(target.getParent());
|
||||
Files.writeString(target, content, StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
+74
@@ -0,0 +1,74 @@
|
||||
package art.arcane.iris.core.pregenerator.methods;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class PregenMantleCleanupContractTest {
|
||||
@Test
|
||||
public void normalEngineCleanupRetriesCandidatesCoveredByTheNewestRealChunk() throws IOException {
|
||||
String source = read("art/arcane/iris/engine/framework/Engine.java");
|
||||
String cleanup = method(source, "default void cleanupMantleChunk(int x, int z)");
|
||||
|
||||
assertTrue(cleanup.contains("cleanupChunksCoveredBy("));
|
||||
assertTrue(cleanup.contains("EngineMantle.ChunkCleanupCallback.NONE"));
|
||||
assertFalse(cleanup.contains(".cleanupChunk(x, z)"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void asyncPregenReportsOnlyCleanupCallbacksFromCoveredTargets() throws IOException {
|
||||
String source = read("art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java");
|
||||
String completion = method(source, "private void completeChunk(");
|
||||
String cleanup = method(source, "private void cleanupMantleChunksCoveredBy(");
|
||||
|
||||
assertTrue(completion.contains("cleanupMantleChunksCoveredBy(x, z, listener)"));
|
||||
assertFalse(completion.contains("listener.onChunkCleaned"));
|
||||
assertTrue(cleanup.contains("cleanupChunksCoveredBy(x, z, true, listener::onChunkCleaned)"));
|
||||
assertFalse(cleanup.contains("forceCleanupChunk"));
|
||||
}
|
||||
|
||||
private static String read(String relativePath) throws IOException {
|
||||
Path current;
|
||||
try {
|
||||
current = Path.of(PregenMantleCleanupContractTest.class.getProtectionDomain()
|
||||
.getCodeSource()
|
||||
.getLocation()
|
||||
.toURI());
|
||||
} catch (URISyntaxException e) {
|
||||
throw new IOException("Unable to resolve the core source root", e);
|
||||
}
|
||||
while (current != null && !Files.isDirectory(current.resolve("src/main/java"))) {
|
||||
current = current.getParent();
|
||||
}
|
||||
if (current == null) {
|
||||
throw new IOException("Unable to locate the core source root");
|
||||
}
|
||||
return Files.readString(current.resolve("src/main/java").resolve(relativePath));
|
||||
}
|
||||
|
||||
private static String method(String source, String signature) {
|
||||
int start = source.indexOf(signature);
|
||||
assertTrue("Missing source contract signature: " + signature, start >= 0);
|
||||
int openBrace = source.indexOf('{', start);
|
||||
assertTrue("Missing source contract method body: " + signature, openBrace >= 0);
|
||||
int depth = 0;
|
||||
for (int index = openBrace; index < source.length(); index++) {
|
||||
char current = source.charAt(index);
|
||||
if (current == '{') {
|
||||
depth++;
|
||||
} else if (current == '}') {
|
||||
depth--;
|
||||
if (depth == 0) {
|
||||
return source.substring(start, index + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Unclosed source contract method: " + signature);
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,8 @@ import art.arcane.iris.core.loader.ResourceLoader;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBlockData;
|
||||
import art.arcane.iris.engine.object.IrisExpression;
|
||||
import art.arcane.iris.engine.object.IrisRiverNetwork;
|
||||
import art.arcane.iris.engine.object.IrisRiverOverride;
|
||||
import art.arcane.iris.engine.object.IrisHydrology;
|
||||
import art.arcane.iris.engine.object.IrisRiverPolicy;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.json.JSONArray;
|
||||
@@ -53,91 +53,134 @@ public class IrisRiverSchemaTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void riverNetworkSchemaExposesNestedNoiseLimitsModesAndBiomePools() {
|
||||
JSONObject schema = new SchemaBuilder(IrisRiverNetwork.class, schemaData()).construct();
|
||||
public void hydrologySchemaExposesTypedPhysicalHierarchy() {
|
||||
JSONObject schema = new SchemaBuilder(IrisHydrology.class, schemaData()).construct();
|
||||
JSONObject definitions = schema.getJSONObject("definitions");
|
||||
JSONObject properties = schema.getJSONObject("properties");
|
||||
JSONObject topology = referencedProperties(definitions, properties.getJSONObject("topology"));
|
||||
JSONObject source = referencedProperties(definitions, topology.getJSONObject("source"));
|
||||
JSONObject terrainDefinition = referencedDefinition(definitions, properties.getJSONObject("terrain"));
|
||||
JSONObject terrain = terrainDefinition.getJSONObject("properties");
|
||||
JSONObject worms = terrain.getJSONObject("worms");
|
||||
JSONObject worm = referencedProperties(definitions, worms.getJSONObject("items"));
|
||||
JSONObject water = referencedProperties(definitions, properties.getJSONObject("water"));
|
||||
JSONObject biomes = referencedProperties(definitions, properties.getJSONObject("biomes"));
|
||||
JSONObject caves = referencedProperties(definitions, properties.getJSONObject("caves"));
|
||||
JSONObject deepPools = referencedProperties(definitions, caves.getJSONObject("deepPools"));
|
||||
JSONObject rivers = referencedProperties(definitions, properties.getJSONObject("rivers"));
|
||||
JSONObject routing = referencedProperties(definitions, rivers.getJSONObject("routing"));
|
||||
JSONObject branching = referencedProperties(definitions, routing.getJSONObject("branching"));
|
||||
JSONObject geometry = referencedProperties(definitions, rivers.getJSONObject("geometry"));
|
||||
JSONObject drops = referencedProperties(definitions, geometry.getJSONObject("drops"));
|
||||
JSONObject surface = referencedProperties(definitions, rivers.getJSONObject("surface"));
|
||||
JSONObject sources = referencedProperties(definitions, surface.getJSONObject("sources"));
|
||||
JSONObject channel = referencedProperties(definitions, surface.getJSONObject("channel"));
|
||||
JSONObject hydraulics = referencedProperties(definitions, surface.getJSONObject("hydraulics"));
|
||||
JSONObject ridgeTunnels = referencedProperties(definitions, surface.getJSONObject("ridgeTunnels"));
|
||||
JSONObject mouths = referencedProperties(definitions, surface.getJSONObject("mouths"));
|
||||
JSONObject underground = referencedProperties(definitions, rivers.getJSONObject("underground"));
|
||||
JSONObject undergroundSources = referencedProperties(definitions, underground.getJSONObject("sources"));
|
||||
JSONObject grottos = referencedProperties(definitions, rivers.getJSONObject("grottos"));
|
||||
JSONObject coastal = referencedProperties(definitions, grottos.getJSONObject("coastal"));
|
||||
JSONObject inland = referencedProperties(definitions, grottos.getJSONObject("inland"));
|
||||
JSONObject profiles = rivers.getJSONObject("profiles");
|
||||
JSONObject profile = referencedProperties(definitions, profiles.getJSONObject("items"));
|
||||
JSONObject deepFluids = properties.getJSONObject("deepFluids");
|
||||
JSONObject deepFluid = referencedProperties(definitions, deepFluids.getJSONObject("items"));
|
||||
|
||||
assertEquals("boolean", properties.getJSONObject("enabled").getString("type"));
|
||||
assertEquals(64, topology.getJSONObject("cellSize").getInt("minimum"));
|
||||
assertEquals(4096, topology.getJSONObject("cellSize").getInt("maximum"));
|
||||
assertEquals(7, topology.getJSONObject("sinkSearchReaches").getInt("maximum"));
|
||||
assertEquals(8, topology.getJSONObject("routingBasinCells").getInt("minimum"));
|
||||
assertEquals(256, topology.getJSONObject("routingBasinCells").getInt("maximum"));
|
||||
assertEquals(0D, source.getJSONObject("chance").getDouble("minimum"), 0D);
|
||||
assertEquals(1D, source.getJSONObject("chance").getDouble("maximum"), 0D);
|
||||
assertEquals(1D, terrain.getJSONObject("maxChannelWidth").getDouble("minimum"), 0D);
|
||||
assertEquals(2048D, terrain.getJSONObject("maxChannelWidth").getDouble("maximum"), 0D);
|
||||
assertEquals(0D, terrain.getJSONObject("maxBankWidth").getDouble("minimum"), 0D);
|
||||
assertEquals(512D, terrain.getJSONObject("maxDepth").getDouble("maximum"), 0D);
|
||||
assertTrue(arrayContains(terrainDefinition.getJSONArray("required"), "worms"));
|
||||
assertEquals("array", worms.getString("type"));
|
||||
assertEquals(1, worms.getInt("minItems"));
|
||||
assertEquals(0.000001D, worm.getJSONObject("weight").getDouble("minimum"), 0D);
|
||||
assertEquals(16384D, worm.getJSONObject("wavelength").getDouble("maximum"), 0D);
|
||||
assertEquals(1D, worm.getJSONObject("tortuosity").getDouble("maximum"), 0D);
|
||||
assertEquals(1024D, worm.getJSONObject("maxOffset").getDouble("maximum"), 0D);
|
||||
assertEquals(64, worm.getJSONObject("segments").getInt("maximum"));
|
||||
assertEquals(0.125D, worm.getJSONObject("widthMultiplier").getDouble("minimum"), 0D);
|
||||
assertEquals(8D, worm.getJSONObject("bankMultiplier").getDouble("maximum"), 0D);
|
||||
assertEquals(8D, worm.getJSONObject("depthMultiplier").getDouble("maximum"), 0D);
|
||||
assertEquals(8D, worm.getJSONObject("bodyWavelength").getDouble("minimum"), 0D);
|
||||
assertEquals(16384D, worm.getJSONObject("bodyDetailWavelength").getDouble("maximum"), 0D);
|
||||
assertEquals(1D, worm.getJSONObject("bodyDetailInfluence").getDouble("maximum"), 0D);
|
||||
assertEquals(0.875D, worm.getJSONObject("widthVariation").getDouble("maximum"), 0D);
|
||||
assertEquals(0.875D, worm.getJSONObject("bankVariation").getDouble("maximum"), 0D);
|
||||
assertEquals(0.875D, worm.getJSONObject("depthVariation").getDouble("maximum"), 0D);
|
||||
assertEquals(0.875D, worm.getJSONObject("roofVariation").getDouble("maximum"), 0D);
|
||||
assertEquals(8, worm.getJSONObject("branchCap").getInt("maximum"));
|
||||
assertEquals(1D, worm.getJSONObject("branchDecay").getDouble("maximum"), 0D);
|
||||
assertEquals(8D, worm.getJSONObject("confluenceMultiplier").getDouble("maximum"), 0D);
|
||||
assertEquals(1D, worm.getJSONObject("childChance").getDouble("maximum"), 0D);
|
||||
assertEquals(1D, worm.getJSONObject("branchChildChance").getDouble("maximum"), 0D);
|
||||
assertEquals("array", worm.getJSONObject("children").getString("type"));
|
||||
assertEquals(List.of("FIXED", "TERRACED"), enumValues(definitions, water.getJSONObject("mode")));
|
||||
assertEquals(-2048, water.getJSONObject("fluidHeight").getInt("minimum"));
|
||||
assertEquals(2048, water.getJSONObject("fluidHeight").getInt("maximum"));
|
||||
assertTrue(water.has("fluidPalette"));
|
||||
assertEquals(64D, terrain.getJSONObject("channelRadiusBonus").getDouble("maximum"), 0D);
|
||||
assertEquals("array", biomes.getJSONObject("channel").getString("type"));
|
||||
assertEquals("#/definitions/erzbiomes",
|
||||
biomes.getJSONObject("channel").getJSONObject("items").getString("$ref"));
|
||||
assertTrue(properties.has("terrain"));
|
||||
assertTrue(properties.has("caves"));
|
||||
assertEquals("boolean", deepPools.getJSONObject("enabled").getString("type"));
|
||||
assertEquals(-2048, deepPools.getJSONObject("minimumFluidY").getInt("minimum"));
|
||||
assertEquals(2048, deepPools.getJSONObject("maximumFluidY").getInt("maximum"));
|
||||
assertEquals(128, deepPools.getJSONObject("horizontalRadius").getInt("maximum"));
|
||||
assertEquals(64, deepPools.getJSONObject("verticalRadius").getInt("maximum"));
|
||||
assertEquals(0.75D, deepPools.getJSONObject("shapeVariation").getDouble("maximum"), 0D);
|
||||
assertEquals(64D, deepPools.getJSONObject("warpStrength").getDouble("maximum"), 0D);
|
||||
assertTrue(deepPools.has("reach"));
|
||||
assertTrue(deepPools.has("shapeStyle"));
|
||||
assertTrue(deepPools.has("warpStyle"));
|
||||
assertTrue(deepPools.has("fluidPalette"));
|
||||
assertEquals(8192, routing.getJSONObject("tileSize").getInt("maximum"));
|
||||
assertEquals(64, routing.getJSONObject("refinementSpacing").getInt("maximum"));
|
||||
assertEquals(32768, routing.getJSONObject("maximumRouteLength").getInt("maximum"));
|
||||
assertEquals(32768, branching.getJSONObject("minimumSurfaceCourseLength").getInt("maximum"));
|
||||
assertEquals(32768, branching.getJSONObject("minimumUndergroundCourseLength").getInt("maximum"));
|
||||
JSONObject meanders = referencedProperties(definitions, geometry.getJSONObject("meanders"));
|
||||
assertEquals(10D, meanders.getJSONObject("maximumTurnDegrees").getDouble("minimum"), 0D);
|
||||
assertEquals(List.of("SINKHOLE_GROTTO"),
|
||||
enumValues(definitions, routing.getJSONObject("inlandOutlets").getJSONObject("items")));
|
||||
assertEquals(64D, sources.getJSONObject("density").getDouble("maximum"), 0D);
|
||||
assertEquals(64D, undergroundSources.getJSONObject("density").getDouble("maximum"), 0D);
|
||||
assertEquals(8192, sources.getJSONObject("minimumSpacing").getInt("maximum"));
|
||||
assertEquals(8192, undergroundSources.getJSONObject("minimumSpacing").getInt("maximum"));
|
||||
assertEquals(16, drops.getJSONObject("cascadeRunPerBlock").getInt("maximum"));
|
||||
assertEquals(6D, drops.getJSONObject("cascadeExponent").getDouble("maximum"), 0D);
|
||||
assertEquals(4, drops.getJSONObject("maximumCascadeStep").getInt("maximum"));
|
||||
assertEquals(1D, drops.getJSONObject("flowWidthRatio").getDouble("maximum"), 0D);
|
||||
assertEquals(16, drops.getJSONObject("maximumFlowDepth").getInt("maximum"));
|
||||
assertEquals(4D, drops.getJSONObject("basinWidthRatio").getDouble("maximum"), 0D);
|
||||
assertEquals(32, drops.getJSONObject("maximumBasinDepth").getInt("maximum"));
|
||||
assertSnippetBackedObjectReference(channel.getJSONObject("width"));
|
||||
assertSnippetBackedObjectReference(channel.getJSONObject("depth"));
|
||||
assertSnippetBackedObjectReference(channel.getJSONObject("surfaceInset"));
|
||||
assertSnippetBackedObjectReference(channel.getJSONObject("terrainBlendWidth"));
|
||||
assertEquals(64, channel.getJSONObject("maximumIncision").getInt("maximum"));
|
||||
assertEquals(1D, channel.getJSONObject("shoreWidth").getDouble("minimum"), 0D);
|
||||
assertEquals(2D, channel.getJSONObject("shoreWidth").getDouble("maximum"), 0D);
|
||||
assertEquals(0, hydraulics.getJSONObject("riffleDrop").getInt("minimum"));
|
||||
assertEquals(16, hydraulics.getJSONObject("riffleDrop").getInt("maximum"));
|
||||
assertEquals(0, hydraulics.getJSONObject("maximumGradualDrop").getInt("minimum"));
|
||||
assertEquals(1024, hydraulics.getJSONObject("maximumGradualLength").getInt("maximum"));
|
||||
assertEquals(1, hydraulics.getJSONObject("waterfallMinimumDrop").getInt("minimum"));
|
||||
assertEquals(128, hydraulics.getJSONObject("waterfallMinimumDrop").getInt("maximum"));
|
||||
assertEquals(4096, ridgeTunnels.getJSONObject("maximumLength").getInt("maximum"));
|
||||
assertEquals(0, mouths.getJSONObject("levelingDistance").getInt("minimum"));
|
||||
assertEquals(64, mouths.getJSONObject("maximumOceanApron").getInt("maximum"));
|
||||
assertSnippetBackedObjectReference(underground.getJSONObject("fluidLevel"));
|
||||
assertSnippetBackedObjectReference(underground.getJSONObject("channelWidth"));
|
||||
assertSnippetBackedObjectReference(underground.getJSONObject("headroom"));
|
||||
|
||||
assertGrottoBounds(coastal);
|
||||
assertGrottoBounds(inland);
|
||||
assertEquals("boolean", inland.getJSONObject("connectSurfaceRivers").getString("type"));
|
||||
assertEquals("array", profiles.getString("type"));
|
||||
assertEquals(1, profiles.getInt("minItems"));
|
||||
assertTrue(profile.has("id"));
|
||||
assertTrue(profile.has("fluidPalette"));
|
||||
|
||||
assertEquals("array", deepFluids.getString("type"));
|
||||
assertTrue(deepFluid.has("id"));
|
||||
assertTrue(deepFluid.has("fluidPalette"));
|
||||
assertTrue(deepFluid.has("height"));
|
||||
assertEquals(64D, deepFluid.getJSONObject("density").getDouble("maximum"), 0D);
|
||||
assertEquals(8192, deepFluid.getJSONObject("spacing").getInt("maximum"));
|
||||
assertEquals(16, deepFluid.getJSONObject("spacing").getInt("minimum"));
|
||||
assertEquals(128, deepFluid.getJSONObject("horizontalRadius").getInt("maximum"));
|
||||
assertEquals(2, deepFluid.getJSONObject("horizontalRadius").getInt("minimum"));
|
||||
assertEquals(64, deepFluid.getJSONObject("verticalRadius").getInt("maximum"));
|
||||
assertEquals(2, deepFluid.getJSONObject("verticalRadius").getInt("minimum"));
|
||||
assertEquals(32, deepFluid.getJSONObject("channelWidth").getInt("maximum"));
|
||||
assertEquals(32, deepFluid.getJSONObject("depth").getInt("maximum"));
|
||||
assertEquals(63, deepFluid.getJSONObject("headroom").getInt("maximum"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void overrideSchemaKeepsEveryFieldOptionalAndTyped() {
|
||||
JSONObject schema = new SchemaBuilder(IrisRiverOverride.class, schemaData()).construct();
|
||||
public void riverPolicySchemaKeepsEveryOverlayOptionalAndTyped() {
|
||||
JSONObject schema = new SchemaBuilder(IrisRiverPolicy.class, schemaData()).construct();
|
||||
JSONObject definitions = schema.getJSONObject("definitions");
|
||||
JSONObject properties = schema.getJSONObject("properties");
|
||||
|
||||
assertTrue(!schema.has("required") || schema.getJSONArray("required").length() == 0);
|
||||
assertEquals("boolean", properties.getJSONObject("allowSources").getString("type"));
|
||||
assertEquals("number", properties.getJSONObject("routingCostMultiplier").getString("type"));
|
||||
assertEquals("array", properties.getJSONObject("channelBiomes").getString("type"));
|
||||
assertEquals(List.of(
|
||||
"DISABLED",
|
||||
"TRANSIT_ONLY",
|
||||
"NATURAL",
|
||||
"PREFERRED_HEADWATER",
|
||||
"REQUIRED_HEADWATER"
|
||||
),
|
||||
enumValues(definitions, properties.getJSONObject("placement")));
|
||||
assertEquals(List.of("BLOCK", "AVOID", "ALLOW", "PREFER"),
|
||||
enumValues(definitions, properties.getJSONObject("routing")));
|
||||
assertEquals("boolean", properties.getJSONObject("outletAdmission").getString("type"));
|
||||
assertEquals("array", properties.getJSONObject("profiles").getString("type"));
|
||||
assertEquals("array", properties.getJSONObject("surfaceBiomes").getString("type"));
|
||||
assertEquals("array", properties.getJSONObject("mouthBiomes").getString("type"));
|
||||
assertEquals("array", properties.getJSONObject("shoreBiomes").getString("type"));
|
||||
assertEquals("array", properties.getJSONObject("dryBiomes").getString("type"));
|
||||
assertEquals("array", properties.getJSONObject("floodedCaveBiomes").getString("type"));
|
||||
assertEquals(16D, properties.getJSONObject("widthMultiplier").getDouble("maximum"), 0D);
|
||||
assertEquals(0D, properties.getJSONObject("incisionMultiplier").getDouble("minimum"), 0D);
|
||||
assertEquals(64D, properties.getJSONObject("routingMultiplier").getDouble("maximum"), 0D);
|
||||
assertEquals("#/definitions/erzbiomes",
|
||||
properties.getJSONObject("floodedCaveBiomes").getJSONObject("items").getString("$ref"));
|
||||
properties.getJSONObject("shoreBiomes").getJSONObject("items").getString("$ref"));
|
||||
}
|
||||
|
||||
private static void assertGrottoBounds(JSONObject grotto) {
|
||||
assertEquals(1, grotto.getJSONObject("horizontalRadius").getInt("minimum"));
|
||||
assertEquals(1, grotto.getJSONObject("verticalRadius").getInt("minimum"));
|
||||
assertEquals(128, grotto.getJSONObject("horizontalRadius").getInt("maximum"));
|
||||
assertEquals(64, grotto.getJSONObject("verticalRadius").getInt("maximum"));
|
||||
assertEquals(63, grotto.getJSONObject("headroom").getInt("maximum"));
|
||||
assertEquals(1, grotto.getJSONObject("maximumVolume").getInt("minimum"));
|
||||
assertEquals(1048576, grotto.getJSONObject("maximumVolume").getInt("maximum"));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -152,7 +195,7 @@ public class IrisRiverSchemaTest {
|
||||
when(data.getBlockLoader()).thenReturn(blockLoader);
|
||||
when(data.getLoaders()).thenReturn(loaders);
|
||||
when(data.getPossibleSnippets(anyString())).thenReturn(new KList<>());
|
||||
when(biomeLoader.getPossibleKeys()).thenReturn(new String[]{"river/channel"});
|
||||
when(biomeLoader.getPossibleKeys()).thenReturn(new String[]{"river/surface"});
|
||||
when(biomeLoader.getFolderName()).thenReturn("biomes");
|
||||
when(biomeLoader.getResourceTypeName()).thenReturn("Biome");
|
||||
when(blockLoader.getPossibleKeys()).thenReturn(new String[0]);
|
||||
@@ -163,29 +206,33 @@ public class IrisRiverSchemaTest {
|
||||
}
|
||||
|
||||
private static JSONObject referencedProperties(JSONObject definitions, JSONObject reference) {
|
||||
return referencedDefinition(definitions, reference).getJSONObject("properties");
|
||||
}
|
||||
|
||||
private static JSONObject referencedDefinition(JSONObject definitions, JSONObject reference) {
|
||||
String key = reference.getString("$ref").substring("#/definitions/".length());
|
||||
return definitions.getJSONObject(key);
|
||||
return definitions.getJSONObject(key).getJSONObject("properties");
|
||||
}
|
||||
|
||||
private static boolean arrayContains(JSONArray values, String expected) {
|
||||
for (int index = 0; index < values.length(); index++) {
|
||||
if (expected.equals(values.getString(index))) {
|
||||
return true;
|
||||
private static void assertSnippetBackedObjectReference(JSONObject property) {
|
||||
JSONArray variants = property.getJSONArray("anyOf");
|
||||
boolean hasObjectReference = false;
|
||||
boolean hasSnippetReference = false;
|
||||
for (int index = 0; index < variants.length(); index++) {
|
||||
JSONObject variant = variants.getJSONObject(index);
|
||||
if ("object".equals(variant.getString("type")) && variant.has("$ref")) {
|
||||
hasObjectReference = true;
|
||||
}
|
||||
if ("string".equals(variant.getString("type")) && variant.has("$ref")) {
|
||||
hasSnippetReference = true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
assertTrue(hasObjectReference);
|
||||
assertTrue(hasSnippetReference);
|
||||
}
|
||||
|
||||
private static List<String> enumValues(JSONObject definitions, JSONObject reference) {
|
||||
String key = reference.getString("$ref").substring("#/definitions/".length());
|
||||
JSONArray values = definitions.getJSONObject(key).getJSONArray("oneOf");
|
||||
JSONArray values = definitions.getJSONObject(key).getJSONArray("enum");
|
||||
List<String> names = new ArrayList<>(values.length());
|
||||
for (int index = 0; index < values.length(); index++) {
|
||||
names.add(values.getJSONObject(index).getString("const"));
|
||||
names.add(values.getString(index));
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
package art.arcane.iris.engine;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisGenerator;
|
||||
import art.arcane.iris.engine.object.IrisInterpolator;
|
||||
import art.arcane.iris.util.project.interpolation.InterpolationMethod;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.StringJoiner;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class IrisComplexGeneratorOrderingTest {
|
||||
private static final int[][] PERMUTATIONS = {
|
||||
{0, 1, 2, 3},
|
||||
{3, 2, 1, 0},
|
||||
{1, 3, 0, 2},
|
||||
{2, 0, 3, 1}
|
||||
};
|
||||
|
||||
@Test
|
||||
public void frozenGroupsUseStableOrderAcrossInsertionPermutations() {
|
||||
IrisInterpolator bilinearWide = interpolator(InterpolationMethod.BILINEAR, 16D);
|
||||
IrisInterpolator starcast = interpolator(InterpolationMethod.STARCAST_3, 8D);
|
||||
IrisInterpolator bilinearNarrow = interpolator(InterpolationMethod.BILINEAR, 4D);
|
||||
List<IrisGenerator> generators = List.of(
|
||||
generator("zeta", starcast, 1L, 0D),
|
||||
generator("beta", bilinearWide, 2L, 0D),
|
||||
generator("alpha", bilinearWide, 3L, 0D),
|
||||
generator("delta", bilinearNarrow, 4L, 0D)
|
||||
);
|
||||
|
||||
String expected = "BILINEAR@4.0[delta]|BILINEAR@16.0[alpha,beta]|STARCAST_3@8.0[zeta]";
|
||||
for (int[] permutation : PERMUTATIONS) {
|
||||
IrisComplex.GeneratorGroup[] groups = IrisComplex.freezeGeneratorGroups(
|
||||
groupByInterpolator(generators, permutation)
|
||||
);
|
||||
assertEquals(expected, signature(groups));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void frozenGeneratorOrderProducesBitIdenticalOutputAcrossInsertionPermutations() {
|
||||
IrisInterpolator interpolator = interpolator(InterpolationMethod.BILINEAR_STARCAST_6, 7D);
|
||||
List<IrisGenerator> generators = List.of(
|
||||
generator("alpha", interpolator, 1L, 1.0E16D),
|
||||
generator("beta", interpolator, 2L, -1.0E16D),
|
||||
generator("gamma", interpolator, 3L, 1D),
|
||||
generator("omega", interpolator, 4L, 0D)
|
||||
);
|
||||
long expectedBits = Double.doubleToRawLongBits(0.25D);
|
||||
|
||||
for (int[] permutation : PERMUTATIONS) {
|
||||
IrisComplex.GeneratorGroup[] groups = IrisComplex.freezeGeneratorGroups(
|
||||
groupByInterpolator(generators, permutation)
|
||||
);
|
||||
double output = 0D;
|
||||
for (IrisComplex.GeneratorGroup group : groups) {
|
||||
output += IrisComplex.averageGeneratorHeights(
|
||||
group.generators(),
|
||||
0D,
|
||||
1D,
|
||||
0D,
|
||||
0D,
|
||||
0L
|
||||
);
|
||||
}
|
||||
assertEquals(expectedBits, Double.doubleToRawLongBits(output));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void emptyGeneratorCollectionFreezesAndAccumulatesAsEmpty() {
|
||||
Map<IrisInterpolator, Set<IrisGenerator>> generators = Map.of();
|
||||
|
||||
assertEquals(0, IrisComplex.freezeGeneratorGroups(generators).length);
|
||||
assertEquals(0D, IrisComplex.averageGeneratorHeights(
|
||||
new IrisGenerator[0],
|
||||
-1D,
|
||||
1D,
|
||||
0D,
|
||||
0D,
|
||||
0L
|
||||
), 0D);
|
||||
}
|
||||
|
||||
private IrisInterpolator interpolator(InterpolationMethod function, double horizontalScale) {
|
||||
return new IrisInterpolator()
|
||||
.setFunction(function)
|
||||
.setHorizontalScale(horizontalScale);
|
||||
}
|
||||
|
||||
private FixedGenerator generator(
|
||||
String loadKey,
|
||||
IrisInterpolator interpolator,
|
||||
long seed,
|
||||
double height
|
||||
) {
|
||||
FixedGenerator generator = new FixedGenerator(height);
|
||||
generator.setLoadKey(loadKey);
|
||||
generator.setInterpolator(interpolator);
|
||||
generator.setSeed(seed);
|
||||
return generator;
|
||||
}
|
||||
|
||||
private Map<IrisInterpolator, Set<IrisGenerator>> groupByInterpolator(
|
||||
List<IrisGenerator> generators,
|
||||
int[] permutation
|
||||
) {
|
||||
Map<IrisInterpolator, Set<IrisGenerator>> groups = new LinkedHashMap<>();
|
||||
for (int index : permutation) {
|
||||
IrisGenerator generator = generators.get(index);
|
||||
groups.computeIfAbsent(generator.getInterpolator(), ignored -> new LinkedHashSet<>()).add(generator);
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
private String signature(IrisComplex.GeneratorGroup[] groups) {
|
||||
StringJoiner groupSignature = new StringJoiner("|");
|
||||
for (IrisComplex.GeneratorGroup group : groups) {
|
||||
StringJoiner generatorKeys = new StringJoiner(",");
|
||||
for (IrisGenerator generator : group.generators()) {
|
||||
generatorKeys.add(generator.getLoadKey());
|
||||
}
|
||||
groupSignature.add(group.interpolator().getFunction().name()
|
||||
+ "@" + group.interpolator().getHorizontalScale()
|
||||
+ "[" + generatorKeys + "]");
|
||||
}
|
||||
return groupSignature.toString();
|
||||
}
|
||||
|
||||
private static final class FixedGenerator extends IrisGenerator {
|
||||
private final double height;
|
||||
|
||||
private FixedGenerator(double height) {
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getHeight(double x, double z, long seed) {
|
||||
return height;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,51 @@ public class IrisComplexGridBoundsCacheTest {
|
||||
assertEquals(1, interpolator.getInvocations());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void horizontalGridAxisMatchesLegacyBilerpBitForBit() throws Exception {
|
||||
IrisComplex complex = createComplex();
|
||||
Method gridSampleBounds = gridSampleBoundsMethod();
|
||||
CoordinateInterpolator interpolator = new CoordinateInterpolator();
|
||||
double x = 67D;
|
||||
double z = -32D;
|
||||
|
||||
NoiseBounds actual = invokeGridSampleBounds(complex, gridSampleBounds, interpolator, x, z);
|
||||
NoiseBounds expected = legacyGridSampleBounds(x, z);
|
||||
|
||||
assertBoundsBitsEqual(expected, actual);
|
||||
assertEquals(2, interpolator.getInvocations());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verticalGridAxisMatchesLegacyBilerpBitForBitAtNegativeCoordinates() throws Exception {
|
||||
IrisComplex complex = createComplex();
|
||||
Method gridSampleBounds = gridSampleBoundsMethod();
|
||||
CoordinateInterpolator interpolator = new CoordinateInterpolator();
|
||||
double x = -32D;
|
||||
double z = -29D;
|
||||
|
||||
NoiseBounds actual = invokeGridSampleBounds(complex, gridSampleBounds, interpolator, x, z);
|
||||
NoiseBounds expected = legacyGridSampleBounds(x, z);
|
||||
|
||||
assertBoundsBitsEqual(expected, actual);
|
||||
assertEquals(2, interpolator.getInvocations());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void interiorGridSampleMatchesLegacyBilerpBitForBit() throws Exception {
|
||||
IrisComplex complex = createComplex();
|
||||
Method gridSampleBounds = gridSampleBoundsMethod();
|
||||
CoordinateInterpolator interpolator = new CoordinateInterpolator();
|
||||
double x = -29D;
|
||||
double z = 67D;
|
||||
|
||||
NoiseBounds actual = invokeGridSampleBounds(complex, gridSampleBounds, interpolator, x, z);
|
||||
NoiseBounds expected = legacyGridSampleBounds(x, z);
|
||||
|
||||
assertBoundsBitsEqual(expected, actual);
|
||||
assertEquals(4, interpolator.getInvocations());
|
||||
}
|
||||
|
||||
private IrisComplex createComplex() throws Exception {
|
||||
IrisComplex complex = mock(IrisComplex.class, CALLS_REAL_METHODS);
|
||||
|
||||
@@ -133,6 +178,50 @@ public class IrisComplexGridBoundsCacheTest {
|
||||
return (NoiseBounds) method.invoke(complex, null, interpolator, 0, new IrisGenerator[0], x, z);
|
||||
}
|
||||
|
||||
private NoiseBounds legacyGridSampleBounds(double x, double z) {
|
||||
int grid = 4;
|
||||
int xi = (int) Math.floor(x);
|
||||
int zi = (int) Math.floor(z);
|
||||
int mask = grid - 1;
|
||||
int gx = xi & ~mask;
|
||||
int gz = zi & ~mask;
|
||||
double fx = (x - gx) / grid;
|
||||
double fz = (z - gz) / grid;
|
||||
NoiseBounds b00 = packedCoordinateBounds(gx, gz);
|
||||
NoiseBounds b10 = packedCoordinateBounds(gx + grid, gz);
|
||||
NoiseBounds b01 = packedCoordinateBounds(gx, gz + grid);
|
||||
NoiseBounds b11 = packedCoordinateBounds(gx + grid, gz + grid);
|
||||
return new NoiseBounds(
|
||||
legacyBiLerp(b00.min(), b10.min(), b01.min(), b11.min(), fx, fz),
|
||||
legacyBiLerp(b00.max(), b10.max(), b01.max(), b11.max(), fx, fz)
|
||||
);
|
||||
}
|
||||
|
||||
private NoiseBounds packedCoordinateBounds(int x, int z) {
|
||||
return new NoiseBounds(
|
||||
(float) CoordinateInterpolator.low(x, z),
|
||||
(float) CoordinateInterpolator.high(x, z)
|
||||
);
|
||||
}
|
||||
|
||||
private double legacyBiLerp(
|
||||
double v00,
|
||||
double v10,
|
||||
double v01,
|
||||
double v11,
|
||||
double fx,
|
||||
double fz
|
||||
) {
|
||||
double a = v00 + ((v10 - v00) * fx);
|
||||
double b = v01 + ((v11 - v01) * fx);
|
||||
return a + ((b - a) * fz);
|
||||
}
|
||||
|
||||
private void assertBoundsBitsEqual(NoiseBounds expected, NoiseBounds actual) {
|
||||
assertEquals(Double.doubleToRawLongBits(expected.min()), Double.doubleToRawLongBits(actual.min()));
|
||||
assertEquals(Double.doubleToRawLongBits(expected.max()), Double.doubleToRawLongBits(actual.max()));
|
||||
}
|
||||
|
||||
private float unpackLow(long packed) {
|
||||
return Float.intBitsToFloat((int) (packed >>> 32));
|
||||
}
|
||||
@@ -159,4 +248,26 @@ public class IrisComplexGridBoundsCacheTest {
|
||||
return invocations.get();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class CoordinateInterpolator extends IrisInterpolator {
|
||||
private final AtomicInteger invocations = new AtomicInteger();
|
||||
|
||||
@Override
|
||||
public NoiseBounds interpolateBounds(double x, double z, NoiseBoundsProvider provider) {
|
||||
invocations.incrementAndGet();
|
||||
return new NoiseBounds(low(x, z), high(x, z));
|
||||
}
|
||||
|
||||
private static double low(double x, double z) {
|
||||
return x * 0.125D - z * 0.0625D - 7.75D;
|
||||
}
|
||||
|
||||
private static double high(double x, double z) {
|
||||
return x * -0.03125D + z * 0.1875D + 12.5D;
|
||||
}
|
||||
|
||||
private int getInvocations() {
|
||||
return invocations.get();
|
||||
}
|
||||
}
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
package art.arcane.iris.engine;
|
||||
|
||||
import art.arcane.iris.engine.object.InferredType;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.util.project.stream.ProceduralStream;
|
||||
import art.arcane.iris.util.project.stream.interpolation.Interpolated;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumMap;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
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.assertNotSame;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class IrisComplexInferredBiomeStreamCacheTest {
|
||||
@Test
|
||||
public void compilesOncePerRegionIdentityAndPreservesSampling() {
|
||||
IrisRegion first = new IrisRegion();
|
||||
IrisRegion second = new IrisRegion();
|
||||
AtomicInteger compilations = new AtomicInteger();
|
||||
IdentityHashMap<IrisRegion, EnumMap<InferredType, IrisBiome>> expected = new IdentityHashMap<>();
|
||||
List<InferredType> compilationOrder = new ArrayList<>();
|
||||
|
||||
Map<IrisRegion, Map<InferredType, ProceduralStream<IrisBiome>>> streams =
|
||||
IrisComplex.compileInferredBiomeStreams(
|
||||
List.of(first, first, second),
|
||||
(region, inferredType) -> {
|
||||
compilations.incrementAndGet();
|
||||
compilationOrder.add(inferredType);
|
||||
IrisBiome biome = new IrisBiome().setInferredType(inferredType);
|
||||
expected.computeIfAbsent(region, ignored -> new EnumMap<>(InferredType.class))
|
||||
.put(inferredType, biome);
|
||||
return constant(biome);
|
||||
}
|
||||
);
|
||||
|
||||
assertEquals(8, compilations.get());
|
||||
assertEquals(List.of(
|
||||
InferredType.LAND,
|
||||
InferredType.CAVE,
|
||||
InferredType.SEA,
|
||||
InferredType.SHORE,
|
||||
InferredType.LAND,
|
||||
InferredType.CAVE,
|
||||
InferredType.SEA,
|
||||
InferredType.SHORE
|
||||
), compilationOrder);
|
||||
assertEquals(2, streams.size());
|
||||
assertNotSame(streams.get(first), streams.get(second));
|
||||
for (InferredType inferredType : InferredType.values()) {
|
||||
ProceduralStream<IrisBiome> firstStream = IrisComplex.preparedInferredBiomeStream(
|
||||
streams, first, inferredType);
|
||||
assertSame(firstStream, IrisComplex.preparedInferredBiomeStream(streams, first, inferredType));
|
||||
assertSame(expected.get(first).get(inferredType), firstStream.get(13D, 17D));
|
||||
assertSame(
|
||||
expected.get(second).get(inferredType),
|
||||
IrisComplex.preparedInferredBiomeStream(streams, second, inferredType).get(13D, 17D)
|
||||
);
|
||||
}
|
||||
assertThrows(UnsupportedOperationException.class, streams::clear);
|
||||
assertThrows(
|
||||
UnsupportedOperationException.class,
|
||||
() -> streams.get(first).put(InferredType.LAND, constant(new IrisBiome()))
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsOnlyUnpreparedRegionIdentities() {
|
||||
IrisRegion prepared = new IrisRegion();
|
||||
IrisRegion unprepared = new IrisRegion();
|
||||
unprepared.setLoadKey("missing");
|
||||
Map<IrisRegion, Map<InferredType, ProceduralStream<IrisBiome>>> streams =
|
||||
IrisComplex.compileInferredBiomeStreams(
|
||||
List.of(prepared),
|
||||
(region, inferredType) -> constant(new IrisBiome().setInferredType(inferredType))
|
||||
);
|
||||
|
||||
IllegalStateException error = assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> IrisComplex.preparedInferredBiomeStream(streams, unprepared, InferredType.LAND)
|
||||
);
|
||||
|
||||
assertTrue(error.getMessage().contains("missing"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void immutableCacheSupportsConcurrentReads() throws Exception {
|
||||
IrisRegion region = new IrisRegion();
|
||||
IrisBiome biome = new IrisBiome().setInferredType(InferredType.LAND);
|
||||
Map<IrisRegion, Map<InferredType, ProceduralStream<IrisBiome>>> streams =
|
||||
IrisComplex.compileInferredBiomeStreams(
|
||||
List.of(region),
|
||||
(preparedRegion, inferredType) -> constant(
|
||||
inferredType == InferredType.LAND
|
||||
? biome
|
||||
: new IrisBiome().setInferredType(inferredType)
|
||||
)
|
||||
);
|
||||
ExecutorService executor = Executors.newFixedThreadPool(8);
|
||||
try {
|
||||
List<Future<IrisBiome>> reads = new ArrayList<>();
|
||||
for (int task = 0; task < 32; task++) {
|
||||
reads.add(executor.submit(() -> {
|
||||
IrisBiome sampled = null;
|
||||
for (int read = 0; read < 1_000; read++) {
|
||||
sampled = IrisComplex.preparedInferredBiomeStream(
|
||||
streams, region, InferredType.LAND).get(read, -read);
|
||||
}
|
||||
return sampled;
|
||||
}));
|
||||
}
|
||||
for (Future<IrisBiome> read : reads) {
|
||||
assertSame(biome, read.get(5, TimeUnit.SECONDS));
|
||||
}
|
||||
} finally {
|
||||
executor.shutdownNow();
|
||||
assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
|
||||
private static ProceduralStream<IrisBiome> constant(IrisBiome biome) {
|
||||
return ProceduralStream.of(
|
||||
(x, z) -> biome,
|
||||
Interpolated.of(value -> 0D, value -> biome)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,138 +1,114 @@
|
||||
package art.arcane.iris.engine;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureRef;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
|
||||
import art.arcane.iris.engine.object.InferredType;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.IrisRiverOverride;
|
||||
import art.arcane.iris.engine.river.RiverRouteState;
|
||||
import art.arcane.iris.engine.river.RiverSample;
|
||||
import art.arcane.iris.engine.river.RiverSection;
|
||||
import art.arcane.iris.util.project.stream.ProceduralStream;
|
||||
import art.arcane.iris.util.project.stream.interpolation.Interpolated;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.mockito.ArgumentMatchers.anyDouble;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
public class IrisComplexSurfaceBiomeTest {
|
||||
@Test
|
||||
public void focusBiomeWithoutOverrideIgnoresOverridesOutsideFocus() {
|
||||
IrisBiome focusBiome = new IrisBiome();
|
||||
IrisBiome unreachableBiome = new IrisBiome().setRiverOverride(new IrisRiverOverride());
|
||||
public void gradingFootprintKeepsItsExactNaturalParentBiome() {
|
||||
HydrologyFeatureRef feature = new HydrologyFeatureRef(
|
||||
1L,
|
||||
HydrologyFeatureType.SURFACE_POOL,
|
||||
2L,
|
||||
3L,
|
||||
4,
|
||||
70,
|
||||
5,
|
||||
1,
|
||||
0,
|
||||
false
|
||||
);
|
||||
HydrologyColumnLayer grading = new HydrologyColumnLayer(
|
||||
feature,
|
||||
72,
|
||||
74,
|
||||
74,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
"water",
|
||||
"river",
|
||||
"mouth",
|
||||
"shore",
|
||||
"dry",
|
||||
"flooded"
|
||||
);
|
||||
HydrologyColumnSample sample = new HydrologyColumnSample(
|
||||
4,
|
||||
5,
|
||||
76,
|
||||
63,
|
||||
false,
|
||||
"exact_parent",
|
||||
List.of(grading)
|
||||
);
|
||||
|
||||
assertFalse(IrisComplex.biomeRiverOverridesPossible(focusBiome, List.of(unreachableBiome)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void focusBiomeOverrideAlwaysEnablesBiomeSampling() {
|
||||
IrisBiome focusBiome = new IrisBiome().setRiverOverride(new IrisRiverOverride());
|
||||
|
||||
assertTrue(IrisComplex.biomeRiverOverridesPossible(focusBiome, List.of()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nonFocusBiomeSamplingTracksReachableOverrides() {
|
||||
assertFalse(IrisComplex.biomeRiverOverridesPossible(null, List.of(new IrisBiome())));
|
||||
assertTrue(IrisComplex.biomeRiverOverridesPossible(
|
||||
null,
|
||||
List.of(new IrisBiome().setRiverOverride(new IrisRiverOverride()))
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void maxIncisionCapabilityIgnoresIdentityOverrides() {
|
||||
assertFalse(IrisComplex.changesMaxIncision(null));
|
||||
assertFalse(IrisComplex.changesMaxIncision(new IrisRiverOverride()));
|
||||
assertFalse(IrisComplex.changesMaxIncision(
|
||||
new IrisRiverOverride().setMaxIncisionMultiplier(1D)));
|
||||
assertTrue(IrisComplex.changesMaxIncision(
|
||||
new IrisRiverOverride().setMaxIncisionMultiplier(0.75D)));
|
||||
assertEquals("exact_parent", IrisComplex.hydrologySurfaceBiomeKey(sample));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void naturalOceanMaskTracksContinentalIntent() {
|
||||
assertTrue(IrisComplex.createNaturalOceanStream(
|
||||
assertEquals(InferredType.SEA, IrisComplex.resolveNaturalInferredType(
|
||||
constantType(InferredType.SEA),
|
||||
null
|
||||
).get(0D, 0D));
|
||||
assertFalse(IrisComplex.createNaturalOceanStream(
|
||||
null,
|
||||
0D,
|
||||
0D
|
||||
));
|
||||
assertEquals(InferredType.LAND, IrisComplex.resolveNaturalInferredType(
|
||||
constantType(InferredType.LAND),
|
||||
null
|
||||
).get(0D, 0D));
|
||||
null,
|
||||
0D,
|
||||
0D
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void naturalOceanMaskDoesNotSampleNaturalHeight() {
|
||||
AtomicInteger heightSamples = new AtomicInteger();
|
||||
ProceduralStream<Double> height = ProceduralStream.ofDouble((x, z) -> {
|
||||
heightSamples.incrementAndGet();
|
||||
return 62D;
|
||||
});
|
||||
|
||||
Boolean ocean = IrisComplex.createNaturalOceanStream(
|
||||
public void focusNaturalOceanMaskUsesTheFocusedSurfaceType() {
|
||||
assertEquals(InferredType.SEA, IrisComplex.resolveNaturalInferredType(
|
||||
constantType(InferredType.LAND),
|
||||
null
|
||||
).get(8D, -3D);
|
||||
|
||||
assertFalse(ocean);
|
||||
assertEquals(0, heightSamples.get());
|
||||
new IrisBiome().setInferredType(InferredType.SEA),
|
||||
0D,
|
||||
0D
|
||||
));
|
||||
assertEquals(InferredType.LAND, IrisComplex.resolveNaturalInferredType(
|
||||
constantType(InferredType.SEA),
|
||||
new IrisBiome().setInferredType(InferredType.LAND),
|
||||
0D,
|
||||
0D
|
||||
));
|
||||
assertEquals(InferredType.SHORE, IrisComplex.resolveNaturalInferredType(
|
||||
constantType(InferredType.SEA),
|
||||
new IrisBiome().setInferredType(InferredType.SHORE),
|
||||
0D,
|
||||
0D
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void focusNaturalOceanMaskIgnoresHeightForEverySurfaceType() {
|
||||
AtomicInteger heightSamples = new AtomicInteger();
|
||||
ProceduralStream<Double> height = ProceduralStream.ofDouble((x, z) -> {
|
||||
heightSamples.incrementAndGet();
|
||||
return -1_000D;
|
||||
});
|
||||
|
||||
assertTrue(IrisComplex.createNaturalOceanStream(
|
||||
constantType(InferredType.LAND),
|
||||
new IrisBiome().setInferredType(InferredType.SEA)
|
||||
).get(0D, 0D));
|
||||
assertFalse(IrisComplex.createNaturalOceanStream(
|
||||
constantType(InferredType.SEA),
|
||||
new IrisBiome().setInferredType(InferredType.LAND)
|
||||
).get(0D, 0D));
|
||||
assertFalse(IrisComplex.createNaturalOceanStream(
|
||||
constantType(InferredType.SEA),
|
||||
new IrisBiome().setInferredType(InferredType.SHORE)
|
||||
).get(0D, 0D));
|
||||
assertEquals(0, heightSamples.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fixedOceanMaskUsesContinentalIntentWithoutSamplingHeight() {
|
||||
AtomicInteger heightSamples = new AtomicInteger();
|
||||
ProceduralStream<Double> height = ProceduralStream.ofDouble((x, z) -> {
|
||||
heightSamples.incrementAndGet();
|
||||
return -1_000D;
|
||||
});
|
||||
|
||||
Boolean ocean = IrisComplex.createNaturalOceanStream(
|
||||
constantType(InferredType.SEA),
|
||||
null
|
||||
).get(8D, -3D);
|
||||
|
||||
assertTrue(ocean);
|
||||
assertEquals(0, heightSamples.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void emptyRiverPoolsKeepWetChannelsAquaticAndDryChannelsLand() {
|
||||
assertEquals(InferredType.SEA, IrisComplex.directRiverFallback(sample(RiverRouteState.WET, RiverSection.CHANNEL)));
|
||||
assertEquals(InferredType.SEA, IrisComplex.directRiverFallback(sample(RiverRouteState.WET, RiverSection.MOUTH)));
|
||||
assertEquals(InferredType.LAND, IrisComplex.directRiverFallback(sample(RiverRouteState.DRY, RiverSection.DRY_BANK)));
|
||||
assertNull(IrisComplex.directRiverFallback(sample(RiverRouteState.WET, RiverSection.BANK)));
|
||||
public void naturalSlopeMatchesTheRangeThreeStreamFormula() {
|
||||
assertEquals(5D, IrisComplex.calculateNaturalSlope(10D, 13D, 14D), 0D);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -153,10 +129,12 @@ public class IrisComplexSurfaceBiomeTest {
|
||||
70D,
|
||||
constant(base),
|
||||
constant(sea),
|
||||
constant(base));
|
||||
constant(base)
|
||||
);
|
||||
|
||||
assertSame(sea, resolved);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shorelineHeightSelectsShoreBiome() {
|
||||
IrisBiome base = mock(IrisBiome.class);
|
||||
@@ -174,7 +152,8 @@ public class IrisComplexSurfaceBiomeTest {
|
||||
63D,
|
||||
constant(base),
|
||||
constant(base),
|
||||
constant(shore));
|
||||
constant(shore)
|
||||
);
|
||||
|
||||
assertSame(shore, resolved);
|
||||
}
|
||||
@@ -197,7 +176,8 @@ public class IrisComplexSurfaceBiomeTest {
|
||||
63D,
|
||||
constant(land),
|
||||
constant(aquatic),
|
||||
constant(aquatic));
|
||||
constant(aquatic)
|
||||
);
|
||||
|
||||
assertSame(land, resolved);
|
||||
}
|
||||
@@ -215,8 +195,4 @@ public class IrisComplexSurfaceBiomeTest {
|
||||
Interpolated.of(value -> 0D, value -> type)
|
||||
);
|
||||
}
|
||||
|
||||
private static RiverSample sample(RiverRouteState state, RiverSection section) {
|
||||
return new RiverSample(true, state, section, 0D, 0.5D, 1D, 1, 1, 10D, 5D, 3D, false, null);
|
||||
}
|
||||
}
|
||||
+233
-22
@@ -1,40 +1,251 @@
|
||||
package art.arcane.iris.engine.actuator;
|
||||
|
||||
import art.arcane.iris.engine.river.RiverRouteState;
|
||||
import art.arcane.iris.engine.river.RiverSample;
|
||||
import art.arcane.iris.engine.river.RiverSection;
|
||||
import art.arcane.iris.engine.river.runtime.IrisRiverSurfaceSample;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureRef;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.project.hunk.Hunk;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
public class IrisDecorantActuatorRiverTest {
|
||||
@Test
|
||||
public void dryRiverSurfaceDoesNotRunShorelineDecoration() {
|
||||
IrisRiverSurfaceSample dry = new IrisRiverSurfaceSample(
|
||||
river(RiverRouteState.DRY, RiverSection.DRY_CHANNEL),
|
||||
70D,
|
||||
65D,
|
||||
65D,
|
||||
public void acceptedSurfaceRoleControlsShorelineDecoration() {
|
||||
HydrologyColumnSample shore = surfaceRole(false, true, true);
|
||||
HydrologyColumnSample channel = surfaceRole(true, false, false);
|
||||
HydrologyColumnSample grading = surfaceRole(false, false, true);
|
||||
HydrologyColumnSample underground = sample(HydrologyFeatureType.UNDERGROUND_POOL, true, true);
|
||||
HydrologyColumnSample overlap = new HydrologyColumnSample(
|
||||
0,
|
||||
0,
|
||||
70,
|
||||
63,
|
||||
false,
|
||||
"parent",
|
||||
List.of(
|
||||
layer(1L, true, false, false, HydrologyFeatureType.SURFACE_POOL, true, true),
|
||||
layer(2L, false, true, true, HydrologyFeatureType.SURFACE_POOL, false, false)
|
||||
)
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
IrisDecorantActuator.ShorelineDecorationMode.ACCEPTED,
|
||||
IrisDecorantActuator.shorelineDecorationMode(shore, 62, 65)
|
||||
);
|
||||
assertEquals(
|
||||
IrisDecorantActuator.ShorelineDecorationMode.NONE,
|
||||
IrisDecorantActuator.shorelineDecorationMode(channel, 65, 65)
|
||||
);
|
||||
assertEquals(
|
||||
IrisDecorantActuator.ShorelineDecorationMode.NONE,
|
||||
IrisDecorantActuator.shorelineDecorationMode(grading, 65, 65)
|
||||
);
|
||||
assertEquals(
|
||||
IrisDecorantActuator.ShorelineDecorationMode.NONE,
|
||||
IrisDecorantActuator.shorelineDecorationMode(overlap, 65, 65)
|
||||
);
|
||||
assertEquals(
|
||||
IrisDecorantActuator.ShorelineDecorationMode.LEGACY,
|
||||
IrisDecorantActuator.shorelineDecorationMode(underground, 65, 65)
|
||||
);
|
||||
assertEquals(
|
||||
IrisDecorantActuator.ShorelineDecorationMode.LEGACY,
|
||||
IrisDecorantActuator.shorelineDecorationMode(null, 65, 65)
|
||||
);
|
||||
assertEquals(
|
||||
IrisDecorantActuator.ShorelineDecorationMode.NONE,
|
||||
IrisDecorantActuator.shorelineDecorationMode(null, 64, 65)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onlyAcceptedFallingWaterfallThroatsRejectSurfaceDecoration() {
|
||||
assertTrue(IrisDecorantActuator.isFallingWaterfallThroat(
|
||||
sample(HydrologyFeatureType.WATERFALL, true, true, true, true)
|
||||
));
|
||||
assertTrue(IrisDecorantActuator.isFallingWaterfallThroat(
|
||||
sample(HydrologyFeatureType.SINKHOLE, true, true, true, true)
|
||||
));
|
||||
assertFalse(IrisDecorantActuator.isFallingWaterfallThroat(
|
||||
sample(HydrologyFeatureType.WATERFALL, false, false, false, false)
|
||||
));
|
||||
assertFalse(IrisDecorantActuator.isFallingWaterfallThroat(
|
||||
sample(HydrologyFeatureType.UNDERGROUND_DROP, true, true)
|
||||
));
|
||||
assertFalse(IrisDecorantActuator.isFallingWaterfallThroat(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void seaDecoratorsRequireConnectedWaterBelowTheConfiguredSurface() {
|
||||
Hunk<PlatformBlockState> output = Hunk.newArrayHunk(3, 4, 1);
|
||||
PlatformBlockState water = mock(PlatformBlockState.class);
|
||||
PlatformBlockState adjacentWater = mock(PlatformBlockState.class);
|
||||
doReturn(true).when(water).isWater();
|
||||
doReturn(true).when(adjacentWater).isWater();
|
||||
output.set(1, 1, 0, water);
|
||||
|
||||
assertFalse(IrisDecorantActuator.hasConnectedSurfaceWater(output, 1, 0, 0, 1));
|
||||
assertFalse(IrisDecorantActuator.hasConnectedSurfaceWater(output, 1, 0, 0, 2));
|
||||
|
||||
output.set(0, 1, 0, adjacentWater);
|
||||
assertTrue(IrisDecorantActuator.hasConnectedSurfaceWater(output, 1, 0, 0, 2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void seaDecoratorStacksRequireConnectedWaterAtEveryPlacedLevel() {
|
||||
Hunk<PlatformBlockState> output = Hunk.newArrayHunk(3, 4, 1);
|
||||
PlatformBlockState lowerWater = waterState();
|
||||
PlatformBlockState upperWater = waterState();
|
||||
PlatformBlockState adjacentWater = waterState();
|
||||
output.set(1, 1, 0, lowerWater);
|
||||
output.set(1, 2, 0, upperWater);
|
||||
output.set(0, 1, 0, adjacentWater);
|
||||
|
||||
assertFalse(IrisDecorantActuator.hasConnectedWaterColumn(output, 1, 0, 1, 2));
|
||||
|
||||
output.set(0, 2, 0, waterState());
|
||||
assertTrue(IrisDecorantActuator.hasConnectedWaterColumn(output, 1, 0, 1, 2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dryAquaticSeaSurfacePlacementRestoresTheOriginalBlock() {
|
||||
Hunk<PlatformBlockState> output = Hunk.newArrayHunk(3, 3, 1);
|
||||
PlatformBlockState air = state("minecraft:air");
|
||||
PlatformBlockState seagrass = state("minecraft:seagrass");
|
||||
output.set(1, 1, 0, air);
|
||||
|
||||
output.set(1, 1, 0, seagrass);
|
||||
IrisDecorantActuator.restoreUnsupportedAquaticPlacement(output, 1, 1, 0, air);
|
||||
|
||||
assertSame(air, output.get(1, 1, 0));
|
||||
}
|
||||
|
||||
private static HydrologyColumnSample sample(
|
||||
HydrologyFeatureType type,
|
||||
boolean connectedFluid,
|
||||
boolean fluidOwned
|
||||
) {
|
||||
return sample(
|
||||
type,
|
||||
connectedFluid,
|
||||
fluidOwned,
|
||||
true,
|
||||
type == HydrologyFeatureType.WATERFALL || type == HydrologyFeatureType.SINKHOLE
|
||||
);
|
||||
}
|
||||
|
||||
private static HydrologyColumnSample sample(
|
||||
HydrologyFeatureType type,
|
||||
boolean connectedFluid,
|
||||
boolean fluidOwned,
|
||||
boolean channel,
|
||||
boolean fallingFluid
|
||||
) {
|
||||
HydrologyColumnLayer layer = layer(
|
||||
1L,
|
||||
channel,
|
||||
false,
|
||||
!channel,
|
||||
type,
|
||||
connectedFluid,
|
||||
fluidOwned,
|
||||
fallingFluid
|
||||
);
|
||||
return new HydrologyColumnSample(0, 0, 70, 63, false, "parent", List.of(layer));
|
||||
}
|
||||
|
||||
private static HydrologyColumnSample surfaceRole(
|
||||
boolean channel,
|
||||
boolean shore,
|
||||
boolean grading
|
||||
) {
|
||||
HydrologyColumnLayer layer = layer(
|
||||
1L,
|
||||
channel,
|
||||
shore,
|
||||
grading,
|
||||
HydrologyFeatureType.SURFACE_POOL,
|
||||
channel,
|
||||
channel
|
||||
);
|
||||
return new HydrologyColumnSample(0, 0, 70, 63, false, "parent", List.of(layer));
|
||||
}
|
||||
|
||||
private static HydrologyColumnLayer layer(
|
||||
long id,
|
||||
boolean channel,
|
||||
boolean shore,
|
||||
boolean grading,
|
||||
HydrologyFeatureType type,
|
||||
boolean connectedFluid,
|
||||
boolean fluidOwned
|
||||
) {
|
||||
return layer(id, channel, shore, grading, type, connectedFluid, fluidOwned, false);
|
||||
}
|
||||
|
||||
private static HydrologyColumnLayer layer(
|
||||
long id,
|
||||
boolean channel,
|
||||
boolean shore,
|
||||
boolean grading,
|
||||
HydrologyFeatureType type,
|
||||
boolean connectedFluid,
|
||||
boolean fluidOwned,
|
||||
boolean fallingFluid
|
||||
) {
|
||||
HydrologyFeatureRef feature = new HydrologyFeatureRef(
|
||||
id,
|
||||
type,
|
||||
2L,
|
||||
3L,
|
||||
0,
|
||||
65,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
false
|
||||
);
|
||||
IrisRiverSurfaceSample wet = new IrisRiverSurfaceSample(
|
||||
river(RiverRouteState.WET, RiverSection.CHANNEL),
|
||||
70D,
|
||||
65D,
|
||||
65D,
|
||||
return new HydrologyColumnLayer(
|
||||
feature,
|
||||
63,
|
||||
65,
|
||||
type.isUnderground() ? 72 : 65,
|
||||
channel,
|
||||
shore,
|
||||
grading,
|
||||
connectedFluid,
|
||||
fallingFluid,
|
||||
fallingFluid,
|
||||
true,
|
||||
fluidOwned,
|
||||
false,
|
||||
true
|
||||
"water",
|
||||
"river",
|
||||
"mouth",
|
||||
"shore",
|
||||
"dry",
|
||||
"cave"
|
||||
);
|
||||
|
||||
assertFalse(IrisDecorantActuator.shouldDecorateShoreline(dry, 65));
|
||||
assertTrue(IrisDecorantActuator.shouldDecorateShoreline(wet, 65));
|
||||
assertTrue(IrisDecorantActuator.shouldDecorateShoreline(IrisRiverSurfaceSample.none(65D, 65D), 65));
|
||||
}
|
||||
|
||||
private static RiverSample river(RiverRouteState state, RiverSection section) {
|
||||
return new RiverSample(true, state, section, 0D, 0.5D, 1D, 1, 1, 10D, 5D, 3D, false, null);
|
||||
private static PlatformBlockState waterState() {
|
||||
PlatformBlockState water = state("minecraft:water");
|
||||
doReturn(true).when(water).isWater();
|
||||
return water;
|
||||
}
|
||||
|
||||
private static PlatformBlockState state(String key) {
|
||||
PlatformBlockState state = mock(PlatformBlockState.class);
|
||||
doReturn(key).when(state).key();
|
||||
return state;
|
||||
}
|
||||
}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
package art.arcane.iris.engine.actuator;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureRef;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
|
||||
import art.arcane.iris.engine.object.IrisDecorationStep;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
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.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class IrisTerrainNormalActuatorHydrologyTest {
|
||||
@Test
|
||||
public void hydrologyOwnedFluidBypassesBiomeSeaLayers() {
|
||||
PlatformBlockState water = mock(PlatformBlockState.class);
|
||||
PlatformBlockState ice = mock(PlatformBlockState.class);
|
||||
KList<PlatformBlockState> seaLayers = new KList<>();
|
||||
seaLayers.add(ice);
|
||||
|
||||
assertSame(water, HydrologyFluidLayerSelector.select(seaLayers, 0, water, true));
|
||||
assertSame(ice, HydrologyFluidLayerSelector.select(seaLayers, 0, water, false));
|
||||
assertSame(water, HydrologyFluidLayerSelector.select(seaLayers, 1, water, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void frozenTerrainPublishesSourceWaterBeforeStandardTopLayerFreezing() {
|
||||
PlatformBlockState water = mock(PlatformBlockState.class);
|
||||
PlatformBlockState ice = mock(PlatformBlockState.class);
|
||||
when(water.key()).thenReturn("minecraft:water");
|
||||
when(water.isWater()).thenReturn(true);
|
||||
when(ice.key()).thenReturn("minecraft:ice");
|
||||
KList<PlatformBlockState> frozenSeaLayers = new KList<>();
|
||||
frozenSeaLayers.add(ice);
|
||||
HydrologyFeatureRef feature = new HydrologyFeatureRef(
|
||||
11L,
|
||||
HydrologyFeatureType.SURFACE_POOL,
|
||||
12L,
|
||||
13L,
|
||||
4,
|
||||
70,
|
||||
6,
|
||||
1,
|
||||
0,
|
||||
true
|
||||
);
|
||||
HydrologyColumnLayer acceptedLayer = new HydrologyColumnLayer(
|
||||
feature,
|
||||
67,
|
||||
70,
|
||||
70,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
"default",
|
||||
"frozen_river",
|
||||
"frozen_mouth",
|
||||
"frozen_shore",
|
||||
"frozen_dry",
|
||||
"frozen_cave"
|
||||
);
|
||||
HydrologyColumnSample acceptedColumn = new HydrologyColumnSample(
|
||||
4,
|
||||
6,
|
||||
76,
|
||||
63,
|
||||
false,
|
||||
"frozen_parent",
|
||||
List.of(acceptedLayer)
|
||||
);
|
||||
boolean hydrologyOwned = acceptedColumn.primarySurfaceFluidLayer().isPresent();
|
||||
ArrayList<String> stages = new ArrayList<>();
|
||||
|
||||
PlatformBlockState state = HydrologyFluidLayerSelector.select(
|
||||
frozenSeaLayers,
|
||||
0,
|
||||
water,
|
||||
hydrologyOwned
|
||||
);
|
||||
stages.add("hydrology");
|
||||
assertTrue(hydrologyOwned);
|
||||
assertSame(water, state);
|
||||
assertEquals("minecraft:water", state.key());
|
||||
assertTrue(state.isWater());
|
||||
assertFalse(acceptedLayer.fallingFluid());
|
||||
|
||||
for (IrisDecorationStep step : IrisDecorationStep.values()) {
|
||||
if (step == IrisDecorationStep.TOP_LAYER_MODIFICATION) {
|
||||
assertSame(water, state);
|
||||
state = ice;
|
||||
stages.add(step.getSerializedName());
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(List.of("hydrology", "top_layer_modification"), stages);
|
||||
assertSame(ice, state);
|
||||
assertEquals("minecraft:ice", state.key());
|
||||
assertFalse(state.isWater());
|
||||
}
|
||||
}
|
||||
+176
@@ -16,6 +16,7 @@ import org.junit.Test;
|
||||
|
||||
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.any;
|
||||
import static org.mockito.ArgumentMatchers.anyDouble;
|
||||
@@ -74,4 +75,179 @@ public class IrisDecoratorCaveContextTest {
|
||||
assertTrue(IrisSurfaceDecorator.isUnderwater(null, 10, 63));
|
||||
assertFalse(IrisSurfaceDecorator.skipsFluid(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void underwaterSurfaceDecorationRequiresActualFluidAboveTheTerrain() {
|
||||
Hunk<PlatformBlockState> output = mock(Hunk.class);
|
||||
PlatformBlockState dry = mock(PlatformBlockState.class);
|
||||
PlatformBlockState fluid = mock(PlatformBlockState.class);
|
||||
doReturn(128).when(output).getHeight();
|
||||
doReturn(dry).when(output).get(0, 11, 0);
|
||||
doReturn(false).when(dry).isFluid();
|
||||
|
||||
assertFalse(IrisSurfaceDecorator.hasFluidAbove(output, 0, 10, 0));
|
||||
|
||||
doReturn(fluid).when(output).get(0, 11, 0);
|
||||
doReturn(true).when(fluid).isFluid();
|
||||
|
||||
assertTrue(IrisSurfaceDecorator.hasFluidAbove(output, 0, 10, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aquaticPlacementClassificationCoversWaterPlantsCoralAndWaterloggedStates() {
|
||||
assertTrue(IrisSurfaceDecorator.isAquaticPlacement(state("minecraft:seagrass")));
|
||||
assertTrue(IrisSurfaceDecorator.isAquaticPlacement(state("minecraft:tall_seagrass[half=lower]")));
|
||||
assertTrue(IrisSurfaceDecorator.isAquaticPlacement(state("minecraft:kelp")));
|
||||
assertTrue(IrisSurfaceDecorator.isAquaticPlacement(state("minecraft:kelp_plant")));
|
||||
assertTrue(IrisSurfaceDecorator.isAquaticPlacement(state("minecraft:sea_pickle[pickles=2]")));
|
||||
assertTrue(IrisSurfaceDecorator.isAquaticPlacement(state("minecraft:fire_coral_fan")));
|
||||
|
||||
PlatformBlockState waterlogged = state("minecraft:oak_fence[waterlogged=true]");
|
||||
doReturn(true).when(waterlogged).isWaterLogged();
|
||||
assertTrue(IrisSurfaceDecorator.isAquaticPlacement(waterlogged));
|
||||
|
||||
assertFalse(IrisSurfaceDecorator.isAquaticPlacement(state("minecraft:stone")));
|
||||
assertFalse(IrisSurfaceDecorator.isAquaticPlacement(state("minecraft:dead_fire_coral_fan")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void compatibleWaterMustBeHorizontallyConnected() {
|
||||
Hunk<PlatformBlockState> output = Hunk.newArrayHunk(3, 3, 1);
|
||||
PlatformBlockState centerWater = waterState();
|
||||
PlatformBlockState neighborWater = waterState();
|
||||
PlatformBlockState lava = fluidState();
|
||||
output.set(1, 1, 0, centerWater);
|
||||
|
||||
assertFalse(IrisSurfaceDecorator.hasConnectedWater(output, 1, 1, 0));
|
||||
|
||||
output.set(0, 1, 0, lava);
|
||||
assertFalse(IrisSurfaceDecorator.hasConnectedWater(output, 1, 1, 0));
|
||||
|
||||
output.set(0, 1, 0, neighborWater);
|
||||
assertTrue(IrisSurfaceDecorator.hasConnectedWater(output, 1, 1, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aquaticPlacementAtFluidHeightBoundaryRestoresDryBank() {
|
||||
IrisData data = mock(IrisData.class);
|
||||
PlatformBlockState air = state("minecraft:air");
|
||||
PlatformBlockState seagrass = state("minecraft:seagrass");
|
||||
IrisDecorator decorator = aquaticDecorator(data, seagrass, false, 1);
|
||||
Hunk<PlatformBlockState> output = Hunk.newArrayHunk(3, 4, 1);
|
||||
output.set(1, 1, 0, air);
|
||||
|
||||
IrisSurfaceDecorator.AquaticPlacementSnapshot snapshot = IrisSurfaceDecorator.captureAquaticPlacement(
|
||||
decorator, data, output, 1, 0, 0, 4);
|
||||
output.set(1, 1, 0, seagrass);
|
||||
snapshot.restoreIfUnsupported(output, 1, 0);
|
||||
|
||||
assertSame(air, output.get(1, 1, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void legitimateUnderwaterAquaticPlacementIsPreserved() {
|
||||
IrisData data = mock(IrisData.class);
|
||||
PlatformBlockState sourceWater = waterState();
|
||||
PlatformBlockState adjacentWater = waterState();
|
||||
PlatformBlockState kelp = state("minecraft:kelp_plant");
|
||||
IrisDecorator decorator = aquaticDecorator(data, kelp, true, 1);
|
||||
Hunk<PlatformBlockState> output = Hunk.newArrayHunk(3, 4, 1);
|
||||
output.set(1, 1, 0, sourceWater);
|
||||
output.set(0, 1, 0, adjacentWater);
|
||||
|
||||
IrisSurfaceDecorator.AquaticPlacementSnapshot snapshot = IrisSurfaceDecorator.captureAquaticPlacement(
|
||||
decorator, data, output, 1, 0, 0, 4);
|
||||
output.set(1, 1, 0, kelp);
|
||||
snapshot.restoreIfUnsupported(output, 1, 0);
|
||||
|
||||
assertSame(kelp, output.get(1, 1, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unsupportedUpperStackRestoresTheWholeAquaticPlacement() {
|
||||
IrisData data = mock(IrisData.class);
|
||||
PlatformBlockState lowerWater = waterState();
|
||||
PlatformBlockState upperWater = waterState();
|
||||
PlatformBlockState adjacentWater = waterState();
|
||||
PlatformBlockState kelp = state("minecraft:kelp_plant");
|
||||
IrisDecorator decorator = aquaticDecorator(data, kelp, true, 2);
|
||||
Hunk<PlatformBlockState> output = Hunk.newArrayHunk(3, 4, 1);
|
||||
output.set(1, 1, 0, lowerWater);
|
||||
output.set(1, 2, 0, upperWater);
|
||||
output.set(0, 1, 0, adjacentWater);
|
||||
|
||||
IrisSurfaceDecorator.AquaticPlacementSnapshot snapshot = IrisSurfaceDecorator.captureAquaticPlacement(
|
||||
decorator, data, output, 1, 0, 0, 4);
|
||||
output.set(1, 1, 0, kelp);
|
||||
output.set(1, 2, 0, kelp);
|
||||
snapshot.restoreIfUnsupported(output, 1, 0);
|
||||
|
||||
assertSame(lowerWater, output.get(1, 1, 0));
|
||||
assertSame(upperWater, output.get(1, 2, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dryWaterloggedAndTwoBlockAquaticPlacementsAreRestoredAtomically() {
|
||||
IrisData data = mock(IrisData.class);
|
||||
PlatformBlockState lowerAir = state("minecraft:air");
|
||||
PlatformBlockState upperAir = state("minecraft:air");
|
||||
PlatformBlockState lower = state("minecraft:tall_seagrass[half=lower]");
|
||||
PlatformBlockState upper = state("minecraft:tall_seagrass[half=upper]");
|
||||
IrisDecorator decorator = aquaticDecorator(data, lower, false, 1);
|
||||
Hunk<PlatformBlockState> output = Hunk.newArrayHunk(3, 4, 1);
|
||||
output.set(1, 1, 0, lowerAir);
|
||||
output.set(1, 2, 0, upperAir);
|
||||
|
||||
IrisSurfaceDecorator.AquaticPlacementSnapshot snapshot = IrisSurfaceDecorator.captureAquaticPlacement(
|
||||
decorator, data, output, 1, 0, 0, 4);
|
||||
output.set(1, 1, 0, lower);
|
||||
output.set(1, 2, 0, upper);
|
||||
snapshot.restoreIfUnsupported(output, 1, 0);
|
||||
|
||||
assertSame(lowerAir, output.get(1, 1, 0));
|
||||
assertSame(upperAir, output.get(1, 2, 0));
|
||||
|
||||
PlatformBlockState dryWaterlogged = state("minecraft:mangrove_roots[waterlogged=true]");
|
||||
doReturn(true).when(dryWaterlogged).isWaterLogged();
|
||||
IrisDecorator waterloggedDecorator = aquaticDecorator(data, dryWaterlogged, false, 1);
|
||||
IrisSurfaceDecorator.AquaticPlacementSnapshot waterloggedSnapshot = IrisSurfaceDecorator.captureAquaticPlacement(
|
||||
waterloggedDecorator, data, output, 1, 0, 0, 4);
|
||||
output.set(1, 1, 0, dryWaterlogged);
|
||||
waterloggedSnapshot.restoreIfUnsupported(output, 1, 0);
|
||||
|
||||
assertSame(lowerAir, output.get(1, 1, 0));
|
||||
}
|
||||
|
||||
private IrisDecorator aquaticDecorator(
|
||||
IrisData data,
|
||||
PlatformBlockState aquatic,
|
||||
boolean stacking,
|
||||
int stackMaximum
|
||||
) {
|
||||
IrisDecorator decorator = mock(IrisDecorator.class);
|
||||
doReturn(new PlatformBlockState[]{aquatic}).when(decorator).getBlockDataArray(data);
|
||||
doReturn(new PlatformBlockState[0]).when(decorator).getBlockDataTopsArray(data);
|
||||
doReturn(stacking).when(decorator).isStacking();
|
||||
doReturn(stackMaximum).when(decorator).getStackMax();
|
||||
return decorator;
|
||||
}
|
||||
|
||||
private PlatformBlockState state(String key) {
|
||||
PlatformBlockState state = mock(PlatformBlockState.class);
|
||||
doReturn(key).when(state).key();
|
||||
return state;
|
||||
}
|
||||
|
||||
private PlatformBlockState waterState() {
|
||||
PlatformBlockState water = fluidState();
|
||||
doReturn(true).when(water).isWater();
|
||||
return water;
|
||||
}
|
||||
|
||||
private PlatformBlockState fluidState() {
|
||||
PlatformBlockState fluid = state("minecraft:lava");
|
||||
doReturn(true).when(fluid).isFluid();
|
||||
return fluid;
|
||||
}
|
||||
}
|
||||
+88
-4
@@ -75,19 +75,101 @@ public class IrisShoreLineDecoratorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void forcePlaceStillRejectsMissingSurface() {
|
||||
Fixture fixture = createFixture(true);
|
||||
public void acceptedShoreUsesPublishedGeometry() {
|
||||
Fixture fixture = createFixture(false, false);
|
||||
PlatformBlockState support = sturdyState();
|
||||
PlatformBlockState targetAir = airState();
|
||||
Hunk<PlatformBlockState> output = output(airState(), targetAir);
|
||||
when(fixture.decorant.canPlaceOnto(support)).thenReturn(true);
|
||||
Hunk<PlatformBlockState> output = output(support, targetAir);
|
||||
|
||||
fixture.shoreline.decorate(0, 0, 0, 1, -1, 0, 1, -1,
|
||||
output, fixture.biome, FLUID_HEIGHT, output.getHeight());
|
||||
|
||||
assertSame(targetAir, output.get(0, FLUID_HEIGHT + 1, 0));
|
||||
|
||||
fixture.shoreline.decorateAcceptedShore(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
output,
|
||||
fixture.biome,
|
||||
FLUID_HEIGHT,
|
||||
output.getHeight()
|
||||
);
|
||||
|
||||
assertSame(fixture.decorant, output.get(0, FLUID_HEIGHT + 1, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void forcePlaceStillRejectsMissingSurface() {
|
||||
Fixture fixture = createFixture(true);
|
||||
PlatformBlockState targetAir = airState();
|
||||
Hunk<PlatformBlockState> output = output(airState(), targetAir);
|
||||
|
||||
fixture.shoreline.decorateAcceptedShore(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
output,
|
||||
fixture.biome,
|
||||
FLUID_HEIGHT,
|
||||
output.getHeight()
|
||||
);
|
||||
|
||||
assertSame(targetAir, output.get(0, FLUID_HEIGHT + 1, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unsupportedWaterloggedStackRestoresEveryOriginalBlock() {
|
||||
Fixture fixture = createFixture(false, false);
|
||||
PlatformBlockState support = sturdyState();
|
||||
PlatformBlockState lowerOriginal = airState();
|
||||
PlatformBlockState upperOriginal = airState();
|
||||
PlatformBlockState waterloggedTop = mock(PlatformBlockState.class);
|
||||
when(fixture.decorant.key()).thenReturn("minecraft:grass");
|
||||
when(fixture.decorant.canPlaceOnto(support)).thenReturn(true);
|
||||
when(waterloggedTop.isWaterLogged()).thenReturn(true);
|
||||
when(fixture.decorator.isStacking()).thenReturn(true);
|
||||
when(fixture.decorator.isScaleStack()).thenReturn(false);
|
||||
when(fixture.decorator.getStackMax()).thenReturn(2);
|
||||
when(fixture.decorator.getHeight(any(), anyDouble(), anyDouble(), eq(fixture.data))).thenReturn(2);
|
||||
when(fixture.decorator.getTopThreshold()).thenReturn(1D);
|
||||
when(fixture.decorator.getBlockDataArray(fixture.data)).thenReturn(new PlatformBlockState[0]);
|
||||
when(fixture.decorator.getBlockDataTopsArray(fixture.data))
|
||||
.thenReturn(new PlatformBlockState[]{waterloggedTop});
|
||||
when(fixture.decorator.getBlockDataForTop(
|
||||
eq(fixture.biome), any(), anyDouble(), anyDouble(), anyDouble(), eq(fixture.data)))
|
||||
.thenReturn(waterloggedTop);
|
||||
|
||||
Hunk<PlatformBlockState> output = Hunk.newArrayHunk(1, FLUID_HEIGHT + 3, 1);
|
||||
output.set(0, FLUID_HEIGHT, 0, support);
|
||||
output.set(0, FLUID_HEIGHT + 1, 0, lowerOriginal);
|
||||
output.set(0, FLUID_HEIGHT + 2, 0, upperOriginal);
|
||||
|
||||
fixture.shoreline.decorateAcceptedShore(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
output,
|
||||
fixture.biome,
|
||||
FLUID_HEIGHT,
|
||||
output.getHeight()
|
||||
);
|
||||
|
||||
assertSame(lowerOriginal, output.get(0, FLUID_HEIGHT + 1, 0));
|
||||
assertSame(upperOriginal, output.get(0, FLUID_HEIGHT + 2, 0));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Fixture createFixture(boolean forcePlace) {
|
||||
return createFixture(forcePlace, true);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Fixture createFixture(boolean forcePlace, boolean naturalShore) {
|
||||
Engine engine = mock(Engine.class);
|
||||
SeedManager seedManager = mock(SeedManager.class);
|
||||
IrisDimension dimension = mock(IrisDimension.class);
|
||||
@@ -109,7 +191,9 @@ public class IrisShoreLineDecoratorTest {
|
||||
when(complex.getFluidHeight()).thenReturn((double) FLUID_HEIGHT);
|
||||
when(complex.getHeightStream()).thenReturn(heightStream);
|
||||
when(complex.getRiverWaterSurfaceStream()).thenReturn(fluidStream);
|
||||
when(heightStream.get(anyDouble(), anyDouble())).thenReturn((double) FLUID_HEIGHT - 1);
|
||||
when(heightStream.get(anyDouble(), anyDouble())).thenReturn(
|
||||
naturalShore ? (double) FLUID_HEIGHT - 1 : (double) FLUID_HEIGHT
|
||||
);
|
||||
when(fluidStream.get(anyDouble(), anyDouble())).thenReturn((double) FLUID_HEIGHT);
|
||||
when(engine.getData()).thenReturn(data);
|
||||
when(biome.getDecoratorBucket(IrisDecorationPart.SHORE_LINE))
|
||||
|
||||
@@ -7,6 +7,7 @@ import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.hydrology.runtime.IrisHydrologyRuntime;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.util.project.stream.ProceduralStream;
|
||||
@@ -91,6 +92,7 @@ public class HintedLocatorTest {
|
||||
when(engine.isClosed()).thenReturn(false);
|
||||
when(engine.acquireGenerationLease(any(String.class))).thenReturn(GenerationSessionLease.noop());
|
||||
when(data.getBiomeLoader()).thenReturn(biomeLoader);
|
||||
when(dimension.getReachableBiomes(engine)).thenReturn(new KList<>());
|
||||
}
|
||||
|
||||
private static IrisBiome biome(String key) {
|
||||
@@ -211,6 +213,57 @@ public class HintedLocatorTest {
|
||||
assertFalse(plan.isPossible());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void biomePlanFindsPolicyOnlyReachableBiome() throws Exception {
|
||||
IrisRegion regionA = region("reg_a");
|
||||
regionA.setLandBiomes(keys("plains"));
|
||||
|
||||
IrisBiome plains = biome("plains");
|
||||
IrisBiome riverChild = biome("river_child");
|
||||
stubBiome(plains);
|
||||
stubBiome(riverChild);
|
||||
stubRegions(regionA);
|
||||
|
||||
when(dimension.getReachableBiomes(engine)).thenReturn(new KList<>(riverChild));
|
||||
when(complex.getRegionStream()).thenReturn(constantStream(regionA));
|
||||
when(engine.getSurfaceBiome(anyInt(), anyInt())).thenAnswer(invocation -> {
|
||||
int x = invocation.getArgument(0);
|
||||
return x >= 1024 ? riverChild : plains;
|
||||
});
|
||||
|
||||
HintedLocator.SearchPlan plan = HintedLocator.biomePlan(engine, "river_child");
|
||||
assertTrue(plan.isPossible());
|
||||
assertNull(plan.getCoarse());
|
||||
|
||||
Locator<IrisBiome> locator = Locator.surfaceBiome("river_child");
|
||||
Position2 result = locator.find(engine, new Position2(0, 0), 60_000, (Integer count) -> {
|
||||
}).get();
|
||||
|
||||
assertNotNull(result);
|
||||
assertTrue(((result.getX() << 4) + 8) >= 1024);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void surfaceBiomeLocatorFindsNarrowAcceptedBiomeAwayFromChunkCenter() {
|
||||
IrisBiome riverShore = biome("river_shore");
|
||||
IrisBiome plains = biome("plains");
|
||||
IrisHydrologyRuntime hydrology = mock(IrisHydrologyRuntime.class);
|
||||
when(engine.getSurfaceBiome(anyInt(), anyInt())).thenReturn(plains);
|
||||
when(complex.getHydrologyRuntime()).thenReturn(hydrology);
|
||||
when(hydrology.hasAcceptedSurfaceBiomeInChunk("river_shore", 0, 0)).thenReturn(true);
|
||||
|
||||
assertTrue(Locator.chunkContainsSurfaceBiome(
|
||||
engine,
|
||||
new Position2(0, 0),
|
||||
riverShore.getLoadKey()
|
||||
));
|
||||
assertFalse(Locator.chunkContainsSurfaceBiome(
|
||||
engine,
|
||||
new Position2(1, 0),
|
||||
riverShore.getLoadKey()
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void biomePlanFallsBackToCaveStreamForCaveOnlyBiome() {
|
||||
IrisRegion regionA = region("reg_a");
|
||||
|
||||
+8
-8
@@ -3,8 +3,8 @@ package art.arcane.iris.engine.framework;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisStructureAnchorMode;
|
||||
import art.arcane.iris.engine.object.IrisStructurePlacement;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveAction;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveAction;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveCell;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
@@ -181,18 +181,18 @@ public class StructureCaveAnchorResolverTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void wetAndSealHydrologyCannotBecomeStructureAnchors() {
|
||||
public void plannedHydrologyCannotBecomeStructureAnchors() {
|
||||
MatterCavern fluid = new MatterCavern(true, "", (byte) 1);
|
||||
MatterCavern forcedAir = new MatterCavern(true, "", (byte) 3);
|
||||
|
||||
assertFalse(StructureCaveAnchorResolver.acceptsAnchorFluid(
|
||||
true, fluid, RiverCaveHydrology.of(RiverCaveAction.WET_SOURCE), 20, 8));
|
||||
true, fluid, HydrologyCaveCell.of(HydrologyCaveAction.WET_SOURCE), 20, 8));
|
||||
assertFalse(StructureCaveAnchorResolver.acceptsAnchorFluid(
|
||||
true, fluid, RiverCaveHydrology.of(RiverCaveAction.FALLING_FLUID), 20, 8));
|
||||
true, fluid, HydrologyCaveCell.of(HydrologyCaveAction.FALLING_FLUID), 20, 8));
|
||||
assertFalse(StructureCaveAnchorResolver.acceptsAnchorFluid(
|
||||
true, null, RiverCaveHydrology.of(RiverCaveAction.SEAL_GUARD), 20, 8));
|
||||
assertTrue(StructureCaveAnchorResolver.acceptsAnchorFluid(
|
||||
false, forcedAir, RiverCaveHydrology.of(RiverCaveAction.DRY_AIR), 0, 8));
|
||||
true, null, HydrologyCaveCell.of(HydrologyCaveAction.SEAL_GUARD), 20, 8));
|
||||
assertFalse(StructureCaveAnchorResolver.acceptsAnchorFluid(
|
||||
false, forcedAir, HydrologyCaveCell.of(HydrologyCaveAction.DRY_AIR), 0, 8));
|
||||
}
|
||||
|
||||
private static Engine engineWithFloorAnchor(MatterCavern anchorCavern, int surfaceHeight) {
|
||||
|
||||
+190
-32
@@ -2,52 +2,123 @@ package art.arcane.iris.engine.framework.render;
|
||||
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyCandidateKind;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyCandidateRejection;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyDiagnosticCandidate;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyDiagnosticRenderSample;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureRef;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyPoint;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyRenderSample;
|
||||
import art.arcane.iris.engine.hydrology.runtime.IrisHydrologyRuntime;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.river.RiverSample;
|
||||
import art.arcane.iris.engine.river.RiverSection;
|
||||
import art.arcane.iris.engine.river.runtime.IrisRiverRuntime;
|
||||
import art.arcane.iris.util.project.stream.ProceduralStream;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.mockito.ArgumentMatchers.anyDouble;
|
||||
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 IrisRendererRiverTest {
|
||||
@Test
|
||||
public void everyRiverSectionHasItsDiagnosticColor() {
|
||||
Map<RiverSection, Integer> expected = new EnumMap<>(RiverSection.class);
|
||||
expected.put(RiverSection.CHANNEL, new Color(48, 112, 190).getRGB());
|
||||
expected.put(RiverSection.MOUTH, new Color(54, 164, 205).getRGB());
|
||||
expected.put(RiverSection.BANK, new Color(92, 146, 78).getRGB());
|
||||
expected.put(RiverSection.DRY_CHANNEL, new Color(171, 128, 68).getRGB());
|
||||
expected.put(RiverSection.DRY_BANK, new Color(132, 105, 62).getRGB());
|
||||
expected.put(RiverSection.NONE, new Color(28, 31, 38).getRGB());
|
||||
public void everyAcceptedHydrologyFeatureHasItsDiagnosticColor() {
|
||||
Map<HydrologyFeatureType, Integer> expected = new EnumMap<>(HydrologyFeatureType.class);
|
||||
expected.put(HydrologyFeatureType.SURFACE_POOL, new Color(48, 112, 190).getRGB());
|
||||
expected.put(HydrologyFeatureType.RIFFLE, new Color(96, 166, 214).getRGB());
|
||||
expected.put(HydrologyFeatureType.CASCADE, new Color(135, 204, 232).getRGB());
|
||||
expected.put(HydrologyFeatureType.WATERFALL, new Color(190, 236, 255).getRGB());
|
||||
expected.put(HydrologyFeatureType.SINKHOLE, new Color(176, 146, 232).getRGB());
|
||||
expected.put(HydrologyFeatureType.RIDGE_BORE, new Color(120, 99, 174).getRGB());
|
||||
expected.put(HydrologyFeatureType.UNDERGROUND_POOL, new Color(102, 86, 196).getRGB());
|
||||
expected.put(HydrologyFeatureType.UNDERGROUND_DROP, new Color(102, 86, 196).getRGB());
|
||||
expected.put(HydrologyFeatureType.COASTAL_GROTTO, new Color(51, 174, 174).getRGB());
|
||||
expected.put(HydrologyFeatureType.INLAND_GROTTO, new Color(116, 76, 150).getRGB());
|
||||
expected.put(HydrologyFeatureType.MOUTH, new Color(54, 164, 205).getRGB());
|
||||
expected.put(HydrologyFeatureType.DEEP_POOL, new Color(194, 50, 22).getRGB());
|
||||
expected.put(HydrologyFeatureType.DEEP_CHANNEL, new Color(194, 50, 22).getRGB());
|
||||
|
||||
assertEquals(RiverSection.values().length, expected.size());
|
||||
for (RiverSection section : RiverSection.values()) {
|
||||
assertEquals(section.name(), expected.get(section).intValue(), IrisRenderer.riverColor(section));
|
||||
assertEquals(HydrologyFeatureType.values().length, expected.size());
|
||||
for (HydrologyFeatureType type : HydrologyFeatureType.values()) {
|
||||
assertEquals(type.name(), expected.get(type).intValue(), IrisRenderer.hydrologyFeatureColor(type));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void riverRenderTypeSamplesOneWorldFootprintPerRenderedPixel() {
|
||||
public void headwatersAndWaterfallsRemainDistinct() {
|
||||
HydrologyRenderSample headwater = sample(HydrologyFeatureType.SURFACE_POOL, true);
|
||||
HydrologyRenderSample waterfall = sample(HydrologyFeatureType.WATERFALL, false);
|
||||
|
||||
assertEquals(IrisRenderer.headwaterColor(), IrisRenderer.hydrologyColor(headwater));
|
||||
assertEquals(IrisRenderer.waterfallColor(), IrisRenderer.hydrologyColor(waterfall));
|
||||
assertNotEquals(IrisRenderer.headwaterColor(), IrisRenderer.waterfallColor());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptedHeadwaterArrowsFollowOppositePlanVectors() {
|
||||
BufferedImage east = renderHeadwater(1, 0);
|
||||
BufferedImage west = renderHeadwater(-1, 0);
|
||||
int direction = IrisRenderer.headwaterDirectionColor();
|
||||
|
||||
assertEquals(direction, east.getRGB(4, 2));
|
||||
assertNotEquals(direction, west.getRGB(4, 2));
|
||||
assertEquals(direction, west.getRGB(2, 2));
|
||||
assertNotEquals(direction, east.getRGB(2, 2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void projectedCandidatesUseASeparateDiagnosticPalette() {
|
||||
assertNotEquals(
|
||||
IrisRenderer.hydrologyFeatureColor(HydrologyFeatureType.SURFACE_POOL),
|
||||
IrisRenderer.hydrologyDiagnosticColor(HydrologyCandidateKind.SOURCE)
|
||||
);
|
||||
assertNotEquals(
|
||||
IrisRenderer.hydrologyFeatureColor(HydrologyFeatureType.MOUTH),
|
||||
IrisRenderer.hydrologyDiagnosticColor(HydrologyCandidateKind.OUTLET)
|
||||
);
|
||||
assertNotEquals(
|
||||
IrisRenderer.hydrologyFeatureColor(HydrologyFeatureType.DEEP_POOL),
|
||||
IrisRenderer.hydrologyDiagnosticColor(HydrologyCandidateKind.DEEP_FLUID)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptedWaterfallTakesPrecedenceOverAnOverlappingPool() {
|
||||
HydrologyFeatureRef pool = feature(1L, HydrologyFeatureType.SURFACE_POOL, false);
|
||||
HydrologyFeatureRef waterfall = feature(2L, HydrologyFeatureType.WATERFALL, false);
|
||||
HydrologyRenderSample sample = new HydrologyRenderSample(0, 0, List.of(pool, waterfall));
|
||||
|
||||
assertEquals(IrisRenderer.waterfallColor(), IrisRenderer.hydrologyColor(sample));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptedSinkholeTakesPrecedenceOverItsReceivingGrotto() {
|
||||
HydrologyFeatureRef grotto = feature(1L, HydrologyFeatureType.INLAND_GROTTO, false);
|
||||
HydrologyFeatureRef sinkhole = feature(2L, HydrologyFeatureType.SINKHOLE, false);
|
||||
HydrologyRenderSample sample = new HydrologyRenderSample(0, 0, List.of(grotto, sinkhole));
|
||||
|
||||
assertEquals(IrisRenderer.hydrologyFeatureColor(HydrologyFeatureType.SINKHOLE),
|
||||
IrisRenderer.hydrologyColor(sample));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void riverRenderTypeSamplesOneAcceptedWorldFootprintPerRenderedPixel() {
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
IrisRiverRuntime runtime = mock(IrisRiverRuntime.class);
|
||||
RiverSample river = mock(RiverSample.class);
|
||||
|
||||
IrisHydrologyRuntime runtime = mock(IrisHydrologyRuntime.class);
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
when(complex.getRiverRuntime()).thenReturn(runtime);
|
||||
when(runtime.sampleFootprint(12D, -7D, 20D, 1D)).thenReturn(river);
|
||||
when(river.present()).thenReturn(true);
|
||||
when(river.section()).thenReturn(RiverSection.CHANNEL);
|
||||
when(complex.getHydrologyRuntime()).thenReturn(runtime);
|
||||
when(runtime.sampleRenderFootprint(12D, -7D, 20D, 1D))
|
||||
.thenReturn(sample(HydrologyFeatureType.SURFACE_POOL, false));
|
||||
|
||||
BufferedImage image = new IrisRenderer(engine).renderStudio(
|
||||
12D,
|
||||
@@ -59,26 +130,61 @@ public class IrisRendererRiverTest {
|
||||
);
|
||||
|
||||
assertEquals(new Color(48, 112, 190).getRGB(), image.getRGB(0, 0));
|
||||
verify(runtime).sampleFootprint(12D, -7D, 20D, 1D);
|
||||
verify(runtime).sampleRenderFootprint(12D, -7D, 20D, 1D);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void biomeAtlasCompositesRiverChannelsOverTheFastBaseBiome() {
|
||||
public void riverVisionShowsAProjectedCandidateWithoutTreatingItAsAccepted() {
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
IrisRiverRuntime runtime = mock(IrisRiverRuntime.class);
|
||||
RiverSample river = mock(RiverSample.class);
|
||||
IrisHydrologyRuntime runtime = mock(IrisHydrologyRuntime.class);
|
||||
HydrologyDiagnosticCandidate candidate = new HydrologyDiagnosticCandidate(
|
||||
44L,
|
||||
HydrologyCandidateKind.SOURCE,
|
||||
HydrologyFeatureType.SURFACE_POOL,
|
||||
new HydrologyPoint(0, 80, 0),
|
||||
HydrologyCandidateRejection.NO_LEGAL_OUTLET
|
||||
);
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
when(complex.getHydrologyRuntime()).thenReturn(runtime);
|
||||
when(runtime.sampleRenderFootprint(0D, 0D, 4D, 4D))
|
||||
.thenReturn(new HydrologyRenderSample(0, 0, List.of()));
|
||||
when(runtime.sampleDiagnosticFootprint(0D, 0D, 4D, 4D))
|
||||
.thenReturn(new HydrologyDiagnosticRenderSample(0, 0, List.of(candidate)));
|
||||
|
||||
BufferedImage image = new IrisRenderer(engine).renderStudio(
|
||||
0D,
|
||||
0D,
|
||||
4D,
|
||||
1,
|
||||
RenderType.RIVER,
|
||||
() -> false
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
IrisRenderer.hydrologyDiagnosticColor(HydrologyCandidateKind.SOURCE),
|
||||
image.getRGB(0, 0)
|
||||
);
|
||||
assertNotEquals(IrisRenderer.headwaterDirectionColor(), image.getRGB(0, 0));
|
||||
verify(runtime).sampleRenderFootprint(0D, 0D, 4D, 4D);
|
||||
verify(runtime).sampleDiagnosticFootprint(0D, 0D, 4D, 4D);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void biomeAtlasCompositesTheSameAcceptedFootprint() {
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
IrisHydrologyRuntime runtime = mock(IrisHydrologyRuntime.class);
|
||||
IrisBiome biome = mock(IrisBiome.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
ProceduralStream<IrisBiome> base = mock(ProceduralStream.class);
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
when(complex.getBaseBiomeStream()).thenReturn(base);
|
||||
when(complex.getRiverRuntime()).thenReturn(runtime);
|
||||
when(complex.getHydrologyRuntime()).thenReturn(runtime);
|
||||
when(base.get(0D, 0D)).thenReturn(biome);
|
||||
when(biome.getColor(engine, RenderType.BIOME)).thenReturn(Color.GREEN);
|
||||
when(runtime.sampleFootprint(0D, 0D, 4D, 4D)).thenReturn(river);
|
||||
when(river.present()).thenReturn(true);
|
||||
when(river.section()).thenReturn(RiverSection.CHANNEL);
|
||||
when(runtime.sampleRenderFootprint(0D, 0D, 4D, 4D))
|
||||
.thenReturn(sample(HydrologyFeatureType.WATERFALL, false));
|
||||
|
||||
BufferedImage image = new IrisRenderer(engine).renderStudio(
|
||||
0D,
|
||||
@@ -89,8 +195,60 @@ public class IrisRendererRiverTest {
|
||||
() -> false
|
||||
);
|
||||
|
||||
assertEquals(new Color(48, 112, 190).getRGB(), image.getRGB(0, 0));
|
||||
assertNotEquals(Color.GREEN.getRGB(), image.getRGB(0, 0));
|
||||
verify(base).get(0D, 0D);
|
||||
verify(runtime).sampleFootprint(0D, 0D, 4D, 4D);
|
||||
verify(runtime).sampleRenderFootprint(0D, 0D, 4D, 4D);
|
||||
verify(runtime, never()).sampleDiagnosticFootprint(anyDouble(), anyDouble(), anyDouble(), anyDouble());
|
||||
}
|
||||
|
||||
private static HydrologyRenderSample sample(HydrologyFeatureType type, boolean source) {
|
||||
return new HydrologyRenderSample(0, 0, List.of(feature(1L, type, source)));
|
||||
}
|
||||
|
||||
private static BufferedImage renderHeadwater(int flowX, int flowZ) {
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
IrisHydrologyRuntime runtime = mock(IrisHydrologyRuntime.class);
|
||||
HydrologyFeatureRef headwater = new HydrologyFeatureRef(
|
||||
71L,
|
||||
HydrologyFeatureType.SURFACE_POOL,
|
||||
10L,
|
||||
20L,
|
||||
3,
|
||||
64,
|
||||
3,
|
||||
flowX,
|
||||
flowZ,
|
||||
true
|
||||
);
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
when(complex.getHydrologyRuntime()).thenReturn(runtime);
|
||||
when(runtime.sampleRenderFootprint(anyDouble(), anyDouble(), anyDouble(), anyDouble()))
|
||||
.thenAnswer(invocation -> {
|
||||
double minimumX = invocation.getArgument(0, Double.class);
|
||||
double minimumZ = invocation.getArgument(1, Double.class);
|
||||
double maximumX = invocation.getArgument(2, Double.class);
|
||||
double maximumZ = invocation.getArgument(3, Double.class);
|
||||
boolean contains = minimumX <= headwater.x() && maximumX > headwater.x()
|
||||
&& minimumZ <= headwater.z() && maximumZ > headwater.z();
|
||||
return new HydrologyRenderSample(
|
||||
(int) StrictMath.floor(minimumX),
|
||||
(int) StrictMath.floor(minimumZ),
|
||||
contains ? List.of(headwater) : List.of()
|
||||
);
|
||||
});
|
||||
|
||||
return new IrisRenderer(engine).renderStudio(
|
||||
0D,
|
||||
0D,
|
||||
7D,
|
||||
7,
|
||||
RenderType.RIVER,
|
||||
() -> false
|
||||
);
|
||||
}
|
||||
|
||||
private static HydrologyFeatureRef feature(long id, HydrologyFeatureType type, boolean source) {
|
||||
return new HydrologyFeatureRef(id, type, 10L, 20L, 0, 64, 0, 1, 0, source);
|
||||
}
|
||||
}
|
||||
+1434
File diff suppressed because it is too large.
Load diff
+283
@@ -0,0 +1,283 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.cave.CavePosition;
|
||||
import art.arcane.iris.engine.hydrology.cave.CaveVoxel;
|
||||
import art.arcane.iris.engine.hydrology.cave.CaveVoxelPrecondition;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveAction;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveMode;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCavePlan;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveRejection;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveSource;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.OptionalLong;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class HydrologyCrossTileCaveAdmissionTest {
|
||||
@Test
|
||||
public void compatibleLowerRankClaimRetainsTheCurrentClaim() {
|
||||
CavePosition shared = position(4, 20, 7);
|
||||
HydrologyCrossTileCaveAdmission.Claim current = claim(
|
||||
11L,
|
||||
30,
|
||||
"water",
|
||||
Map.of(shared, HydrologyCaveAction.WET_SOURCE)
|
||||
);
|
||||
HydrologyCrossTileCaveAdmission.Claim blocker = claim(
|
||||
12L,
|
||||
10,
|
||||
"water",
|
||||
Map.of(shared, HydrologyCaveAction.WET_SOURCE)
|
||||
);
|
||||
|
||||
HydrologyCrossTileCaveAdmission.Result result = HydrologyCrossTileCaveAdmission.admit(
|
||||
4,
|
||||
List.of(current),
|
||||
List.of(ranked(0, 0, 1, blocker))
|
||||
);
|
||||
|
||||
assertTrue(result.rejections().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void actionConflictYieldsToTheLowerOwnerColorRank() {
|
||||
CavePosition shared = position(8, 24, 9);
|
||||
HydrologyCrossTileCaveAdmission.Claim current = claim(
|
||||
21L,
|
||||
40,
|
||||
"water",
|
||||
Map.of(shared, HydrologyCaveAction.WET_SOURCE)
|
||||
);
|
||||
HydrologyCrossTileCaveAdmission.Claim blocker = claim(
|
||||
22L,
|
||||
20,
|
||||
"water",
|
||||
Map.of(shared, HydrologyCaveAction.FALLING_FLUID)
|
||||
);
|
||||
|
||||
HydrologyCrossTileCaveAdmission.Result result = HydrologyCrossTileCaveAdmission.admit(
|
||||
4,
|
||||
List.of(current),
|
||||
List.of(ranked(0, 1, 1, blocker))
|
||||
);
|
||||
|
||||
assertEquals(Map.of(21L, 22L), result.loserCourseIdsToWinnerSourceIds());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void profileConflictYieldsToTheLowerOwnerColorRank() {
|
||||
CavePosition shared = position(12, 18, 13);
|
||||
HydrologyCrossTileCaveAdmission.Claim current = claim(
|
||||
31L,
|
||||
40,
|
||||
"water",
|
||||
Map.of(shared, HydrologyCaveAction.WET_SOURCE)
|
||||
);
|
||||
HydrologyCrossTileCaveAdmission.Claim blocker = claim(
|
||||
32L,
|
||||
20,
|
||||
"lava",
|
||||
Map.of(shared, HydrologyCaveAction.WET_SOURCE)
|
||||
);
|
||||
|
||||
HydrologyCrossTileCaveAdmission.Result result = HydrologyCrossTileCaveAdmission.admit(
|
||||
5,
|
||||
List.of(current),
|
||||
List.of(ranked(1, 0, 2, blocker))
|
||||
);
|
||||
|
||||
assertEquals(Map.of(31L, 32L), result.loserCourseIdsToWinnerSourceIds());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ownerColorRankPrecedesCaveSourceHeadPriority() {
|
||||
CavePosition shared = position(16, 28, 17);
|
||||
HydrologyCrossTileCaveAdmission.Claim higherHeadCurrent = claim(
|
||||
41L,
|
||||
90,
|
||||
"water",
|
||||
Map.of(shared, HydrologyCaveAction.WET_SOURCE)
|
||||
);
|
||||
HydrologyCrossTileCaveAdmission.Claim lowerHeadBlocker = claim(
|
||||
42L,
|
||||
10,
|
||||
"water",
|
||||
Map.of(shared, HydrologyCaveAction.DRY_AIR)
|
||||
);
|
||||
|
||||
HydrologyCrossTileCaveAdmission.Result result = HydrologyCrossTileCaveAdmission.admit(
|
||||
8,
|
||||
List.of(higherHeadCurrent),
|
||||
List.of(ranked(-1, -1, 7, lowerHeadBlocker))
|
||||
);
|
||||
|
||||
assertEquals(Map.of(41L, 42L), result.loserCourseIdsToWinnerSourceIds());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sourcePriorityBreaksTiesWithinOneBlockerRank() {
|
||||
CavePosition firstOverlap = position(20, 32, 21);
|
||||
CavePosition secondOverlap = position(24, 32, 25);
|
||||
HydrologyCrossTileCaveAdmission.Claim current = claim(
|
||||
51L,
|
||||
30,
|
||||
"water",
|
||||
Map.of(
|
||||
firstOverlap, HydrologyCaveAction.DRY_AIR,
|
||||
secondOverlap, HydrologyCaveAction.DRY_AIR
|
||||
)
|
||||
);
|
||||
HydrologyCrossTileCaveAdmission.Claim higherHead = claim(
|
||||
52L,
|
||||
40,
|
||||
"water",
|
||||
Map.of(firstOverlap, HydrologyCaveAction.WET_SOURCE)
|
||||
);
|
||||
HydrologyCrossTileCaveAdmission.Claim lowerHead = claim(
|
||||
53L,
|
||||
20,
|
||||
"water",
|
||||
Map.of(secondOverlap, HydrologyCaveAction.WET_SOURCE)
|
||||
);
|
||||
|
||||
HydrologyCrossTileCaveAdmission.Result result = HydrologyCrossTileCaveAdmission.admit(
|
||||
6,
|
||||
List.of(current),
|
||||
List.of(
|
||||
ranked(1, 1, 2, lowerHead),
|
||||
ranked(1, 1, 2, higherHead)
|
||||
)
|
||||
);
|
||||
|
||||
assertEquals(Map.of(51L, 52L), result.loserCourseIdsToWinnerSourceIds());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void negativeOwnersAndInputOrderProduceTheSameDeterministicResult() {
|
||||
CavePosition first = position(-257, 22, -513);
|
||||
CavePosition second = position(-300, 22, -520);
|
||||
HydrologyCrossTileCaveAdmission.Claim currentOne = claim(
|
||||
61L,
|
||||
30,
|
||||
"water",
|
||||
Map.of(first, HydrologyCaveAction.WET_SOURCE)
|
||||
);
|
||||
HydrologyCrossTileCaveAdmission.Claim currentTwo = claim(
|
||||
62L,
|
||||
20,
|
||||
"water",
|
||||
Map.of(second, HydrologyCaveAction.WET_SOURCE)
|
||||
);
|
||||
HydrologyCrossTileCaveAdmission.RankedClaim firstBlocker = ranked(
|
||||
-2,
|
||||
-3,
|
||||
1,
|
||||
claim(63L, 10, "water", Map.of(first, HydrologyCaveAction.DRY_AIR))
|
||||
);
|
||||
HydrologyCrossTileCaveAdmission.RankedClaim secondBlocker = ranked(
|
||||
-1,
|
||||
-2,
|
||||
2,
|
||||
claim(64L, 10, "water", Map.of(second, HydrologyCaveAction.DRY_AIR))
|
||||
);
|
||||
ArrayList<HydrologyCrossTileCaveAdmission.Claim> reverseClaims = new ArrayList<>(List.of(
|
||||
currentOne,
|
||||
currentTwo
|
||||
));
|
||||
Collections.reverse(reverseClaims);
|
||||
ArrayList<HydrologyCrossTileCaveAdmission.RankedClaim> reverseBlockers = new ArrayList<>(List.of(
|
||||
firstBlocker,
|
||||
secondBlocker
|
||||
));
|
||||
Collections.reverse(reverseBlockers);
|
||||
|
||||
HydrologyCrossTileCaveAdmission.Result forward = HydrologyCrossTileCaveAdmission.admit(
|
||||
5,
|
||||
List.of(currentOne, currentTwo),
|
||||
List.of(firstBlocker, secondBlocker)
|
||||
);
|
||||
HydrologyCrossTileCaveAdmission.Result reverse = HydrologyCrossTileCaveAdmission.admit(
|
||||
5,
|
||||
reverseClaims,
|
||||
reverseBlockers
|
||||
);
|
||||
|
||||
assertEquals(Map.of(61L, 63L, 62L, 64L), forward.loserCourseIdsToWinnerSourceIds());
|
||||
assertEquals(forward, reverse);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sameOrHigherRankClaimsCannotBeUsedAsBlockers() {
|
||||
HydrologyCrossTileCaveAdmission.Claim current = claim(
|
||||
71L,
|
||||
30,
|
||||
"water",
|
||||
Map.of(position(1, 20, 1), HydrologyCaveAction.WET_SOURCE)
|
||||
);
|
||||
HydrologyCrossTileCaveAdmission.RankedClaim blocker = ranked(
|
||||
1,
|
||||
1,
|
||||
3,
|
||||
claim(72L, 20, "water", Map.of(position(1, 20, 1), HydrologyCaveAction.DRY_AIR))
|
||||
);
|
||||
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> HydrologyCrossTileCaveAdmission.admit(3, List.of(current), List.of(blocker))
|
||||
);
|
||||
}
|
||||
|
||||
private HydrologyCrossTileCaveAdmission.RankedClaim ranked(
|
||||
int tileX,
|
||||
int tileZ,
|
||||
int rank,
|
||||
HydrologyCrossTileCaveAdmission.Claim claim
|
||||
) {
|
||||
return new HydrologyCrossTileCaveAdmission.RankedClaim(
|
||||
new HydrologyTileKey(tileX, tileZ),
|
||||
rank,
|
||||
claim
|
||||
);
|
||||
}
|
||||
|
||||
private HydrologyCrossTileCaveAdmission.Claim claim(
|
||||
long sourceId,
|
||||
int waterHeadY,
|
||||
String profileKey,
|
||||
Map<CavePosition, HydrologyCaveAction> actions
|
||||
) {
|
||||
CavePosition entry = position((int) sourceId, waterHeadY + 4, 0);
|
||||
CavePosition target = position((int) sourceId, waterHeadY, 0);
|
||||
HydrologyCaveSource source = new HydrologyCaveSource(
|
||||
sourceId,
|
||||
entry,
|
||||
target,
|
||||
waterHeadY,
|
||||
HydrologyCaveMode.CLOSED_COMPONENT
|
||||
);
|
||||
LinkedHashMap<CavePosition, CaveVoxelPrecondition> preconditions = new LinkedHashMap<>();
|
||||
for (CavePosition position : actions.keySet()) {
|
||||
preconditions.put(position, new CaveVoxelPrecondition(CaveVoxel.SOLID, false));
|
||||
}
|
||||
HydrologyCavePlan plan = new HydrologyCavePlan(
|
||||
source,
|
||||
HydrologyCaveRejection.NONE,
|
||||
actions,
|
||||
preconditions,
|
||||
OptionalLong.empty()
|
||||
);
|
||||
return new HydrologyCrossTileCaveAdmission.Claim(profileKey, plan);
|
||||
}
|
||||
|
||||
private CavePosition position(int x, int y, int z) {
|
||||
return new CavePosition(x, y, z);
|
||||
}
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class HydrologyCrossTileSurfaceAdmissionTest {
|
||||
@Test
|
||||
public void rejectsClusteredIndependentMouths() {
|
||||
HydrologyCrossTileSurfaceAdmission.Claim current = claim(
|
||||
10L,
|
||||
100L,
|
||||
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(100, 63, 0)),
|
||||
true
|
||||
);
|
||||
HydrologyCrossTileSurfaceAdmission.Claim blocker = claim(
|
||||
20L,
|
||||
200L,
|
||||
List.of(new HydrologyPoint(0, 70, 48), new HydrologyPoint(112, 63, 24)),
|
||||
true
|
||||
);
|
||||
|
||||
HydrologyCrossTileSurfaceAdmission.Result result = HydrologyCrossTileSurfaceAdmission.admit(
|
||||
List.of(current),
|
||||
List.of(new HydrologyCrossTileSurfaceAdmission.RankedClaim(
|
||||
new HydrologyTileKey(0, 0),
|
||||
0,
|
||||
blocker
|
||||
)),
|
||||
128
|
||||
);
|
||||
|
||||
assertEquals(1, result.rejections().size());
|
||||
assertEquals(current.courseId(), result.rejections().getFirst().loser().courseId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsCrossingUnrelatedCourses() {
|
||||
HydrologyCrossTileSurfaceAdmission.Claim current = claim(
|
||||
10L,
|
||||
100L,
|
||||
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(100, 65, 100)),
|
||||
false
|
||||
);
|
||||
HydrologyCrossTileSurfaceAdmission.Claim blocker = claim(
|
||||
20L,
|
||||
200L,
|
||||
List.of(new HydrologyPoint(0, 70, 100), new HydrologyPoint(100, 65, 0)),
|
||||
false
|
||||
);
|
||||
|
||||
HydrologyCrossTileSurfaceAdmission.Result result = HydrologyCrossTileSurfaceAdmission.admit(
|
||||
List.of(current),
|
||||
List.of(new HydrologyCrossTileSurfaceAdmission.RankedClaim(
|
||||
new HydrologyTileKey(0, 0),
|
||||
0,
|
||||
blocker
|
||||
)),
|
||||
64
|
||||
);
|
||||
|
||||
assertEquals(1, result.rejections().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsNearbySourcesWhenTheirCoursesImmediatelyDiverge() {
|
||||
HydrologyCrossTileSurfaceAdmission.Claim current = claim(
|
||||
10L,
|
||||
100L,
|
||||
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(512, 63, 0)),
|
||||
true
|
||||
);
|
||||
HydrologyCrossTileSurfaceAdmission.Claim blocker = claim(
|
||||
20L,
|
||||
200L,
|
||||
List.of(new HydrologyPoint(0, 70, 128), new HydrologyPoint(-512, 63, 128)),
|
||||
true
|
||||
);
|
||||
|
||||
HydrologyCrossTileSurfaceAdmission.Result result = HydrologyCrossTileSurfaceAdmission.admit(
|
||||
List.of(current),
|
||||
List.of(new HydrologyCrossTileSurfaceAdmission.RankedClaim(
|
||||
new HydrologyTileKey(0, 0),
|
||||
0,
|
||||
blocker
|
||||
)),
|
||||
192
|
||||
);
|
||||
|
||||
assertEquals(1, result.rejections().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void keepsSeparatedNetworks() {
|
||||
HydrologyCrossTileSurfaceAdmission.Claim current = claim(
|
||||
10L,
|
||||
100L,
|
||||
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(100, 63, 0)),
|
||||
true
|
||||
);
|
||||
HydrologyCrossTileSurfaceAdmission.Claim blocker = claim(
|
||||
20L,
|
||||
200L,
|
||||
List.of(new HydrologyPoint(0, 70, 256), new HydrologyPoint(100, 63, 256)),
|
||||
true
|
||||
);
|
||||
|
||||
HydrologyCrossTileSurfaceAdmission.Result result = HydrologyCrossTileSurfaceAdmission.admit(
|
||||
List.of(current),
|
||||
List.of(new HydrologyCrossTileSurfaceAdmission.RankedClaim(
|
||||
new HydrologyTileKey(0, 0),
|
||||
0,
|
||||
blocker
|
||||
)),
|
||||
128
|
||||
);
|
||||
|
||||
assertTrue(result.rejections().isEmpty());
|
||||
}
|
||||
|
||||
private HydrologyCrossTileSurfaceAdmission.Claim claim(
|
||||
long courseId,
|
||||
long outletId,
|
||||
List<HydrologyPoint> centerline,
|
||||
boolean reachesOutlet
|
||||
) {
|
||||
return new HydrologyCrossTileSurfaceAdmission.Claim(
|
||||
courseId,
|
||||
outletId,
|
||||
centerline.getLast(),
|
||||
reachesOutlet,
|
||||
4,
|
||||
centerline
|
||||
);
|
||||
}
|
||||
}
|
||||
+2226
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,718 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.cave.CavePosition;
|
||||
import art.arcane.iris.engine.hydrology.cave.CaveVoxel;
|
||||
import art.arcane.iris.engine.hydrology.cave.CaveVoxelPrecondition;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveAction;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveMode;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCavePlan;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveRejection;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveSource;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.OptionalLong;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class HydrologyModelTest {
|
||||
@Test
|
||||
public void footprintAndColumnCollectionsAreImmutableCopies() {
|
||||
HydrologyFeatureRef feature = feature(HydrologyFeatureType.SURFACE_POOL, 10L, 70);
|
||||
ArrayList<HydrologyColumnLayer> mutableLayers = new ArrayList<>();
|
||||
mutableLayers.add(layer(feature, 67, 70, 70, true, false, false));
|
||||
HydrologyColumnSample column = new HydrologyColumnSample(4, 9, 74, 63, false, "parent", mutableLayers);
|
||||
HashMap<Long, HydrologyColumnSample> mutableColumns = new HashMap<>();
|
||||
mutableColumns.put(RiverFootprint.pack(4, 9), column);
|
||||
RiverFootprint footprint = new RiverFootprint(mutableColumns);
|
||||
|
||||
mutableLayers.clear();
|
||||
mutableColumns.clear();
|
||||
|
||||
assertEquals(1, column.layers().size());
|
||||
assertEquals(1, footprint.size());
|
||||
assertThrows(UnsupportedOperationException.class, () -> column.layers().add(layer(
|
||||
feature,
|
||||
67,
|
||||
70,
|
||||
70,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
)));
|
||||
assertThrows(UnsupportedOperationException.class, () -> footprint.columns().clear());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void positiveHeadTransitionsCanShareOneReceivingPoolAcrossAComplex() {
|
||||
List<HydrologyPoint> centerline = List.of(
|
||||
new HydrologyPoint(0, 80, 0),
|
||||
new HydrologyPoint(1, 74, 0)
|
||||
);
|
||||
|
||||
HydraulicSegment graded = new HydraulicSegment(
|
||||
1L,
|
||||
2L,
|
||||
HydrologyFeatureType.WATERFALL,
|
||||
80,
|
||||
74,
|
||||
4,
|
||||
2,
|
||||
false,
|
||||
true,
|
||||
centerline
|
||||
);
|
||||
assertEquals(6, graded.drop());
|
||||
assertFalse(graded.fallingFluid());
|
||||
HydraulicSegment intermediate = new HydraulicSegment(
|
||||
1L,
|
||||
2L,
|
||||
HydrologyFeatureType.WATERFALL,
|
||||
80,
|
||||
74,
|
||||
4,
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
centerline
|
||||
);
|
||||
assertFalse(intermediate.receivingPool());
|
||||
HydraulicSegment accepted = new HydraulicSegment(
|
||||
1L,
|
||||
2L,
|
||||
HydrologyFeatureType.WATERFALL,
|
||||
80,
|
||||
74,
|
||||
4,
|
||||
2,
|
||||
true,
|
||||
true,
|
||||
centerline
|
||||
);
|
||||
assertEquals(6, accepted.drop());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void surfaceInlandGrottoRequiresOneExplicitSinkholeLinkInTheSameCourse() {
|
||||
HydraulicSegment lip = new HydraulicSegment(
|
||||
1L,
|
||||
9L,
|
||||
HydrologyFeatureType.SURFACE_POOL,
|
||||
80,
|
||||
80,
|
||||
4,
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(0, 80, 0))
|
||||
);
|
||||
HydraulicSegment sinkhole = new HydraulicSegment(
|
||||
2L,
|
||||
9L,
|
||||
HydrologyFeatureType.SINKHOLE,
|
||||
80,
|
||||
74,
|
||||
4,
|
||||
2,
|
||||
true,
|
||||
true,
|
||||
List.of(new HydrologyPoint(0, 80, 0), new HydrologyPoint(1, 74, 0))
|
||||
);
|
||||
HydraulicSegment grotto = new HydraulicSegment(
|
||||
3L,
|
||||
9L,
|
||||
HydrologyFeatureType.INLAND_GROTTO,
|
||||
74,
|
||||
74,
|
||||
4,
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(1, 74, 0))
|
||||
);
|
||||
|
||||
RiverCourse accepted = new RiverCourse(
|
||||
9L,
|
||||
RiverCourseType.SURFACE,
|
||||
OptionalLong.of(4L),
|
||||
OptionalLong.of(5L),
|
||||
"water",
|
||||
1,
|
||||
List.of(),
|
||||
List.of(lip, sinkhole, grotto)
|
||||
);
|
||||
|
||||
assertTrue(accepted.surfaceSinkholeContinuation());
|
||||
HydraulicSegment containedLip = new HydraulicSegment(
|
||||
4L,
|
||||
9L,
|
||||
HydrologyFeatureType.RIDGE_BORE,
|
||||
80,
|
||||
80,
|
||||
4,
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
List.of(new HydrologyPoint(0, 80, 0))
|
||||
);
|
||||
RiverCourse containedApproach = new RiverCourse(
|
||||
9L,
|
||||
RiverCourseType.SURFACE,
|
||||
OptionalLong.of(4L),
|
||||
OptionalLong.of(5L),
|
||||
"water",
|
||||
1,
|
||||
List.of(),
|
||||
List.of(containedLip, sinkhole, grotto)
|
||||
);
|
||||
assertTrue(containedApproach.surfaceSinkholeContinuation());
|
||||
assertThrows(IllegalArgumentException.class, () -> new RiverCourse(
|
||||
9L,
|
||||
RiverCourseType.SURFACE,
|
||||
OptionalLong.of(4L),
|
||||
OptionalLong.of(5L),
|
||||
"water",
|
||||
1,
|
||||
List.of(),
|
||||
List.of(lip, grotto)
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void oceanColumnsRejectOwnedOrElevatedWrites() {
|
||||
HydrologyFeatureRef feature = feature(HydrologyFeatureType.MOUTH, 7L, 63);
|
||||
HydrologyColumnLayer apron = layer(feature, 63, 63, 63, true, false, true);
|
||||
HydrologyColumnSample accepted = new HydrologyColumnSample(
|
||||
4,
|
||||
9,
|
||||
50,
|
||||
63,
|
||||
true,
|
||||
"ocean_parent",
|
||||
List.of(apron)
|
||||
);
|
||||
assertTrue(accepted.layers().getFirst().oceanApron());
|
||||
|
||||
HydrologyColumnLayer owned = layer(feature, 61, 63, 63, true, true, false);
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyColumnSample(
|
||||
4,
|
||||
9,
|
||||
50,
|
||||
63,
|
||||
true,
|
||||
"ocean_parent",
|
||||
List.of(owned)
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void submergedNonOceanColumnsRejectSurfaceOwnershipButAllowSubterrainHydrology() {
|
||||
HydrologyColumnLayer surface = layer(
|
||||
feature(HydrologyFeatureType.SURFACE_POOL, 8L, 63),
|
||||
60,
|
||||
63,
|
||||
63,
|
||||
true,
|
||||
true,
|
||||
false
|
||||
);
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyColumnSample(
|
||||
4,
|
||||
9,
|
||||
63,
|
||||
63,
|
||||
false,
|
||||
"shore_parent",
|
||||
List.of(surface)
|
||||
));
|
||||
|
||||
HydrologyColumnSample raised = new HydrologyColumnSample(
|
||||
4,
|
||||
9,
|
||||
64,
|
||||
63,
|
||||
false,
|
||||
"shore_parent",
|
||||
List.of(surface)
|
||||
);
|
||||
assertTrue(raised.primarySurfaceFluidLayer().isPresent());
|
||||
|
||||
HydrologyColumnLayer underground = layer(
|
||||
feature(HydrologyFeatureType.UNDERGROUND_POOL, 9L, 40),
|
||||
36,
|
||||
40,
|
||||
45,
|
||||
true,
|
||||
true,
|
||||
false
|
||||
);
|
||||
HydrologyColumnSample subterrain = new HydrologyColumnSample(
|
||||
4,
|
||||
9,
|
||||
50,
|
||||
63,
|
||||
false,
|
||||
"shore_parent",
|
||||
List.of(underground)
|
||||
);
|
||||
assertTrue(subterrain.hasFeature(HydrologyFeatureType.UNDERGROUND_POOL));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void outerGradingUsesTheExactParentBiome() {
|
||||
HydrologyFeatureRef feature = feature(HydrologyFeatureType.SURFACE_POOL, 3L, 70);
|
||||
HydrologyColumnLayer grading = layer(feature, 72, 72, 72, false, false, false);
|
||||
grading = new HydrologyColumnLayer(
|
||||
grading.feature(),
|
||||
grading.bedY(),
|
||||
grading.fluidHeadY(),
|
||||
grading.ceilingY(),
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
"alpha",
|
||||
"surface",
|
||||
"mouth",
|
||||
"shore",
|
||||
"dry",
|
||||
"flooded"
|
||||
);
|
||||
HydrologyColumnSample sample = new HydrologyColumnSample(0, 0, 74, 63, false, "exact_parent", List.of(grading));
|
||||
|
||||
assertTrue(grading.biomeKey() == null);
|
||||
assertEquals("exact_parent", sample.parentBiomeKey());
|
||||
assertFalse(sample.hasConnectedFluid());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void overlappingDrySurfaceGradesKeepTheHighestContainedBank() {
|
||||
HydrologyColumnLayer lowerCascadeGrade = surfaceLayer(
|
||||
feature(HydrologyFeatureType.CASCADE, 30L, 70),
|
||||
70,
|
||||
70,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false
|
||||
);
|
||||
HydrologyColumnLayer higherPoolGrade = surfaceLayer(
|
||||
feature(HydrologyFeatureType.SURFACE_POOL, 31L, 72),
|
||||
72,
|
||||
72,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false
|
||||
);
|
||||
HydrologyColumnSample sample = new HydrologyColumnSample(
|
||||
0,
|
||||
0,
|
||||
75,
|
||||
63,
|
||||
false,
|
||||
"parent",
|
||||
List.of(lowerCascadeGrade, higherPoolGrade)
|
||||
);
|
||||
|
||||
assertEquals(72, sample.terrainHeight());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptedSurfaceArbitrationPrefersChannelThenHydraulicType() {
|
||||
HydrologyColumnLayer grading = surfaceLayer(
|
||||
feature(HydrologyFeatureType.WATERFALL, 4L, 78),
|
||||
73,
|
||||
78,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false
|
||||
);
|
||||
HydrologyColumnLayer pool = surfaceLayer(
|
||||
feature(HydrologyFeatureType.SURFACE_POOL, 5L, 74),
|
||||
70,
|
||||
74,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
);
|
||||
HydrologyColumnLayer waterfall = surfaceLayer(
|
||||
feature(HydrologyFeatureType.WATERFALL, 6L, 72),
|
||||
66,
|
||||
72,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
);
|
||||
HydrologyColumnSample sample = new HydrologyColumnSample(
|
||||
4,
|
||||
9,
|
||||
80,
|
||||
63,
|
||||
false,
|
||||
"parent",
|
||||
List.of(grading, pool, waterfall)
|
||||
);
|
||||
|
||||
assertEquals(waterfall, sample.primarySurfaceLayer().orElseThrow());
|
||||
assertEquals(waterfall, sample.primarySurfaceFluidLayer().orElseThrow());
|
||||
assertEquals(66, sample.terrainHeight());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void oceanApronDoesNotOwnSurfaceArbitration() {
|
||||
HydrologyFeatureRef feature = feature(HydrologyFeatureType.MOUTH, 8L, 63);
|
||||
HydrologyColumnLayer apron = layer(feature, 63, 63, 63, true, false, true);
|
||||
HydrologyColumnSample sample = new HydrologyColumnSample(
|
||||
4,
|
||||
9,
|
||||
52,
|
||||
63,
|
||||
true,
|
||||
"ocean_parent",
|
||||
List.of(apron)
|
||||
);
|
||||
|
||||
assertTrue(sample.primarySurfaceLayer().isEmpty());
|
||||
assertTrue(sample.primarySurfaceFluidLayer().isEmpty());
|
||||
assertEquals(52, sample.terrainHeight());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rendererClassificationIsExactlyTheAcceptedColumnClassification() {
|
||||
HydrologyColumnLayer pool = surfaceLayer(
|
||||
feature(HydrologyFeatureType.SURFACE_POOL, 10L, 70),
|
||||
67,
|
||||
70,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
);
|
||||
HydrologyColumnLayer waterfall = surfaceLayer(
|
||||
feature(HydrologyFeatureType.WATERFALL, 11L, 76),
|
||||
64,
|
||||
76,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
);
|
||||
HydrologyColumnSample sample = new HydrologyColumnSample(
|
||||
4,
|
||||
9,
|
||||
80,
|
||||
63,
|
||||
false,
|
||||
"parent",
|
||||
List.of(pool, waterfall)
|
||||
);
|
||||
|
||||
HydrologyRenderSample render = sample.renderSample();
|
||||
assertEquals(2, render.features().size());
|
||||
assertEquals(HydrologyFeatureType.WATERFALL, render.primaryFeature().orElseThrow().type());
|
||||
assertTrue(render.hasFeature(HydrologyFeatureType.SURFACE_POOL));
|
||||
assertTrue(render.hasFeature(HydrologyFeatureType.WATERFALL));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void locatorIndexKeepsOnePublishedRepresentativePerSegment() {
|
||||
HydrologyFeatureRef gradingFeature = new HydrologyFeatureRef(
|
||||
10L, HydrologyFeatureType.WATERFALL, 1L, 100L, 0, 74, 0, 1, 0, false
|
||||
);
|
||||
HydrologyFeatureRef fallingFeature = new HydrologyFeatureRef(
|
||||
11L, HydrologyFeatureType.WATERFALL, 1L, 100L, 1, 74, 0, 1, 0, false
|
||||
);
|
||||
HydrologyFeatureRef poolFeature = new HydrologyFeatureRef(
|
||||
12L, HydrologyFeatureType.SURFACE_POOL, 1L, 101L, 2, 70, 0, 1, 0, false
|
||||
);
|
||||
HydrologyColumnLayer grading = surfaceLayer(
|
||||
gradingFeature, 74, 74, false, false, true, true, false
|
||||
);
|
||||
HydrologyColumnLayer falling = new HydrologyColumnLayer(
|
||||
fallingFeature,
|
||||
68,
|
||||
74,
|
||||
74,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
"alpha",
|
||||
"surface",
|
||||
"mouth",
|
||||
"shore",
|
||||
"dry",
|
||||
"flooded"
|
||||
);
|
||||
HydrologyColumnLayer pool = surfaceLayer(
|
||||
poolFeature, 67, 70, true, false, false, true, true
|
||||
);
|
||||
Map<Long, HydrologyColumnSample> columns = Map.of(
|
||||
RiverFootprint.pack(0, 0), new HydrologyColumnSample(0, 0, 80, 63, false, "parent", List.of(grading)),
|
||||
RiverFootprint.pack(1, 0), new HydrologyColumnSample(1, 0, 80, 63, false, "parent", List.of(falling)),
|
||||
RiverFootprint.pack(2, 0), new HydrologyColumnSample(2, 0, 80, 63, false, "parent", List.of(pool))
|
||||
);
|
||||
RiverFootprint footprint = new RiverFootprint(columns);
|
||||
|
||||
List<HydrologyFeatureRef> features = HydrologyTile.collectFeatures(footprint);
|
||||
|
||||
assertEquals(2, features.size());
|
||||
assertTrue(features.contains(fallingFeature));
|
||||
assertTrue(features.contains(poolFeature));
|
||||
for (HydrologyFeatureRef feature : features) {
|
||||
assertTrue(footprint.sample(feature.x(), feature.z()).orElseThrow().layers().stream()
|
||||
.anyMatch(layer -> layer.feature().id() == feature.id()));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void featureQuerySuggestionsIncludeConfiguredDeepFluidsWithoutHardcodedProfiles() {
|
||||
List<String> suggestions = HydrologyFeatureQuery.suggestions(
|
||||
List.of("acid", "deep_lava", "acid", "surface", "ridge-tunnel")
|
||||
);
|
||||
|
||||
assertEquals(List.of(
|
||||
"surface",
|
||||
"waterfall",
|
||||
"sinkhole",
|
||||
"underground",
|
||||
"grotto",
|
||||
"coastal_grotto",
|
||||
"inland_grotto",
|
||||
"mouth",
|
||||
"ridge_tunnel",
|
||||
"deep",
|
||||
"acid",
|
||||
"deep_lava"
|
||||
), suggestions);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void featureQueryReservesEveryBuiltInKeywordAndEquivalentHyphenSpelling() {
|
||||
List<String> builtInKeywords = List.of(
|
||||
"surface",
|
||||
"waterfall",
|
||||
"sinkhole",
|
||||
"underground",
|
||||
"grotto",
|
||||
"coastal_grotto",
|
||||
"inland_grotto",
|
||||
"mouth",
|
||||
"ridge_tunnel",
|
||||
"deep"
|
||||
);
|
||||
|
||||
assertEquals(builtInKeywords, HydrologyFeatureQuery.suggestions(List.of()));
|
||||
for (String keyword : builtInKeywords) {
|
||||
assertTrue(keyword, HydrologyFeatureQuery.isReservedKeyword(keyword));
|
||||
assertTrue(keyword, HydrologyFeatureQuery.isReservedKeyword(keyword.replace('_', '-')));
|
||||
}
|
||||
assertFalse(HydrologyFeatureQuery.isReservedKeyword("acid"));
|
||||
assertFalse(HydrologyFeatureQuery.isReservedKeyword(null));
|
||||
assertEquals(Set.of(HydrologyFeatureType.SINKHOLE), HydrologyFeatureQuery.parse("sinkhole").types());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void compactLocatorIndexFindsThePublishedEndOfALongSegment() {
|
||||
HydraulicSegment segment = new HydraulicSegment(
|
||||
200L,
|
||||
201L,
|
||||
HydrologyFeatureType.DEEP_CHANNEL,
|
||||
20,
|
||||
20,
|
||||
4,
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
List.of(
|
||||
new HydrologyPoint(0, 20, 0),
|
||||
new HydrologyPoint(500, 20, 0),
|
||||
new HydrologyPoint(1000, 20, 0)
|
||||
)
|
||||
);
|
||||
RiverCourse course = new RiverCourse(
|
||||
201L,
|
||||
RiverCourseType.DEEP_FLUID,
|
||||
OptionalLong.empty(),
|
||||
OptionalLong.empty(),
|
||||
"acid",
|
||||
1,
|
||||
List.of(),
|
||||
List.of(segment)
|
||||
);
|
||||
HydrologyFeatureRef endpoint = new HydrologyFeatureRef(
|
||||
202L,
|
||||
HydrologyFeatureType.DEEP_CHANNEL,
|
||||
course.id(),
|
||||
segment.id(),
|
||||
1000,
|
||||
20,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
false
|
||||
);
|
||||
HydrologyColumnLayer layer = new HydrologyColumnLayer(
|
||||
endpoint,
|
||||
18,
|
||||
20,
|
||||
24,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
"acid",
|
||||
"surface",
|
||||
"mouth",
|
||||
"shore",
|
||||
"dry",
|
||||
"flooded"
|
||||
);
|
||||
HydrologyColumnSample column = new HydrologyColumnSample(
|
||||
1000, 0, 80, 63, false, "parent", List.of(layer)
|
||||
);
|
||||
HashMap<CavePosition, HydrologyCaveAction> actions = new HashMap<>();
|
||||
HashMap<CavePosition, CaveVoxelPrecondition> preconditions = new HashMap<>();
|
||||
for (int y = layer.bedY() + 1; y <= layer.ceilingY(); y++) {
|
||||
CavePosition position = new CavePosition(column.x(), y, column.z());
|
||||
actions.put(
|
||||
position,
|
||||
y > layer.fluidHeadY() ? HydrologyCaveAction.DRY_AIR : HydrologyCaveAction.WET_SOURCE
|
||||
);
|
||||
preconditions.put(position, new CaveVoxelPrecondition(CaveVoxel.SOLID, false));
|
||||
}
|
||||
HydrologyCavePlan plan = new HydrologyCavePlan(
|
||||
new HydrologyCaveSource(
|
||||
course.id(),
|
||||
new CavePosition(column.x(), layer.ceilingY(), column.z()),
|
||||
new CavePosition(column.x(), layer.fluidHeadY(), column.z()),
|
||||
layer.fluidHeadY(),
|
||||
HydrologyCaveMode.GENERATED_GROTTO
|
||||
),
|
||||
HydrologyCaveRejection.NONE,
|
||||
actions,
|
||||
preconditions,
|
||||
OptionalLong.empty()
|
||||
);
|
||||
HydrologyTile tile = new HydrologyTile(
|
||||
new HydrologyTileKey(0, 0),
|
||||
1L,
|
||||
2L,
|
||||
2048,
|
||||
List.of(),
|
||||
List.of(),
|
||||
List.of(),
|
||||
List.of(course),
|
||||
List.of(plan),
|
||||
List.of(),
|
||||
new RiverFootprint(Map.of(RiverFootprint.pack(1000, 0), column))
|
||||
);
|
||||
|
||||
assertEquals(endpoint, tile.nearestFeature(
|
||||
Set.of(HydrologyFeatureType.DEEP_CHANNEL), "acid", 1000, 0, 10).orElseThrow());
|
||||
assertTrue(tile.nearestFeature(
|
||||
Set.of(HydrologyFeatureType.DEEP_CHANNEL), "deep_lava", 1000, 0, 10).isEmpty());
|
||||
}
|
||||
|
||||
private HydrologyFeatureRef feature(HydrologyFeatureType type, long id, int y) {
|
||||
return new HydrologyFeatureRef(id, type, 1L, 2L, 4, y, 9, 1, 0, false);
|
||||
}
|
||||
|
||||
private HydrologyColumnLayer layer(
|
||||
HydrologyFeatureRef feature,
|
||||
int bed,
|
||||
int head,
|
||||
int ceiling,
|
||||
boolean channel,
|
||||
boolean owned,
|
||||
boolean apron
|
||||
) {
|
||||
return new HydrologyColumnLayer(
|
||||
feature,
|
||||
bed,
|
||||
head,
|
||||
ceiling,
|
||||
channel,
|
||||
false,
|
||||
false,
|
||||
channel,
|
||||
false,
|
||||
false,
|
||||
owned,
|
||||
owned,
|
||||
apron,
|
||||
"alpha",
|
||||
"surface",
|
||||
"mouth",
|
||||
"shore",
|
||||
"dry",
|
||||
"flooded"
|
||||
);
|
||||
}
|
||||
|
||||
private HydrologyColumnLayer surfaceLayer(
|
||||
HydrologyFeatureRef feature,
|
||||
int bed,
|
||||
int head,
|
||||
boolean channel,
|
||||
boolean shore,
|
||||
boolean grading,
|
||||
boolean terrainOwned,
|
||||
boolean fluidOwned
|
||||
) {
|
||||
return new HydrologyColumnLayer(
|
||||
feature,
|
||||
bed,
|
||||
head,
|
||||
head,
|
||||
channel,
|
||||
shore,
|
||||
grading,
|
||||
fluidOwned,
|
||||
false,
|
||||
false,
|
||||
terrainOwned,
|
||||
fluidOwned,
|
||||
false,
|
||||
"alpha",
|
||||
"surface",
|
||||
"mouth",
|
||||
"shore",
|
||||
"dry",
|
||||
"flooded"
|
||||
);
|
||||
}
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
|
||||
public class HydrologyOceanBoundaryRefinerTest {
|
||||
@Test
|
||||
public void firstOceanUsesOneFullSampleAtTheImmediatelyPreviousLandPoint() {
|
||||
List<HydrologyPoint> probes = new ArrayList<>();
|
||||
List<HydrologyPoint> detailSamples = new ArrayList<>();
|
||||
List<HydrologyPoint> crossing = crossing(0, 4, -7);
|
||||
HydrologyTerrainSample land = HydrologyTerrainSample.openLand(91, 5D, "parent");
|
||||
HydrologyRoutingTerrainSampler routingSampler = routingSampler(
|
||||
probes,
|
||||
3,
|
||||
HydrologyRoutingTerrainSampler.NaturalClassification.OCEAN
|
||||
);
|
||||
HydrologyTerrainSampler terrainSampler = (int x, int z) -> {
|
||||
detailSamples.add(new HydrologyPoint(x, 0, z));
|
||||
return land;
|
||||
};
|
||||
|
||||
HydrologyOceanBoundaryRefiner.Result result = HydrologyOceanBoundaryRefiner.refine(
|
||||
crossing,
|
||||
terrainSampler,
|
||||
routingSampler
|
||||
);
|
||||
|
||||
assertEquals(List.of(point(1, -7), point(2, -7), point(3, -7)), probes);
|
||||
assertEquals(List.of(point(2, -7)), detailSamples);
|
||||
assertEquals(new HydrologyPoint(2, 90, -7), result.landwardPoint());
|
||||
assertEquals(new HydrologyPoint(3, 90, -7), result.oceanPoint());
|
||||
assertEquals(land, result.landwardTerrain());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void immediateOceanResamplesTheStartingLandPoint() {
|
||||
List<HydrologyPoint> detailSamples = new ArrayList<>();
|
||||
HydrologyTerrainSample land = HydrologyTerrainSample.openLand(88, 1D, "parent");
|
||||
HydrologyTerrainSampler terrainSampler = (int x, int z) -> {
|
||||
detailSamples.add(new HydrologyPoint(x, 0, z));
|
||||
return land;
|
||||
};
|
||||
|
||||
HydrologyOceanBoundaryRefiner.Result result = HydrologyOceanBoundaryRefiner.refine(
|
||||
crossing(-2, 1, 9),
|
||||
terrainSampler,
|
||||
routingSampler(new ArrayList<>(), -1, HydrologyRoutingTerrainSampler.NaturalClassification.OCEAN)
|
||||
);
|
||||
|
||||
assertEquals(List.of(point(-2, 9)), detailSamples);
|
||||
assertEquals(new HydrologyPoint(-2, 90, 9), result.landwardPoint());
|
||||
assertEquals(new HydrologyPoint(-1, 90, 9), result.oceanPoint());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unavailableClassificationAbortsWithoutFullTerrainSampling() {
|
||||
List<HydrologyPoint> detailSamples = new ArrayList<>();
|
||||
HydrologyTerrainSampler terrainSampler = (int x, int z) -> {
|
||||
detailSamples.add(new HydrologyPoint(x, 0, z));
|
||||
return HydrologyTerrainSample.openLand(90, 0D, "parent");
|
||||
};
|
||||
|
||||
HydrologyOceanBoundaryRefiner.Result result = HydrologyOceanBoundaryRefiner.refine(
|
||||
crossing(0, 3, 0),
|
||||
terrainSampler,
|
||||
routingSampler(new ArrayList<>(), 2, HydrologyRoutingTerrainSampler.NaturalClassification.UNAVAILABLE)
|
||||
);
|
||||
|
||||
assertNull(result);
|
||||
assertEquals(List.of(), detailSamples);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noOceanDoesNotRequestFullTerrain() {
|
||||
List<HydrologyPoint> detailSamples = new ArrayList<>();
|
||||
HydrologyTerrainSampler terrainSampler = (int x, int z) -> {
|
||||
detailSamples.add(new HydrologyPoint(x, 0, z));
|
||||
return HydrologyTerrainSample.openLand(90, 0D, "parent");
|
||||
};
|
||||
|
||||
HydrologyOceanBoundaryRefiner.Result result = HydrologyOceanBoundaryRefiner.refine(
|
||||
crossing(-3, 0, 2),
|
||||
terrainSampler,
|
||||
routingSampler(new ArrayList<>(), Integer.MAX_VALUE, HydrologyRoutingTerrainSampler.NaturalClassification.LAND)
|
||||
);
|
||||
|
||||
assertNull(result);
|
||||
assertEquals(List.of(), detailSamples);
|
||||
}
|
||||
|
||||
private HydrologyRoutingTerrainSampler routingSampler(
|
||||
List<HydrologyPoint> probes,
|
||||
int classifiedX,
|
||||
HydrologyRoutingTerrainSampler.NaturalClassification classification
|
||||
) {
|
||||
return new HydrologyRoutingTerrainSampler() {
|
||||
@Override
|
||||
public HydrologyTerrainSample[] sampleGrid(GridRequest request) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public NaturalClassification classifyNatural(int blockX, int blockZ) {
|
||||
probes.add(new HydrologyPoint(blockX, 0, blockZ));
|
||||
return blockX == classifiedX ? classification : NaturalClassification.LAND;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private List<HydrologyPoint> crossing(int minimumX, int maximumX, int z) {
|
||||
ArrayList<HydrologyPoint> crossing = new ArrayList<>();
|
||||
for (int x = minimumX; x <= maximumX; x++) {
|
||||
crossing.add(new HydrologyPoint(x, 90, z));
|
||||
}
|
||||
return List.copyOf(crossing);
|
||||
}
|
||||
|
||||
private HydrologyPoint point(int x, int z) {
|
||||
return new HydrologyPoint(x, 0, z);
|
||||
}
|
||||
}
|
||||
+544
@@ -0,0 +1,544 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.cave.CavePosition;
|
||||
import art.arcane.iris.engine.hydrology.cave.CaveVoxel;
|
||||
import art.arcane.iris.engine.hydrology.cave.CaveVoxelPrecondition;
|
||||
import art.arcane.iris.engine.hydrology.cave.CaveVoxelView;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveConflictPolicy;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCavePlan;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
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 HydrologyPlannerCrossTileCaveAdmissionIntegrationTest {
|
||||
private static final long SEED = 77L;
|
||||
private static final List<TilePair> ADJACENT_PAIRS = List.of(
|
||||
new TilePair(new HydrologyTileKey(0, -2), new HydrologyTileKey(0, -1)),
|
||||
new TilePair(new HydrologyTileKey(0, 0), new HydrologyTileKey(0, 1))
|
||||
);
|
||||
|
||||
@Test
|
||||
public void seed77AdjacentTilesRejectIncompatibleCaveOverlapsRegardlessOfRequestOrder() {
|
||||
List<HydrologyTileKey> forwardOrder = List.of(
|
||||
ADJACENT_PAIRS.get(0).first(),
|
||||
ADJACENT_PAIRS.get(0).second(),
|
||||
ADJACENT_PAIRS.get(1).first(),
|
||||
ADJACENT_PAIRS.get(1).second()
|
||||
);
|
||||
ArrayList<HydrologyTileKey> reverseOrder = new ArrayList<>(forwardOrder);
|
||||
Collections.reverse(reverseOrder);
|
||||
|
||||
Map<HydrologyTileKey, HydrologyTile> forward = plans(planner(), forwardOrder);
|
||||
Map<HydrologyTileKey, HydrologyTile> reverse = plans(planner(), reverseOrder);
|
||||
|
||||
assertEquals(forward, reverse);
|
||||
for (TilePair pair : ADJACENT_PAIRS) {
|
||||
assertNoIncompatibleCaveOverlaps(
|
||||
pair,
|
||||
forward.get(pair.first()),
|
||||
forward.get(pair.second())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void clusteredRequiredSourcesResolveToStableNonOverlappingNetworksRegardlessOfRequestOrder() {
|
||||
TilePair pair = ADJACENT_PAIRS.getFirst();
|
||||
List<HydrologyTileKey> forwardOrder = List.of(pair.first(), pair.second());
|
||||
ArrayList<HydrologyTileKey> reverseOrder = new ArrayList<>(forwardOrder);
|
||||
Collections.reverse(reverseOrder);
|
||||
|
||||
Map<HydrologyTileKey, HydrologyTile> forward = plans(requiredFallbackPlanner(), forwardOrder);
|
||||
Map<HydrologyTileKey, HydrologyTile> reverse = plans(requiredFallbackPlanner(), reverseOrder);
|
||||
|
||||
assertEquals(forward, reverse);
|
||||
List<HydrologyPoint> firstSources = sourcePoints(forward.get(pair.first()), RiverCourseType.SURFACE);
|
||||
List<HydrologyPoint> secondSources = sourcePoints(forward.get(pair.second()), RiverCourseType.SURFACE);
|
||||
assertEquals(forward.get(pair.first()).diagnosticCandidates().toString(), 1, firstSources.size());
|
||||
assertEquals(forward.get(pair.second()).diagnosticCandidates().toString(), 1, secondSources.size());
|
||||
assertPointNear(new HydrologyPoint(32, 121, -272), firstSources.getFirst(), 8);
|
||||
assertPointNear(new HydrologyPoint(16, 126, -192), secondSources.getFirst(), 8);
|
||||
assertNoIncompatibleCaveOverlaps(pair, forward.get(pair.first()), forward.get(pair.second()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void colorRankBoundsTheCompleteOwnerDependencyGeometry() {
|
||||
HydrologyPlanner planner = planner();
|
||||
|
||||
assertEquals(1, planner.maximumCrossTileDependencyOwners(new HydrologyTileKey(0, 0)));
|
||||
assertEquals(49, planner.maximumCrossTileDependencyOwners(new HydrologyTileKey(-1, -1)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptedCavePlansMatchTheirMaterializedSurfaceAfterCrossTileAdmission() {
|
||||
HydrologyTerrainSampler terrain = terrain();
|
||||
HydrologyPlanner planner = new HydrologyPlanner(
|
||||
SEED,
|
||||
settings(),
|
||||
terrain,
|
||||
-4096,
|
||||
(HydrologyCaveVoxelViewFactory.PlannedSurface surface) ->
|
||||
new PlannedSurfaceCaveView(terrain, surface)
|
||||
);
|
||||
|
||||
for (TilePair pair : ADJACENT_PAIRS) {
|
||||
assertMaterializedPreconditions(planner.plan(pair.first()), terrain);
|
||||
assertMaterializedPreconditions(planner.plan(pair.second()), terrain);
|
||||
}
|
||||
}
|
||||
|
||||
private HydrologyPlanner planner() {
|
||||
return new HydrologyPlanner(SEED, settings(), terrain());
|
||||
}
|
||||
|
||||
private HydrologyPlanner requiredFallbackPlanner() {
|
||||
HydrologyTerrainSampler terrain = requiredFallbackTerrain();
|
||||
return new HydrologyPlanner(
|
||||
SEED,
|
||||
requiredFallbackSettings(),
|
||||
terrain,
|
||||
-4096,
|
||||
(HydrologyCaveVoxelViewFactory.PlannedSurface surface) ->
|
||||
new PlannedSurfaceCaveView(terrain, surface)
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
CaveVoxelView view = new PlannedSurfaceCaveView(terrain, surface);
|
||||
for (HydrologyCavePlan plan : tile.cavePlans()) {
|
||||
for (Map.Entry<CavePosition, CaveVoxelPrecondition> entry
|
||||
: plan.baselinePreconditions().entrySet()) {
|
||||
CavePosition position = entry.getKey();
|
||||
CaveVoxelPrecondition expected = entry.getValue();
|
||||
assertEquals(expected.voxel(), view.voxelAt(position));
|
||||
assertEquals(expected.openToSurface(), view.isOpenToSurface(position));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Map<HydrologyTileKey, HydrologyTile> plans(
|
||||
HydrologyPlanner planner,
|
||||
List<HydrologyTileKey> requestOrder
|
||||
) {
|
||||
LinkedHashMap<HydrologyTileKey, HydrologyTile> plans = new LinkedHashMap<>();
|
||||
for (HydrologyTileKey key : requestOrder) {
|
||||
plans.put(key, planner.plan(key));
|
||||
}
|
||||
return Map.copyOf(plans);
|
||||
}
|
||||
|
||||
private void assertNoIncompatibleCaveOverlaps(
|
||||
TilePair pair,
|
||||
HydrologyTile first,
|
||||
HydrologyTile second
|
||||
) {
|
||||
Map<Long, String> profileKeys = profileKeys(first, second);
|
||||
int comparedPlanPairs = 0;
|
||||
for (HydrologyCavePlan firstPlan : first.cavePlans()) {
|
||||
if (!firstPlan.accepted()) {
|
||||
continue;
|
||||
}
|
||||
for (HydrologyCavePlan secondPlan : second.cavePlans()) {
|
||||
if (!secondPlan.accepted()
|
||||
|| firstPlan.source().sourceId() == secondPlan.source().sourceId()) {
|
||||
continue;
|
||||
}
|
||||
comparedPlanPairs++;
|
||||
long firstCourseId = firstPlan.source().sourceId();
|
||||
long secondCourseId = secondPlan.source().sourceId();
|
||||
assertFalse(
|
||||
"Incompatible seed 77 cave overlap between " + pair.first() + " course "
|
||||
+ firstCourseId + " and " + pair.second() + " course " + secondCourseId,
|
||||
HydrologyCaveConflictPolicy.hasIncompatibleOverlap(
|
||||
requiredProfileKey(profileKeys, firstCourseId),
|
||||
firstPlan.actions(),
|
||||
requiredProfileKey(profileKeys, secondCourseId),
|
||||
secondPlan.actions()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
assertTrue("Seed 77 tile pair must retain accepted cave plans " + pair, comparedPlanPairs > 0);
|
||||
}
|
||||
|
||||
private Map<Long, String> profileKeys(HydrologyTile first, HydrologyTile second) {
|
||||
LinkedHashMap<Long, String> profileKeys = new LinkedHashMap<>();
|
||||
addProfileKeys(profileKeys, first);
|
||||
addProfileKeys(profileKeys, second);
|
||||
return Map.copyOf(profileKeys);
|
||||
}
|
||||
|
||||
private void addProfileKeys(Map<Long, String> profileKeys, HydrologyTile tile) {
|
||||
for (RiverCourse course : tile.courses()) {
|
||||
String existing = profileKeys.putIfAbsent(course.id(), course.profileKey());
|
||||
if (existing != null) {
|
||||
assertEquals(existing, course.profileKey());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String requiredProfileKey(Map<Long, String> profileKeys, long courseId) {
|
||||
String profileKey = profileKeys.get(courseId);
|
||||
assertTrue("Missing profile for cave course " + courseId, profileKey != null);
|
||||
return profileKey;
|
||||
}
|
||||
|
||||
private List<HydrologyPoint> sourcePoints(HydrologyTile tile, RiverCourseType type) {
|
||||
ArrayList<HydrologyPoint> points = new ArrayList<>();
|
||||
for (RiverCourse course : tile.courses()) {
|
||||
if (course.type() != type) {
|
||||
continue;
|
||||
}
|
||||
DrainageNode source = tile.node(course.sourceNodeId().orElseThrow()).orElseThrow();
|
||||
points.add(source.naturalPoint());
|
||||
}
|
||||
return List.copyOf(points);
|
||||
}
|
||||
|
||||
private void assertPointNear(HydrologyPoint expected, HydrologyPoint actual, int radius) {
|
||||
assertTrue(expected + " vs " + actual, StrictMath.abs(expected.y() - actual.y()) <= 2);
|
||||
assertTrue(expected + " vs " + actual, actual.distanceSquared2D(expected) <= (long) radius * radius);
|
||||
}
|
||||
|
||||
private HydrologyPlannerSettings settings() {
|
||||
HydrologyPlannerSettings.Source surfaceSources = new HydrologyPlannerSettings.Source(
|
||||
true,
|
||||
4D,
|
||||
80,
|
||||
1,
|
||||
6,
|
||||
32
|
||||
);
|
||||
HydrologyPlannerSettings.Source undergroundSources = new HydrologyPlannerSettings.Source(
|
||||
true,
|
||||
4D,
|
||||
Integer.MIN_VALUE,
|
||||
1,
|
||||
4,
|
||||
48
|
||||
);
|
||||
HydrologyPlannerSettings.DeepFluid deepFluid = new HydrologyPlannerSettings.DeepFluid(
|
||||
"deep_lava",
|
||||
true,
|
||||
3D,
|
||||
48,
|
||||
18,
|
||||
42,
|
||||
3,
|
||||
5,
|
||||
2,
|
||||
3,
|
||||
8,
|
||||
24,
|
||||
4,
|
||||
2,
|
||||
3,
|
||||
4096,
|
||||
4,
|
||||
true,
|
||||
true
|
||||
);
|
||||
return new HydrologyPlannerSettings(
|
||||
63,
|
||||
new HydrologyPlannerSettings.Routing(256, 16, 4, 1_089, 256,
|
||||
new HydrologyPlannerSettings.Branching(16, 8), 0.5D, 12D, 0.5D, 0.2D),
|
||||
new HydrologyPlannerSettings.Surface(
|
||||
true,
|
||||
surfaceSources,
|
||||
4,
|
||||
20,
|
||||
2,
|
||||
5,
|
||||
4,
|
||||
8,
|
||||
12,
|
||||
1.5D,
|
||||
4,
|
||||
10,
|
||||
true,
|
||||
96,
|
||||
10
|
||||
),
|
||||
new HydrologyPlannerSettings.Hydraulics(12, 40, 1, 3, 8, 4),
|
||||
new HydrologyPlannerSettings.Underground(
|
||||
true,
|
||||
undergroundSources,
|
||||
68,
|
||||
82,
|
||||
4,
|
||||
14,
|
||||
2,
|
||||
5,
|
||||
6,
|
||||
12,
|
||||
true
|
||||
),
|
||||
new HydrologyPlannerSettings.Outlets(
|
||||
true,
|
||||
new HydrologyPlannerSettings.Grotto(true, 4, 3, 3, 4096),
|
||||
new HydrologyPlannerSettings.Grotto(true, 4, 3, 3, 4096),
|
||||
true,
|
||||
12,
|
||||
48,
|
||||
2,
|
||||
8
|
||||
),
|
||||
stableGeometry(),
|
||||
List.of(deepFluid)
|
||||
);
|
||||
}
|
||||
|
||||
private HydrologyPlannerSettings.Geometry stableGeometry() {
|
||||
HydrologyPlannerSettings.ChannelShape stableChannel =
|
||||
new HydrologyPlannerSettings.ChannelShape(2D, 0D, 0D, 11);
|
||||
return new HydrologyPlannerSettings.Geometry(
|
||||
new HydrologyPlannerSettings.Meanders(224, 72, 0D, 0D, 0D, 0, 75D),
|
||||
stableChannel,
|
||||
stableChannel,
|
||||
stableChannel,
|
||||
HydrologyPlannerSettings.Geometry.defaults().drops()
|
||||
);
|
||||
}
|
||||
|
||||
private HydrologyPlannerSettings requiredFallbackSettings() {
|
||||
HydrologyPlannerSettings base = settings();
|
||||
HydrologyPlannerSettings.Surface baseSurface = base.surface();
|
||||
HydrologyPlannerSettings.Underground baseUnderground = base.underground();
|
||||
HydrologyPlannerSettings.Source surfaceSources = new HydrologyPlannerSettings.Source(
|
||||
true,
|
||||
0D,
|
||||
80,
|
||||
1,
|
||||
1,
|
||||
32
|
||||
);
|
||||
HydrologyPlannerSettings.Source disabledSources = new HydrologyPlannerSettings.Source(
|
||||
false,
|
||||
0D,
|
||||
Integer.MIN_VALUE,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
);
|
||||
return new HydrologyPlannerSettings(
|
||||
base.seaLevel(),
|
||||
base.routing(),
|
||||
new HydrologyPlannerSettings.Surface(
|
||||
true,
|
||||
surfaceSources,
|
||||
baseSurface.minimumWidth(),
|
||||
baseSurface.maximumWidth(),
|
||||
baseSurface.minimumDepth(),
|
||||
baseSurface.maximumDepth(),
|
||||
baseSurface.minimumSurfaceInset(),
|
||||
baseSurface.maximumSurfaceInset(),
|
||||
64,
|
||||
baseSurface.shoreWidth(),
|
||||
baseSurface.minimumTerrainBlendWidth(),
|
||||
baseSurface.maximumTerrainBlendWidth(),
|
||||
baseSurface.ridgeTunnelsEnabled(),
|
||||
baseSurface.maximumRidgeTunnelLength(),
|
||||
baseSurface.ridgeTunnelHeadroom()
|
||||
),
|
||||
base.hydraulics(),
|
||||
new HydrologyPlannerSettings.Underground(
|
||||
false,
|
||||
disabledSources,
|
||||
baseUnderground.minimumFluidY(),
|
||||
baseUnderground.maximumFluidY(),
|
||||
baseUnderground.minimumWidth(),
|
||||
baseUnderground.maximumWidth(),
|
||||
baseUnderground.minimumDepth(),
|
||||
baseUnderground.maximumDepth(),
|
||||
baseUnderground.minimumHeadroom(),
|
||||
baseUnderground.maximumHeadroom(),
|
||||
baseUnderground.connectToExistingCaves()
|
||||
),
|
||||
new HydrologyPlannerSettings.Outlets(
|
||||
base.outlets().oceanEnabled(),
|
||||
new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096),
|
||||
base.outlets().inlandGrotto(),
|
||||
base.outlets().surfaceSinkholesEnabled(),
|
||||
base.outlets().coastalCliffMinimumHeight(),
|
||||
base.outlets().mouthLevelingDistance(),
|
||||
base.outlets().maximumOceanApron(),
|
||||
base.outlets().maximumPerTile()
|
||||
),
|
||||
base.geometry(),
|
||||
base.deepFluids()
|
||||
);
|
||||
}
|
||||
|
||||
private HydrologyTerrainSampler terrain() {
|
||||
return (int x, int z) -> {
|
||||
if (x >= 224) {
|
||||
return oceanTerrain();
|
||||
}
|
||||
double wave = StrictMath.sin((z + SEED % 37L) / 19D) * 3D;
|
||||
double ridge = x >= 96 && x <= 128 ? 34D : 0D;
|
||||
int height = 126 - Math.floorDiv(x, 13) + (int) StrictMath.round(wave + ridge);
|
||||
double slope = ridge > 0D ? 18D : 1D + StrictMath.abs(wave) * 0.25D;
|
||||
boolean surfaceSource = x >= 0 && x <= 48;
|
||||
boolean undergroundSource = x >= 0 && x <= 64;
|
||||
return new HydrologyTerrainSample(
|
||||
height,
|
||||
slope,
|
||||
false,
|
||||
true,
|
||||
70,
|
||||
74 + Math.floorMod(x + z, 7),
|
||||
true,
|
||||
true,
|
||||
surfaceSource,
|
||||
surfaceSource,
|
||||
undergroundSource,
|
||||
undergroundSource,
|
||||
0D,
|
||||
surfaceSource ? 1D : 0D,
|
||||
undergroundSource ? 1D : 0D,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
"parent",
|
||||
"surface",
|
||||
"mouth",
|
||||
"shore",
|
||||
"dry",
|
||||
"flooded",
|
||||
List.of("water")
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
private HydrologyTerrainSampler requiredFallbackTerrain() {
|
||||
HydrologyTerrainSampler base = terrain();
|
||||
return (int x, int z) -> {
|
||||
HydrologyTerrainSample sample = base.sample(x, z);
|
||||
boolean winner = x == 32 && z == -272;
|
||||
boolean initialLoser = x == 16 && z == -224;
|
||||
boolean fallback = x == 16 && z == -192;
|
||||
boolean required = winner || initialLoser || fallback;
|
||||
double weight = initialLoser ? 64D : required ? 1D : 0D;
|
||||
return new HydrologyTerrainSample(
|
||||
sample.naturalHeight(),
|
||||
sample.slope(),
|
||||
sample.ocean(),
|
||||
sample.caveAvailable(),
|
||||
sample.caveFloorY(),
|
||||
sample.caveFluidY(),
|
||||
sample.transitAllowed(),
|
||||
sample.outletAllowed(),
|
||||
required,
|
||||
required,
|
||||
false,
|
||||
false,
|
||||
sample.routingCost(),
|
||||
weight,
|
||||
0D,
|
||||
sample.widthMultiplier(),
|
||||
sample.depthMultiplier(),
|
||||
sample.incisionMultiplier(),
|
||||
sample.routingMultiplier(),
|
||||
sample.parentBiomeKey(),
|
||||
sample.surfaceBiomeKey(),
|
||||
sample.mouthBiomeKey(),
|
||||
sample.shoreBiomeKey(),
|
||||
sample.dryBiomeKey(),
|
||||
sample.floodedCaveBiomeKey(),
|
||||
sample.preferredProfileKeys()
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
private HydrologyTerrainSample oceanTerrain() {
|
||||
return new HydrologyTerrainSample(
|
||||
54,
|
||||
0D,
|
||||
true,
|
||||
false,
|
||||
30,
|
||||
32,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
0D,
|
||||
0D,
|
||||
0D,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
"ocean_parent",
|
||||
"surface",
|
||||
"mouth",
|
||||
"shore",
|
||||
"dry",
|
||||
"flooded",
|
||||
List.of("water")
|
||||
);
|
||||
}
|
||||
|
||||
private record TilePair(HydrologyTileKey first, HydrologyTileKey second) {
|
||||
}
|
||||
|
||||
private static final class PlannedSurfaceCaveView implements CaveVoxelView {
|
||||
private final HydrologyTerrainSampler terrain;
|
||||
private final HydrologyCaveVoxelViewFactory.PlannedSurface surface;
|
||||
|
||||
private PlannedSurfaceCaveView(
|
||||
HydrologyTerrainSampler terrain,
|
||||
HydrologyCaveVoxelViewFactory.PlannedSurface surface
|
||||
) {
|
||||
this.terrain = terrain;
|
||||
this.surface = surface;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInWorld(CavePosition position) {
|
||||
return position.y() > -4096 && position.y() < 4095;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CaveVoxel voxelAt(CavePosition position) {
|
||||
return position.y() <= surfaceHeight(position.x(), position.z())
|
||||
? CaveVoxel.SOLID
|
||||
: CaveVoxel.CAVE_AIR;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpenToSurface(CavePosition position) {
|
||||
return position.y() > surfaceHeight(position.x(), position.z());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAboveTerrainSurface(CavePosition position) {
|
||||
return isOpenToSurface(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasAboveTerrainSurface(int x, int z, int minimumY, int maximumY) {
|
||||
return maximumY > surfaceHeight(x, z);
|
||||
}
|
||||
|
||||
private int surfaceHeight(int x, int z) {
|
||||
HydrologyTerrainSample sample = terrain.sample(x, z);
|
||||
return surface.resolve(x, z, sample.naturalHeight());
|
||||
}
|
||||
}
|
||||
}
|
||||
+1369
File diff suppressed because it is too large.
Load diff
+120
@@ -0,0 +1,120 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
public class HydrologyPlannerRoutingSamplerTest {
|
||||
@Test
|
||||
public void coarseLatticeUsesOnlyTheRoutingBatch() {
|
||||
AtomicInteger batchCalls = new AtomicInteger();
|
||||
AtomicInteger detailCalls = new AtomicInteger();
|
||||
HydrologyTerrainSampler detailSampler = (int x, int z) -> {
|
||||
detailCalls.incrementAndGet();
|
||||
return HydrologyTerrainSample.ocean(62, "ocean");
|
||||
};
|
||||
HydrologyRoutingTerrainSampler routingSampler = new HydrologyRoutingTerrainSampler() {
|
||||
@Override
|
||||
public HydrologyTerrainSample[] sampleGrid(GridRequest request) {
|
||||
batchCalls.incrementAndGet();
|
||||
int width = request.width();
|
||||
HydrologyTerrainSample[] samples = new HydrologyTerrainSample[width * width];
|
||||
Arrays.fill(samples, HydrologyTerrainSample.ocean(62, "ocean"));
|
||||
return samples;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NaturalClassification classifyNatural(int blockX, int blockZ) {
|
||||
return NaturalClassification.OCEAN;
|
||||
}
|
||||
};
|
||||
HydrologyPlannerSettings settings = HydrologyPlannerSettings.defaults();
|
||||
HydrologyPlanner planner = new HydrologyPlanner(
|
||||
17L,
|
||||
settings,
|
||||
detailSampler,
|
||||
routingSampler,
|
||||
HydrologyGeometrySampler.deterministic(17L, detailSampler),
|
||||
-4096,
|
||||
footprint -> new HydrologyTerrainCaveVoxelView(detailSampler, settings.seaLevel(), -4096, 4096)
|
||||
);
|
||||
|
||||
planner.plan(new HydrologyTileKey(-1, 1));
|
||||
|
||||
assertEquals(1, batchCalls.get());
|
||||
assertEquals(0, detailCalls.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void malformedRoutingBatchIsRejected() {
|
||||
HydrologyTerrainSampler detailSampler = (int x, int z) -> HydrologyTerrainSample.ocean(62, "ocean");
|
||||
HydrologyRoutingTerrainSampler routingSampler = new HydrologyRoutingTerrainSampler() {
|
||||
@Override
|
||||
public HydrologyTerrainSample[] sampleGrid(GridRequest request) {
|
||||
int width = request.width();
|
||||
return new HydrologyTerrainSample[width * width - 1];
|
||||
}
|
||||
|
||||
@Override
|
||||
public NaturalClassification classifyNatural(int blockX, int blockZ) {
|
||||
return NaturalClassification.OCEAN;
|
||||
}
|
||||
};
|
||||
HydrologyPlannerSettings settings = HydrologyPlannerSettings.defaults();
|
||||
HydrologyPlanner planner = new HydrologyPlanner(
|
||||
19L,
|
||||
settings,
|
||||
detailSampler,
|
||||
routingSampler,
|
||||
HydrologyGeometrySampler.deterministic(19L, detailSampler),
|
||||
-4096,
|
||||
footprint -> new HydrologyTerrainCaveVoxelView(detailSampler, settings.seaLevel(), -4096, 4096)
|
||||
);
|
||||
|
||||
assertThrows(IllegalStateException.class, () -> planner.plan(new HydrologyTileKey(0, 0)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void oceanClassificationBypassesNaturalBasisSampling() {
|
||||
AtomicInteger basisCalls = new AtomicInteger();
|
||||
HydrologyTerrainSampler detailSampler = (int x, int z) -> HydrologyTerrainSample.openLand(96, 0D, "land");
|
||||
HydrologyNaturalTerrainSampler routingSampler = new HydrologyNaturalTerrainSampler() {
|
||||
@Override
|
||||
public HydrologyTerrainSample[] sampleGrid(GridRequest request) {
|
||||
int width = request.width();
|
||||
HydrologyTerrainSample[] samples = new HydrologyTerrainSample[width * width];
|
||||
Arrays.fill(samples, HydrologyTerrainSample.openLand(96, 0D, "land"));
|
||||
return samples;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NaturalClassification classifyNatural(int blockX, int blockZ) {
|
||||
return NaturalClassification.OCEAN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HydrologyTerrainSample sampleBasis(int blockX, int blockZ) {
|
||||
basisCalls.incrementAndGet();
|
||||
throw new IllegalStateException("Ocean basis must not be sampled");
|
||||
}
|
||||
};
|
||||
HydrologyPlannerSettings settings = HydrologyPlannerSettings.defaults();
|
||||
HydrologyPlanner planner = new HydrologyPlanner(
|
||||
23L,
|
||||
settings,
|
||||
detailSampler,
|
||||
routingSampler,
|
||||
HydrologyGeometrySampler.deterministic(23L, detailSampler),
|
||||
-4096,
|
||||
footprint -> new HydrologyTerrainCaveVoxelView(detailSampler, settings.seaLevel(), -4096, 4096)
|
||||
);
|
||||
|
||||
planner.plan(new HydrologyTileKey(0, 0));
|
||||
|
||||
assertEquals(0, basisCalls.get());
|
||||
}
|
||||
}
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class HydrologyPlannerSettingsTest {
|
||||
@Test
|
||||
public void branchingUsesIndependentSurfaceAndUndergroundCourseFloors() {
|
||||
HydrologyPlannerSettings.Branching branching = new HydrologyPlannerSettings.Branching(384, 192);
|
||||
|
||||
assertEquals(384, branching.minimumCourseLength(true));
|
||||
assertEquals(192, branching.minimumCourseLength(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void crossTileAdmissionRejectsPublicationEnvelopesBeyondFourColorsPerAxis() {
|
||||
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults();
|
||||
assertEquals(0.5D, base.surface().sources().density(), 0D);
|
||||
assertEquals(0.25D, base.underground().sources().density(), 0D);
|
||||
HydrologyPlannerSettings.DeepFluid overlongChannel = new HydrologyPlannerSettings.DeepFluid(
|
||||
"deep",
|
||||
true,
|
||||
1D,
|
||||
64,
|
||||
-64,
|
||||
-32,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
3072,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
64,
|
||||
1,
|
||||
false,
|
||||
true
|
||||
);
|
||||
|
||||
assertEquals(2, base.crossTileColorPeriod());
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings(
|
||||
base.seaLevel(),
|
||||
base.routing(),
|
||||
base.surface(),
|
||||
base.hydraulics(),
|
||||
base.underground(),
|
||||
base.outlets(),
|
||||
HydrologyPlannerSettings.Geometry.defaults(),
|
||||
List.of(overlongChannel)
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canonicalBoundaryValuesAreAccepted() {
|
||||
HydrologyPlannerSettings.Routing routing = new HydrologyPlannerSettings.Routing(
|
||||
512,
|
||||
512,
|
||||
64,
|
||||
4,
|
||||
256,
|
||||
new HydrologyPlannerSettings.Branching(0, 0),
|
||||
0D,
|
||||
0D,
|
||||
0D,
|
||||
0D
|
||||
);
|
||||
HydrologyPlannerSettings.Source sources = new HydrologyPlannerSettings.Source(true, 1D, 0, 0, 1, 0);
|
||||
HydrologyPlannerSettings.Surface surface = new HydrologyPlannerSettings.Surface(
|
||||
true,
|
||||
sources,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
1D,
|
||||
0,
|
||||
0,
|
||||
false,
|
||||
0,
|
||||
0
|
||||
);
|
||||
HydrologyPlannerSettings.Hydraulics hydraulics = new HydrologyPlannerSettings.Hydraulics(
|
||||
8,
|
||||
8,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
1
|
||||
);
|
||||
HydrologyPlannerSettings.Grotto grotto = new HydrologyPlannerSettings.Grotto(true, 1, 1, 1, 1);
|
||||
HydrologyPlannerSettings.Outlets outlets = new HydrologyPlannerSettings.Outlets(
|
||||
true,
|
||||
grotto,
|
||||
grotto,
|
||||
true,
|
||||
0,
|
||||
0,
|
||||
64,
|
||||
1
|
||||
);
|
||||
HydrologyPlannerSettings.DeepFluid deepFluid = new HydrologyPlannerSettings.DeepFluid(
|
||||
"deep",
|
||||
true,
|
||||
1D,
|
||||
8192,
|
||||
-64,
|
||||
-32,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
2730,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
64,
|
||||
1,
|
||||
false,
|
||||
true
|
||||
);
|
||||
|
||||
assertEquals(256, routing.maximumRouteLength());
|
||||
assertEquals(0, surface.maximumIncision());
|
||||
assertEquals(1, hydraulics.waterfallMinimumDrop());
|
||||
assertEquals(0, outlets.mouthLevelingDistance());
|
||||
assertEquals(1, grotto.maximumVolume());
|
||||
assertEquals(2730, deepFluid.maximumChannelLength());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inconsistentHydraulicThresholdsAreRejected() {
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Hydraulics(
|
||||
8,
|
||||
16,
|
||||
0,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void valuesOutsideCanonicalBoundsAreRejected() {
|
||||
HydrologyPlannerSettings.Grotto accepted = new HydrologyPlannerSettings.Grotto(true, 1, 1, 1, 1);
|
||||
assertEquals(1, accepted.maximumVolume());
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new HydrologyPlannerSettings.Grotto(true, 1, 1, 1, 0));
|
||||
HydrologyPlannerSettings.Grotto disabled = new HydrologyPlannerSettings.Grotto(false, 1, 1, 1, 1);
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Outlets(
|
||||
true,
|
||||
accepted,
|
||||
disabled,
|
||||
true,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
1
|
||||
));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Outlets(
|
||||
true,
|
||||
accepted,
|
||||
accepted,
|
||||
true,
|
||||
0,
|
||||
0,
|
||||
65,
|
||||
1
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dropGeometryNarrowsFlowAndExpandsItsReceivingBasin() {
|
||||
HydrologyPlannerSettings.Drops drops = new HydrologyPlannerSettings.Drops(
|
||||
2,
|
||||
1.4D,
|
||||
2,
|
||||
0.45D,
|
||||
2,
|
||||
1.8D,
|
||||
8
|
||||
);
|
||||
|
||||
assertEquals(5, drops.flowWidth(10));
|
||||
assertEquals(1, drops.stepLimit(HydrologyFeatureType.CASCADE));
|
||||
assertEquals(2, drops.stepLimit(HydrologyFeatureType.UNDERGROUND_DROP));
|
||||
assertEquals(2, drops.flowDepth(6));
|
||||
assertEquals(9, drops.basinWidth(5));
|
||||
assertEquals(5, drops.basinDepth(2, 8));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Drops(
|
||||
2,
|
||||
1.4D,
|
||||
2,
|
||||
0.45D,
|
||||
4,
|
||||
1.8D,
|
||||
3
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deepSiteAdmissionUsesItsOwnHeadAndEnvelope() {
|
||||
HydrologyPlannerSettings.DeepFluid deepFluid = new HydrologyPlannerSettings.DeepFluid(
|
||||
"deep",
|
||||
true,
|
||||
1D,
|
||||
1024,
|
||||
16,
|
||||
96,
|
||||
4,
|
||||
4,
|
||||
3,
|
||||
3,
|
||||
0,
|
||||
0,
|
||||
2,
|
||||
2,
|
||||
4,
|
||||
4096,
|
||||
1,
|
||||
true,
|
||||
false
|
||||
);
|
||||
HydrologyTerrainSample terrainWithoutUndergroundRiverCave = HydrologyTerrainSample.openLand(100, 1D, "parent");
|
||||
|
||||
assertFalse(HydrologyPlanner.deepSiteFits(terrainWithoutUndergroundRiverCave, deepFluid, 3, 3, 0));
|
||||
assertTrue(HydrologyPlanner.deepSiteFits(terrainWithoutUndergroundRiverCave, deepFluid, 3, 3, -64));
|
||||
assertTrue(HydrologyPlanner.deepSiteFits(terrainWithoutUndergroundRiverCave, deepFluid, 4, 3, 0));
|
||||
assertTrue(HydrologyPlanner.deepSiteFits(terrainWithoutUndergroundRiverCave, deepFluid, 80, 3, 0));
|
||||
assertFalse(HydrologyPlanner.deepSiteFits(terrainWithoutUndergroundRiverCave, deepFluid, 96, 3, 0));
|
||||
assertFalse(HydrologyPlanner.deepSiteFits(
|
||||
HydrologyTerrainSample.ocean(50, "ocean"),
|
||||
deepFluid,
|
||||
40,
|
||||
3,
|
||||
0
|
||||
));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,410 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.cave.CavePosition;
|
||||
import art.arcane.iris.engine.hydrology.cave.CaveVoxel;
|
||||
import art.arcane.iris.engine.hydrology.cave.CaveVoxelView;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class HydrologyStyledGeometryTest {
|
||||
private static final HydrologyTileKey TILE = new HydrologyTileKey(0, 0);
|
||||
|
||||
@Test
|
||||
public void nonFlatGeometryIsResolvedIntoAcceptedPlansDeterministically() {
|
||||
HydrologyPlannerSettings.DeepFluid deepFluid = new HydrologyPlannerSettings.DeepFluid(
|
||||
"deep_water",
|
||||
true,
|
||||
2D,
|
||||
32,
|
||||
20,
|
||||
40,
|
||||
3,
|
||||
3,
|
||||
2,
|
||||
2,
|
||||
8,
|
||||
16,
|
||||
3,
|
||||
2,
|
||||
3,
|
||||
8192,
|
||||
2,
|
||||
false,
|
||||
true
|
||||
);
|
||||
HydrologyPlannerSettings surfaceSettings = settings(2D, 0D, List.of(deepFluid));
|
||||
HydrologyPlannerSettings undergroundSettings = settings(0D, 2D, List.of());
|
||||
HydrologyTerrainSampler terrain = this::terrain;
|
||||
HashSet<HydrologyGeometrySampler.Field> sampledFields = new HashSet<>();
|
||||
HydrologyGeometrySampler geometry = request -> {
|
||||
sampledFields.add(request.field());
|
||||
return styledValue(request);
|
||||
};
|
||||
HydrologyPlanner surfacePlanner = planner(7711L, surfaceSettings, terrain, geometry);
|
||||
HydrologyPlanner undergroundPlanner = planner(7711L, undergroundSettings, terrain, geometry);
|
||||
|
||||
HydrologyTile first = surfacePlanner.plan(TILE);
|
||||
HydrologyTile repeated = surfacePlanner.plan(TILE);
|
||||
HydrologyTile undergroundFirst = undergroundPlanner.plan(TILE);
|
||||
HydrologyTile undergroundRepeated = undergroundPlanner.plan(TILE);
|
||||
|
||||
assertEquals(first, repeated);
|
||||
assertEquals(undergroundFirst, undergroundRepeated);
|
||||
assertEquals(Set.of(HydrologyGeometrySampler.Field.values()), sampledFields);
|
||||
|
||||
RiverCourse surface = courses(first, RiverCourseType.SURFACE).getFirst();
|
||||
DrainageNode surfaceSource = first.node(surface.sourceNodeId().orElseThrow()).orElseThrow();
|
||||
HydraulicSegment surfaceStart = surface.segments().getFirst();
|
||||
int surfaceDischarge = Math.max(1, surface.drainageEdges().getFirst().contributingSurfaceSources());
|
||||
int maximumSurfaceWidth = surfaceSettings.surface().maximumWidth()
|
||||
+ (surfaceStart.receivingPool() ? 2 : 0);
|
||||
int maximumSurfaceDepth = surfaceSettings.surface().maximumDepth()
|
||||
+ (surfaceStart.receivingPool() ? 1 : 0);
|
||||
assertTrue(surfaceStart.width() >= surfaceSettings.surface().minimumWidth());
|
||||
assertTrue(surfaceStart.width() <= maximumSurfaceWidth);
|
||||
assertTrue(surfaceStart.depth() >= surfaceSettings.surface().minimumDepth());
|
||||
assertTrue(surfaceStart.depth() <= maximumSurfaceDepth);
|
||||
assertTrue(
|
||||
surfaceStart.upstreamHeadY() <=
|
||||
terrain(surfaceSource.naturalPoint().x(), surfaceSource.naturalPoint().z()).naturalHeight()
|
||||
- surfaceSettings.surface().maximumSurfaceInset()
|
||||
);
|
||||
int styledSurfaceWidth = scaledDimension(
|
||||
styledValue(request(
|
||||
HydrologyGeometrySampler.Field.SURFACE_WIDTH,
|
||||
surface.profileKey(),
|
||||
surfaceSource.naturalPoint(),
|
||||
surfaceSettings.surface().minimumWidth(),
|
||||
surfaceSettings.surface().maximumWidth()
|
||||
)),
|
||||
surfaceSettings.surface().maximumWidth(),
|
||||
surfaceDischarge
|
||||
);
|
||||
assertTrue(surfaceStart.width() >= styledSurfaceWidth
|
||||
|| surfaceStart.fallingFluid()
|
||||
|| surfaceStart.receivingPool());
|
||||
|
||||
RiverCourse underground = courses(undergroundFirst, RiverCourseType.UNDERGROUND).getFirst();
|
||||
DrainageNode undergroundSource = undergroundFirst.node(underground.sourceNodeId().orElseThrow()).orElseThrow();
|
||||
HydraulicSegment undergroundStart = underground.segments().getFirst();
|
||||
assertEquals(undergroundSettings.underground().maximumFluidY(), undergroundStart.upstreamHeadY());
|
||||
int expectedUndergroundWidth = undergroundStart.type().isDrop()
|
||||
? undergroundSettings.geometry().drops().flowWidth(undergroundSettings.underground().maximumWidth())
|
||||
: undergroundSettings.underground().maximumWidth();
|
||||
int expectedUndergroundDepth = undergroundStart.type().isDrop()
|
||||
? undergroundSettings.geometry().drops().flowDepth(undergroundSettings.underground().maximumDepth())
|
||||
: undergroundSettings.underground().maximumDepth();
|
||||
assertEquals(expectedUndergroundWidth, undergroundStart.width());
|
||||
assertEquals(expectedUndergroundDepth, undergroundStart.depth());
|
||||
assertEquals(
|
||||
styledValue(request(
|
||||
HydrologyGeometrySampler.Field.UNDERGROUND_FLUID_LEVEL,
|
||||
underground.profileKey(),
|
||||
undergroundSource.naturalPoint(),
|
||||
undergroundSettings.underground().minimumFluidY(),
|
||||
undergroundSettings.underground().maximumFluidY()
|
||||
)),
|
||||
undergroundStart.upstreamHeadY()
|
||||
);
|
||||
|
||||
List<RiverCourse> deepCourses = courses(first, RiverCourseType.DEEP_FLUID);
|
||||
assertFalse(deepCourses.isEmpty());
|
||||
for (RiverCourse course : deepCourses) {
|
||||
HydraulicSegment segment = course.segments().getFirst();
|
||||
assertEquals(
|
||||
styledValue(request(
|
||||
HydrologyGeometrySampler.Field.DEEP_FLUID_HEIGHT,
|
||||
course.profileKey(),
|
||||
segment.start(),
|
||||
deepFluid.minimumY(),
|
||||
deepFluid.maximumY()
|
||||
)),
|
||||
segment.upstreamHeadY()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void poolStylingCannotManufactureUnsupportedTransitions() {
|
||||
HydrologyPlannerSettings settings = settings(1D, 0D, List.of());
|
||||
HydrologyTerrainSampler terrain = this::terrain;
|
||||
HydrologyGeometrySampler shortPools = request -> request.field()
|
||||
== HydrologyGeometrySampler.Field.TARGET_POOL_LENGTH
|
||||
? request.minimum()
|
||||
: styledValue(request);
|
||||
HydrologyGeometrySampler longPools = request -> request.field()
|
||||
== HydrologyGeometrySampler.Field.TARGET_POOL_LENGTH
|
||||
? request.maximum()
|
||||
: styledValue(request);
|
||||
|
||||
HydrologyTile shortFirst = planner(8811L, settings, terrain, shortPools).plan(TILE);
|
||||
HydrologyTile shortRepeated = planner(8811L, settings, terrain, shortPools).plan(TILE);
|
||||
HydrologyTile longFirst = planner(8811L, settings, terrain, longPools).plan(TILE);
|
||||
HydrologyTile longRepeated = planner(8811L, settings, terrain, longPools).plan(TILE);
|
||||
|
||||
assertEquals(shortFirst, shortRepeated);
|
||||
assertEquals(longFirst, longRepeated);
|
||||
List<Integer> shortHeads = heads(courses(shortFirst, RiverCourseType.SURFACE).getFirst());
|
||||
List<Integer> longHeads = heads(courses(longFirst, RiverCourseType.SURFACE).getFirst());
|
||||
assertEquals(shortHeads.getFirst(), longHeads.getFirst());
|
||||
assertEquals(shortHeads.getLast(), longHeads.getLast());
|
||||
assertNonIncreasing(shortHeads);
|
||||
assertNonIncreasing(longHeads);
|
||||
assertSurfaceDropsTerrainBacked(shortFirst, terrain);
|
||||
assertSurfaceDropsTerrainBacked(longFirst, terrain);
|
||||
}
|
||||
|
||||
private HydrologyPlanner planner(
|
||||
long seed,
|
||||
HydrologyPlannerSettings settings,
|
||||
HydrologyTerrainSampler terrain,
|
||||
HydrologyGeometrySampler geometry
|
||||
) {
|
||||
return new HydrologyPlanner(seed, settings, terrain, geometry, -4096, footprint -> solidCaveView());
|
||||
}
|
||||
|
||||
private CaveVoxelView solidCaveView() {
|
||||
return new CaveVoxelView() {
|
||||
@Override
|
||||
public boolean isInWorld(CavePosition position) {
|
||||
return position.y() > -4096 && position.y() < 4096;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CaveVoxel voxelAt(CavePosition position) {
|
||||
return CaveVoxel.SOLID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpenToSurface(CavePosition position) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAboveTerrainSurface(CavePosition position) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private HydrologyPlannerSettings settings(
|
||||
double surfaceDensity,
|
||||
double undergroundDensity,
|
||||
List<HydrologyPlannerSettings.DeepFluid> deepFluids
|
||||
) {
|
||||
HydrologyPlannerSettings.Source surfaceSources = new HydrologyPlannerSettings.Source(
|
||||
true,
|
||||
surfaceDensity,
|
||||
80,
|
||||
0,
|
||||
4,
|
||||
24
|
||||
);
|
||||
HydrologyPlannerSettings.Source undergroundSources = new HydrologyPlannerSettings.Source(
|
||||
true,
|
||||
undergroundDensity,
|
||||
Integer.MIN_VALUE,
|
||||
0,
|
||||
4,
|
||||
32
|
||||
);
|
||||
return new HydrologyPlannerSettings(
|
||||
63,
|
||||
new HydrologyPlannerSettings.Routing(128, 16, 4, 512, 256,
|
||||
new HydrologyPlannerSettings.Branching(16, 8), 0.5D, 12D, 0.5D, 0.1D),
|
||||
new HydrologyPlannerSettings.Surface(
|
||||
surfaceDensity > 0D,
|
||||
surfaceSources,
|
||||
4,
|
||||
18,
|
||||
2,
|
||||
4,
|
||||
4,
|
||||
8,
|
||||
10,
|
||||
1.5D,
|
||||
4,
|
||||
8,
|
||||
true,
|
||||
96,
|
||||
8
|
||||
),
|
||||
new HydrologyPlannerSettings.Hydraulics(12, 28, 1, 3, 8, 4),
|
||||
new HydrologyPlannerSettings.Underground(
|
||||
undergroundDensity > 0D,
|
||||
undergroundSources,
|
||||
68,
|
||||
82,
|
||||
4,
|
||||
12,
|
||||
2,
|
||||
4,
|
||||
5,
|
||||
9,
|
||||
true
|
||||
),
|
||||
new HydrologyPlannerSettings.Outlets(
|
||||
true,
|
||||
new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096),
|
||||
new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096),
|
||||
false,
|
||||
12,
|
||||
32,
|
||||
2,
|
||||
4
|
||||
),
|
||||
HydrologyPlannerSettings.Geometry.defaults(),
|
||||
deepFluids
|
||||
);
|
||||
}
|
||||
|
||||
private HydrologyTerrainSample terrain(int x, int z) {
|
||||
if (x >= 112) {
|
||||
return new HydrologyTerrainSample(
|
||||
54,
|
||||
0D,
|
||||
true,
|
||||
false,
|
||||
30,
|
||||
32,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
0D,
|
||||
0D,
|
||||
0D,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
"ocean",
|
||||
"surface",
|
||||
"mouth",
|
||||
"shore",
|
||||
"dry",
|
||||
"flooded",
|
||||
List.of("water")
|
||||
);
|
||||
}
|
||||
int height = 118 - Math.floorDiv(x, 12) + (int) StrictMath.round(StrictMath.sin(z / 18D) * 2D);
|
||||
boolean source = x >= 0 && x <= 24;
|
||||
return new HydrologyTerrainSample(
|
||||
height,
|
||||
1D,
|
||||
false,
|
||||
true,
|
||||
70,
|
||||
74,
|
||||
true,
|
||||
true,
|
||||
source,
|
||||
source,
|
||||
source,
|
||||
source,
|
||||
0D,
|
||||
source ? 1D : 0D,
|
||||
source ? 1D : 0D,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
"parent",
|
||||
"surface",
|
||||
"mouth",
|
||||
"shore",
|
||||
"dry",
|
||||
"flooded",
|
||||
List.of("water")
|
||||
);
|
||||
}
|
||||
|
||||
private int styledValue(HydrologyGeometrySampler.Request request) {
|
||||
return request.x() < 56 ? request.maximum() : request.minimum();
|
||||
}
|
||||
|
||||
private HydrologyGeometrySampler.Request request(
|
||||
HydrologyGeometrySampler.Field field,
|
||||
String profileKey,
|
||||
HydrologyPoint point,
|
||||
int minimum,
|
||||
int maximum
|
||||
) {
|
||||
return new HydrologyGeometrySampler.Request(field, profileKey, point.x(), point.z(), 0L, minimum, maximum);
|
||||
}
|
||||
|
||||
private int scaledDimension(int styledBase, int maximum, int discharge) {
|
||||
double flowScale = Math.min(1D, StrictMath.log(discharge + 1D) / StrictMath.log(9D));
|
||||
return Math.min(maximum, styledBase + (int) StrictMath.round((maximum - styledBase) * flowScale));
|
||||
}
|
||||
|
||||
private List<RiverCourse> courses(HydrologyTile tile, RiverCourseType type) {
|
||||
ArrayList<RiverCourse> selected = new ArrayList<>();
|
||||
for (RiverCourse course : tile.courses()) {
|
||||
if (course.type() == type) {
|
||||
selected.add(course);
|
||||
}
|
||||
}
|
||||
return List.copyOf(selected);
|
||||
}
|
||||
|
||||
private List<Integer> heads(RiverCourse course) {
|
||||
ArrayList<Integer> heads = new ArrayList<>();
|
||||
for (HydraulicSegment segment : course.segments()) {
|
||||
if (segment.type() == HydrologyFeatureType.MOUTH) {
|
||||
continue;
|
||||
}
|
||||
heads.add(segment.upstreamHeadY());
|
||||
heads.add(segment.downstreamHeadY());
|
||||
}
|
||||
return List.copyOf(heads);
|
||||
}
|
||||
|
||||
private void assertNonIncreasing(List<Integer> heads) {
|
||||
for (int index = 1; index < heads.size(); index++) {
|
||||
assertTrue(heads.get(index) <= heads.get(index - 1));
|
||||
}
|
||||
}
|
||||
|
||||
private void assertSurfaceDropsTerrainBacked(HydrologyTile tile, HydrologyTerrainSampler terrain) {
|
||||
for (RiverCourse course : courses(tile, RiverCourseType.SURFACE)) {
|
||||
RiverOutlet outlet = tile.outlet(course.outletId().orElseThrow()).orElseThrow();
|
||||
for (int segmentIndex = 0; segmentIndex < course.segments().size(); segmentIndex++) {
|
||||
HydraulicSegment segment = course.segments().get(segmentIndex);
|
||||
if (!segment.type().isDrop() || !segment.type().isSurface()) {
|
||||
continue;
|
||||
}
|
||||
HydrologyPoint start = segment.start();
|
||||
HydrologyPoint end = segment.end();
|
||||
int downstreamNaturalHeight = outlet.directOcean() && terminalMouthDrop(course, segmentIndex)
|
||||
? terrain.sample(outlet.connectionPoint().x(), outlet.connectionPoint().z()).naturalHeight()
|
||||
: terrain.sample(end.x(), end.z()).naturalHeight();
|
||||
int naturalDrop = terrain.sample(start.x(), start.z()).naturalHeight() - downstreamNaturalHeight;
|
||||
assertTrue(
|
||||
"type=" + segment.type() + " start=" + start + " end=" + end
|
||||
+ " naturalDrop=" + naturalDrop + " solvedDrop=" + segment.drop(),
|
||||
naturalDrop >= segment.drop()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean terminalMouthDrop(RiverCourse course, int segmentIndex) {
|
||||
for (int index = segmentIndex + 1; index < course.segments().size(); index++) {
|
||||
if (course.segments().get(index).type() != HydrologyFeatureType.MOUTH) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.cave.CavePosition;
|
||||
import art.arcane.iris.engine.hydrology.cave.CaveVoxel;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||
import org.junit.Test;
|
||||
|
||||
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 HydrologyTerrainCaveVoxelViewTest {
|
||||
@Test
|
||||
public void reusesOneTerrainSampleAcrossVoxelQueriesInTheSameColumn() {
|
||||
AtomicInteger samples = new AtomicInteger();
|
||||
HydrologyTerrainSampler sampler = (int x, int z) -> {
|
||||
samples.incrementAndGet();
|
||||
return HydrologyTerrainSample.openLand(64, 0D, "land");
|
||||
};
|
||||
HydrologyTerrainCaveVoxelView view = new HydrologyTerrainCaveVoxelView(
|
||||
sampler,
|
||||
63,
|
||||
-64,
|
||||
320
|
||||
);
|
||||
|
||||
assertEquals(CaveVoxel.SOLID, view.voxelAt(new CavePosition(5, 60, 9)));
|
||||
assertEquals(CaveVoxel.CAVE_AIR, view.voxelAt(new CavePosition(5, 70, 9)));
|
||||
assertFalse(view.isAboveTerrainSurface(new CavePosition(5, 64, 9)));
|
||||
assertTrue(view.isOpenToSurface(new CavePosition(5, 65, 9)));
|
||||
assertFalse(view.hasAboveTerrainSurface(5, 9, 50, 64));
|
||||
assertTrue(view.hasAboveTerrainSurface(5, 9, 50, 65));
|
||||
assertEquals(1, samples.get());
|
||||
|
||||
assertEquals(CaveVoxel.SOLID, view.voxelAt(new CavePosition(6, 60, 9)));
|
||||
assertEquals(2, samples.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sharedPlanningCacheReusesTerrainSamplesAcrossCaveViews() {
|
||||
AtomicInteger samples = new AtomicInteger();
|
||||
HydrologyTerrainSampler sampler = (int x, int z) -> {
|
||||
samples.incrementAndGet();
|
||||
return HydrologyTerrainSample.openLand(64, 0D, "land");
|
||||
};
|
||||
Long2ObjectOpenHashMap<HydrologyTerrainSample> shared = new Long2ObjectOpenHashMap<>();
|
||||
HydrologyTerrainCaveVoxelView first = new HydrologyTerrainCaveVoxelView(
|
||||
sampler,
|
||||
63,
|
||||
-64,
|
||||
320
|
||||
).withSampleCache(shared);
|
||||
HydrologyTerrainCaveVoxelView second = new HydrologyTerrainCaveVoxelView(
|
||||
sampler,
|
||||
63,
|
||||
-64,
|
||||
320
|
||||
).withSampleCache(shared);
|
||||
|
||||
assertTrue(first.isAboveTerrainSurface(new CavePosition(5, 65, 9)));
|
||||
assertEquals(CaveVoxel.SOLID, second.voxelAt(new CavePosition(5, 60, 9)));
|
||||
assertEquals(1, samples.get());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
public final class HydrologyTerrainSampleTest {
|
||||
@Test
|
||||
public void reslopingReusesAnAlreadyNormalizedProfileList() {
|
||||
HydrologyTerrainSample sample = sample(List.of("deep", "water"));
|
||||
|
||||
HydrologyTerrainSample resloped = sample.withSlope(2D);
|
||||
|
||||
assertEquals(2D, resloped.slope(), 0D);
|
||||
assertSame(sample.preferredProfileKeys(), resloped.preferredProfileKeys());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void profileNormalizationStillTrimsSortsAndDeduplicates() {
|
||||
HydrologyTerrainSample sample = sample(Arrays.asList(" water ", "", null, "deep", "water"));
|
||||
|
||||
assertEquals(List.of("deep", "water"), sample.preferredProfileKeys());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void normalizedMutableProfilesAreDefensivelyCopied() {
|
||||
ArrayList<String> profiles = new ArrayList<>(List.of("deep", "water"));
|
||||
HydrologyTerrainSample sample = sample(profiles);
|
||||
|
||||
profiles.clear();
|
||||
|
||||
assertEquals(List.of("deep", "water"), sample.preferredProfileKeys());
|
||||
assertThrows(UnsupportedOperationException.class, () -> sample.preferredProfileKeys().add("other"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void emptyProfilesStillUseDefaultAndInvalidSlopeStillFails() {
|
||||
assertEquals(List.of("default"), sample(List.of()).preferredProfileKeys());
|
||||
assertThrows(IllegalArgumentException.class, () -> sample(-1D, List.of("water")));
|
||||
}
|
||||
|
||||
private static HydrologyTerrainSample sample(List<String> profiles) {
|
||||
return sample(1D, profiles);
|
||||
}
|
||||
|
||||
private static HydrologyTerrainSample sample(double slope, List<String> profiles) {
|
||||
return new HydrologyTerrainSample(
|
||||
80,
|
||||
slope,
|
||||
false,
|
||||
true,
|
||||
40,
|
||||
50,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
"parent",
|
||||
"surface",
|
||||
"mouth",
|
||||
"shore",
|
||||
"dry",
|
||||
"flooded",
|
||||
profiles
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.Callable;
|
||||
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.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class HydrologyTileCacheTest {
|
||||
@Test
|
||||
public void chunkPreparationUsesHeightFillColumnOrder() {
|
||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||
HydrologyTile tile = mock(HydrologyTile.class);
|
||||
ArrayList<Long> visitedColumns = new ArrayList<>();
|
||||
when(planner.settings()).thenReturn(emptySettings());
|
||||
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
|
||||
doAnswer(invocation -> {
|
||||
int blockX = invocation.getArgument(0);
|
||||
int blockZ = invocation.getArgument(1);
|
||||
visitedColumns.add(RiverFootprint.pack(blockX, blockZ));
|
||||
return Optional.empty();
|
||||
}).when(tile).columnAt(anyInt(), anyInt());
|
||||
HydrologyTileCache cache = new HydrologyTileCache(planner, 4);
|
||||
|
||||
cache.prepareChunkColumns(32, 48);
|
||||
|
||||
assertEquals(256, visitedColumns.size());
|
||||
int index = 0;
|
||||
for (int localZ = 0; localZ < 16; localZ++) {
|
||||
for (int localX = 0; localX < 16; localX++) {
|
||||
assertEquals(
|
||||
RiverFootprint.pack(32 + localX, 48 + localZ),
|
||||
visitedColumns.get(index).longValue()
|
||||
);
|
||||
index++;
|
||||
}
|
||||
}
|
||||
verify(planner, times(1)).plan(new HydrologyTileKey(0, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void concurrentSameKeyRequestsPlanOnlyOnce() throws Exception {
|
||||
AtomicInteger planningStarts = new AtomicInteger();
|
||||
HydrologyTerrainSampler sampler = (int x, int z) -> HydrologyTerrainSample.openLand(90, 0D, "parent");
|
||||
HydrologyPlanner planner = new HydrologyPlanner(
|
||||
41L,
|
||||
emptySettings(),
|
||||
sampler,
|
||||
-4096,
|
||||
footprint -> {
|
||||
planningStarts.incrementAndGet();
|
||||
return new HydrologyTerrainCaveVoxelView(sampler, 63, -4096, 4096);
|
||||
}
|
||||
);
|
||||
HydrologyTileCache cache = new HydrologyTileCache(planner, 4);
|
||||
ExecutorService executor = Executors.newFixedThreadPool(8);
|
||||
try {
|
||||
ArrayList<Callable<HydrologyTile>> tasks = new ArrayList<>();
|
||||
for (int index = 0; index < 32; index++) {
|
||||
tasks.add(() -> cache.get(new HydrologyTileKey(0, 0)));
|
||||
}
|
||||
List<Future<HydrologyTile>> futures = executor.invokeAll(tasks);
|
||||
HydrologyTile expected = futures.getFirst().get(10L, TimeUnit.SECONDS);
|
||||
for (Future<HydrologyTile> future : futures) {
|
||||
assertSame(expected, future.get(10L, TimeUnit.SECONDS));
|
||||
}
|
||||
assertEquals(1, planningStarts.get());
|
||||
assertEquals(1, cache.size());
|
||||
} finally {
|
||||
executor.shutdownNow();
|
||||
assertTrue(executor.awaitTermination(10L, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void completedTilesAreEvictedAtTheConfiguredBound() {
|
||||
HydrologyPlanner planner = new HydrologyPlanner(
|
||||
73L,
|
||||
emptySettings(),
|
||||
(int x, int z) -> HydrologyTerrainSample.openLand(90, 0D, "parent")
|
||||
);
|
||||
HydrologyTileCache cache = new HydrologyTileCache(planner, 2);
|
||||
|
||||
for (int tileX = 0; tileX < 8; tileX++) {
|
||||
cache.get(new HydrologyTileKey(tileX, 0));
|
||||
assertTrue(cache.size() <= 2);
|
||||
}
|
||||
|
||||
assertEquals(2, cache.maximumEntries());
|
||||
assertEquals(2, cache.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void compositePublicationIsIndependentOfTileAndColumnQueryOrder() {
|
||||
HydrologyPlanner planner = new HydrologyPlanner(811L, featureSettings(), this::featureTerrain);
|
||||
HydrologyTile origin = planner.plan(new HydrologyTileKey(0, 0));
|
||||
HydrologyColumnSample neighboringColumn = null;
|
||||
HydrologyColumnSample ownerColumn = null;
|
||||
for (HydrologyColumnSample column : origin.footprint().columns().values()) {
|
||||
if (new HydrologyTileKey(0, 0).contains(column.x(), column.z(), origin.tileSize())) {
|
||||
ownerColumn = column;
|
||||
} else if (!column.ocean()) {
|
||||
neighboringColumn = column;
|
||||
}
|
||||
if (ownerColumn != null && neighboringColumn != null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertTrue(origin.footprint().columns().values().stream().anyMatch(
|
||||
(HydrologyColumnSample column) -> !new HydrologyTileKey(0, 0)
|
||||
.contains(column.x(), column.z(), origin.tileSize())));
|
||||
assertTrue(ownerColumn != null && neighboringColumn != null);
|
||||
HydrologyTileKey neighboringKey = HydrologyTileKey.fromBlock(
|
||||
neighboringColumn.x(),
|
||||
neighboringColumn.z(),
|
||||
origin.tileSize()
|
||||
);
|
||||
assertFalse(planner.plan(neighboringKey)
|
||||
.columnAt(neighboringColumn.x(), neighboringColumn.z())
|
||||
.isPresent());
|
||||
|
||||
HydrologyTileCache firstOrder = new HydrologyTileCache(planner, 8);
|
||||
HydrologyColumnSample firstNeighbor = firstOrder
|
||||
.columnAt(neighboringColumn.x(), neighboringColumn.z())
|
||||
.orElseThrow();
|
||||
HydrologyColumnSample firstOwner = firstOrder.columnAt(ownerColumn.x(), ownerColumn.z()).orElseThrow();
|
||||
HydrologyTileCache reverseOrder = new HydrologyTileCache(planner, 8);
|
||||
HydrologyColumnSample secondOwner = reverseOrder.columnAt(ownerColumn.x(), ownerColumn.z()).orElseThrow();
|
||||
HydrologyColumnSample secondNeighbor = reverseOrder
|
||||
.columnAt(neighboringColumn.x(), neighboringColumn.z())
|
||||
.orElseThrow();
|
||||
|
||||
assertEquals(firstNeighbor, secondNeighbor);
|
||||
assertEquals(firstOwner, secondOwner);
|
||||
assertEquals(firstNeighbor.renderSample(), reverseOrder.renderAt(neighboringColumn.x(), neighboringColumn.z()));
|
||||
assertTrue(firstNeighbor.layers().containsAll(neighboringColumn.layers()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void realPlannedFeatureIdsAndTypesReachTheRendererAtExactCoordinates() {
|
||||
HydrologyPlanner planner = new HydrologyPlanner(811L, featureSettings(), this::featureTerrain);
|
||||
HydrologyTileCache cache = new HydrologyTileCache(planner, 8);
|
||||
HydrologyTile planned = cache.get(new HydrologyTileKey(0, 0));
|
||||
HydrologyColumnSample plannedColumn = planned.footprint().columns().values().stream()
|
||||
.filter(HydrologyColumnSample::present)
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
HydrologyColumnSample accepted = cache.columnAt(plannedColumn.x(), plannedColumn.z()).orElseThrow();
|
||||
HydrologyRenderSample rendered = cache.renderAt(plannedColumn.x(), plannedColumn.z());
|
||||
Map<Long, HydrologyFeatureType> acceptedFeatures = new HashMap<>();
|
||||
Map<Long, HydrologyFeatureType> renderedFeatures = new HashMap<>();
|
||||
for (HydrologyColumnLayer layer : accepted.layers()) {
|
||||
acceptedFeatures.put(layer.feature().id(), layer.feature().type());
|
||||
}
|
||||
for (HydrologyFeatureRef feature : rendered.features()) {
|
||||
renderedFeatures.put(feature.id(), feature.type());
|
||||
}
|
||||
|
||||
assertFalse(acceptedFeatures.isEmpty());
|
||||
assertEquals(plannedColumn.x(), rendered.x());
|
||||
assertEquals(plannedColumn.z(), rendered.z());
|
||||
assertEquals(acceptedFeatures, renderedFeatures);
|
||||
for (HydrologyColumnLayer layer : plannedColumn.layers()) {
|
||||
assertEquals(layer.feature().type(), renderedFeatures.get(layer.feature().id()));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void concurrentDifferentTileAndCompositeQueriesRemainDeterministic() throws Exception {
|
||||
HydrologyPlanner planner = new HydrologyPlanner(1211L, featureSettings(), this::featureTerrain);
|
||||
HydrologyTileCache cache = new HydrologyTileCache(planner, 16);
|
||||
List<HydrologyTileKey> keys = List.of(
|
||||
new HydrologyTileKey(-1, 0),
|
||||
new HydrologyTileKey(0, 0),
|
||||
new HydrologyTileKey(1, 0)
|
||||
);
|
||||
ExecutorService executor = Executors.newFixedThreadPool(12);
|
||||
try {
|
||||
ArrayList<Callable<HydrologyTile>> tileTasks = new ArrayList<>();
|
||||
for (int index = 0; index < 36; index++) {
|
||||
HydrologyTileKey key = keys.get(index % keys.size());
|
||||
tileTasks.add(() -> cache.get(key));
|
||||
}
|
||||
List<Future<HydrologyTile>> futures = executor.invokeAll(tileTasks);
|
||||
Map<HydrologyTileKey, HydrologyTile> firstByKey = new HashMap<>();
|
||||
for (int index = 0; index < futures.size(); index++) {
|
||||
HydrologyTileKey key = keys.get(index % keys.size());
|
||||
HydrologyTile tile = futures.get(index).get(10L, TimeUnit.SECONDS);
|
||||
HydrologyTile existing = firstByKey.putIfAbsent(key, tile);
|
||||
if (existing != null) {
|
||||
assertSame(existing, tile);
|
||||
}
|
||||
assertEquals(planner.plan(key), tile);
|
||||
}
|
||||
HydrologyColumnSample outside = planner.plan(new HydrologyTileKey(0, 0)).footprint().columns().values()
|
||||
.stream()
|
||||
.filter((HydrologyColumnSample column) -> column.x() >= 64 && !column.ocean())
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
HydrologyColumnSample expected = cache.columnAt(outside.x(), outside.z()).orElseThrow();
|
||||
ArrayList<Callable<HydrologyColumnSample>> columnTasks = new ArrayList<>();
|
||||
for (int index = 0; index < 24; index++) {
|
||||
columnTasks.add(() -> cache.columnAt(outside.x(), outside.z()).orElseThrow());
|
||||
}
|
||||
for (Future<HydrologyColumnSample> future : executor.invokeAll(columnTasks)) {
|
||||
assertEquals(expected, future.get(10L, TimeUnit.SECONDS));
|
||||
assertEquals(expected.renderSample(), future.get(10L, TimeUnit.SECONDS).renderSample());
|
||||
}
|
||||
} finally {
|
||||
executor.shutdownNow();
|
||||
assertTrue(executor.awaitTermination(10L, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
|
||||
private HydrologyPlannerSettings emptySettings() {
|
||||
HydrologyPlannerSettings.Source disabled = new HydrologyPlannerSettings.Source(
|
||||
false,
|
||||
0D,
|
||||
Integer.MIN_VALUE,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
);
|
||||
return new HydrologyPlannerSettings(
|
||||
63,
|
||||
new HydrologyPlannerSettings.Routing(64, 16, 4, 128, 64,
|
||||
new HydrologyPlannerSettings.Branching(0, 0), 0D, 0D, 0D, 0D),
|
||||
new HydrologyPlannerSettings.Surface(false, disabled, 2, 4, 1, 2, 2, 4, 4, 1D, 2, 4, false, 0, 0),
|
||||
new HydrologyPlannerSettings.Hydraulics(8, 16, 1, 2, 8, 3),
|
||||
new HydrologyPlannerSettings.Underground(false, disabled, -32, 32, 2, 4, 1, 2, 3, 4, false),
|
||||
new HydrologyPlannerSettings.Outlets(
|
||||
false,
|
||||
new HydrologyPlannerSettings.Grotto(false, 2, 2, 2, 512),
|
||||
new HydrologyPlannerSettings.Grotto(false, 2, 2, 2, 512),
|
||||
false,
|
||||
8,
|
||||
16,
|
||||
1,
|
||||
2
|
||||
),
|
||||
HydrologyPlannerSettings.Geometry.defaults(),
|
||||
List.of()
|
||||
);
|
||||
}
|
||||
|
||||
private HydrologyPlannerSettings featureSettings() {
|
||||
HydrologyPlannerSettings.Source surfaceSources = new HydrologyPlannerSettings.Source(
|
||||
true,
|
||||
1D,
|
||||
80,
|
||||
0,
|
||||
1,
|
||||
16
|
||||
);
|
||||
HydrologyPlannerSettings.Source disabled = new HydrologyPlannerSettings.Source(
|
||||
false,
|
||||
0D,
|
||||
Integer.MIN_VALUE,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
);
|
||||
return new HydrologyPlannerSettings(
|
||||
63,
|
||||
new HydrologyPlannerSettings.Routing(64, 16, 4, 128, 96,
|
||||
new HydrologyPlannerSettings.Branching(16, 8), 0.5D, 12D, 0.5D, 0.1D),
|
||||
new HydrologyPlannerSettings.Surface(true, surfaceSources, 4, 8, 2, 3, 4, 8, 20, 1.5D, 2, 4, true, 96, 8),
|
||||
new HydrologyPlannerSettings.Hydraulics(8, 16, 1, 2, 8, 3),
|
||||
new HydrologyPlannerSettings.Underground(false, disabled, -32, 32, 2, 4, 1, 2, 3, 4, false),
|
||||
new HydrologyPlannerSettings.Outlets(
|
||||
true,
|
||||
new HydrologyPlannerSettings.Grotto(false, 2, 2, 2, 512),
|
||||
new HydrologyPlannerSettings.Grotto(false, 2, 2, 2, 512),
|
||||
false,
|
||||
8,
|
||||
16,
|
||||
1,
|
||||
2
|
||||
),
|
||||
HydrologyPlannerSettings.Geometry.defaults(),
|
||||
List.of()
|
||||
);
|
||||
}
|
||||
|
||||
private HydrologyTerrainSample featureTerrain(int x, int z) {
|
||||
if (x >= 80) {
|
||||
return HydrologyTerrainSample.ocean(54, "ocean_parent");
|
||||
}
|
||||
boolean source = x >= 0 && x <= 16;
|
||||
return new HydrologyTerrainSample(
|
||||
104 - Math.floorDiv(x, 8),
|
||||
1D,
|
||||
false,
|
||||
false,
|
||||
40,
|
||||
42,
|
||||
true,
|
||||
true,
|
||||
source,
|
||||
source,
|
||||
false,
|
||||
false,
|
||||
0D,
|
||||
source ? 1D : 0D,
|
||||
0D,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
"parent",
|
||||
"surface",
|
||||
"mouth",
|
||||
"shore",
|
||||
"dry",
|
||||
"flooded",
|
||||
List.of("default")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package art.arcane.iris.engine.hydrology.cave;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class CavePositionIndexTest {
|
||||
@Test
|
||||
public void preservesDistinctFullIntegerCoordinatesAcrossGrowth() {
|
||||
CavePositionIndex index = new CavePositionIndex(1);
|
||||
for (int value = -128; value <= 128; value++) {
|
||||
assertTrue(index.add(value, value * 31, value * -17));
|
||||
}
|
||||
assertTrue(index.add(Integer.MIN_VALUE, Integer.MAX_VALUE, 0));
|
||||
assertTrue(index.add(Integer.MAX_VALUE, Integer.MIN_VALUE, -1));
|
||||
|
||||
for (int value = -128; value <= 128; value++) {
|
||||
assertTrue(index.contains(value, value * 31, value * -17));
|
||||
assertFalse(index.add(value, value * 31, value * -17));
|
||||
}
|
||||
assertTrue(index.contains(Integer.MIN_VALUE, Integer.MAX_VALUE, 0));
|
||||
assertTrue(index.contains(Integer.MAX_VALUE, Integer.MIN_VALUE, -1));
|
||||
assertFalse(index.contains(Integer.MIN_VALUE, Integer.MAX_VALUE, 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void copiedIndexMatchesOnlyExactTriples() {
|
||||
CavePositionIndex index = CavePositionIndex.copyOf(List.of(
|
||||
new CavePosition(1, 2, 3),
|
||||
new CavePosition(3, 2, 1)
|
||||
));
|
||||
|
||||
assertTrue(index.contains(1, 2, 3));
|
||||
assertTrue(index.contains(3, 2, 1));
|
||||
assertFalse(index.contains(1, 3, 2));
|
||||
}
|
||||
}
|
||||
+1324
File diff suppressed because it is too large.
Load diff
+150
@@ -0,0 +1,150 @@
|
||||
package art.arcane.iris.engine.hydrology.cave;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.OptionalLong;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public final class HydrologyCaveIterationOrderTest {
|
||||
@Test
|
||||
public void candidatePreservesActionAndOpeningOrder() {
|
||||
CavePosition first = new CavePosition(3, 20, 7);
|
||||
CavePosition second = new CavePosition(-4, 11, 2);
|
||||
LinkedHashMap<CavePosition, HydrologyCaveAction> actions = new LinkedHashMap<>();
|
||||
actions.put(first, HydrologyCaveAction.DRY_AIR);
|
||||
actions.put(second, HydrologyCaveAction.WET_SOURCE);
|
||||
LinkedHashSet<CavePosition> openings = new LinkedHashSet<>(List.of(second, first));
|
||||
|
||||
HydrologyCaveCandidate candidate = new HydrologyCaveCandidate(
|
||||
source(),
|
||||
"water",
|
||||
settings(),
|
||||
false,
|
||||
actions,
|
||||
openings
|
||||
);
|
||||
|
||||
assertEquals(List.of(first, second), new ArrayList<>(candidate.actions().keySet()));
|
||||
assertEquals(List.of(second, first), new ArrayList<>(candidate.intentionalOpenings()));
|
||||
actions.clear();
|
||||
openings.clear();
|
||||
assertEquals(List.of(first, second), new ArrayList<>(candidate.actions().keySet()));
|
||||
assertEquals(List.of(second, first), new ArrayList<>(candidate.intentionalOpenings()));
|
||||
assertThrows(
|
||||
UnsupportedOperationException.class,
|
||||
() -> candidate.actions().put(first, HydrologyCaveAction.WET_SOURCE)
|
||||
);
|
||||
assertThrows(UnsupportedOperationException.class, () -> candidate.intentionalOpenings().add(second));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptedPlanAndPlanningResultPreserveMapOrder() {
|
||||
CavePosition first = new CavePosition(3, 20, 7);
|
||||
CavePosition second = new CavePosition(-4, 11, 2);
|
||||
LinkedHashMap<CavePosition, HydrologyCaveAction> actions = new LinkedHashMap<>();
|
||||
actions.put(first, HydrologyCaveAction.DRY_AIR);
|
||||
actions.put(second, HydrologyCaveAction.WET_SOURCE);
|
||||
LinkedHashMap<CavePosition, CaveVoxelPrecondition> preconditions = new LinkedHashMap<>();
|
||||
preconditions.put(second, new CaveVoxelPrecondition(CaveVoxel.SOLID, false));
|
||||
preconditions.put(first, new CaveVoxelPrecondition(CaveVoxel.CAVE_AIR, false));
|
||||
HydrologyCavePlan plan = new HydrologyCavePlan(
|
||||
source(),
|
||||
HydrologyCaveRejection.NONE,
|
||||
actions,
|
||||
preconditions,
|
||||
OptionalLong.empty()
|
||||
);
|
||||
HydrologyCavePlanningResult result = new HydrologyCavePlanningResult(
|
||||
List.of(plan),
|
||||
actions,
|
||||
preconditions
|
||||
);
|
||||
|
||||
assertEquals(List.of(first, second), new ArrayList<>(plan.actions().keySet()));
|
||||
assertEquals(List.of(second, first), new ArrayList<>(plan.baselinePreconditions().keySet()));
|
||||
assertEquals(List.of(first, second), new ArrayList<>(result.actions().keySet()));
|
||||
assertEquals(List.of(second, first), new ArrayList<>(result.baselinePreconditions().keySet()));
|
||||
actions.clear();
|
||||
preconditions.clear();
|
||||
assertEquals(List.of(first, second), new ArrayList<>(plan.actions().keySet()));
|
||||
assertEquals(List.of(second, first), new ArrayList<>(plan.baselinePreconditions().keySet()));
|
||||
assertEquals(List.of(first, second), new ArrayList<>(result.actions().keySet()));
|
||||
assertEquals(List.of(second, first), new ArrayList<>(result.baselinePreconditions().keySet()));
|
||||
assertThrows(UnsupportedOperationException.class, () -> plan.actions().remove(first));
|
||||
assertThrows(UnsupportedOperationException.class, () -> result.baselinePreconditions().remove(second));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void packedPlanViewsImplementTheReadOnlyMapContract() {
|
||||
CavePosition dry = new CavePosition(3, 20, 7);
|
||||
CavePosition wet = new CavePosition(-4, 11, 2);
|
||||
CavePosition guardOnly = new CavePosition(9, 4, -6);
|
||||
CaveVoxelPrecondition dryBaseline = new CaveVoxelPrecondition(CaveVoxel.CAVE_AIR, false);
|
||||
CaveVoxelPrecondition wetBaseline = new CaveVoxelPrecondition(CaveVoxel.SOLID, true);
|
||||
CaveVoxelPrecondition guardBaseline = new CaveVoxelPrecondition(CaveVoxel.LAVA, false);
|
||||
Map<CavePosition, HydrologyCaveAction> actions = new LinkedHashMap<>();
|
||||
actions.put(dry, HydrologyCaveAction.DRY_AIR);
|
||||
actions.put(wet, HydrologyCaveAction.WET_SOURCE);
|
||||
Map<CavePosition, CaveVoxelPrecondition> preconditions = new LinkedHashMap<>();
|
||||
preconditions.put(guardOnly, guardBaseline);
|
||||
preconditions.put(wet, wetBaseline);
|
||||
preconditions.put(dry, dryBaseline);
|
||||
HydrologyCavePlan plan = new HydrologyCavePlan(
|
||||
source(),
|
||||
HydrologyCaveRejection.NONE,
|
||||
actions,
|
||||
preconditions,
|
||||
OptionalLong.empty()
|
||||
);
|
||||
|
||||
assertEquals(HydrologyCaveAction.DRY_AIR, plan.actions().get(dry));
|
||||
assertNull(plan.actions().get(guardOnly));
|
||||
assertFalse(plan.actions().containsKey(guardOnly));
|
||||
assertTrue(plan.baselinePreconditions().containsKey(guardOnly));
|
||||
assertEquals(guardBaseline, plan.baselinePreconditions().get(guardOnly));
|
||||
assertEquals(actions, plan.actions());
|
||||
assertEquals(preconditions, plan.baselinePreconditions());
|
||||
assertEquals(
|
||||
List.of(HydrologyCaveAction.DRY_AIR, HydrologyCaveAction.WET_SOURCE),
|
||||
new ArrayList<>(plan.actions().values())
|
||||
);
|
||||
assertEquals(
|
||||
List.of(guardBaseline, wetBaseline, dryBaseline),
|
||||
new ArrayList<>(plan.baselinePreconditions().values())
|
||||
);
|
||||
assertThrows(UnsupportedOperationException.class, () -> plan.actions().clear());
|
||||
assertThrows(UnsupportedOperationException.class, () -> plan.baselinePreconditions().keySet().clear());
|
||||
}
|
||||
|
||||
private HydrologyCaveSource source() {
|
||||
CavePosition source = new CavePosition(3, 20, 7);
|
||||
return new HydrologyCaveSource(17L, source, source, 20, HydrologyCaveMode.GENERATED_GROTTO);
|
||||
}
|
||||
|
||||
private HydrologyCavePlannerSettings settings() {
|
||||
return new HydrologyCavePlannerSettings(
|
||||
8,
|
||||
16,
|
||||
64,
|
||||
16,
|
||||
1,
|
||||
2,
|
||||
2,
|
||||
4,
|
||||
HydrologyCaveFluidPolicy.REJECT_EXISTING,
|
||||
HydrologyCaveGrottoShape.ELLIPSOID,
|
||||
8,
|
||||
16
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package art.arcane.iris.engine.hydrology.cave;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.OptionalLong;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class HydrologyCavePlanTest {
|
||||
@Test
|
||||
public void everyActionRequiresABaselinePrecondition() {
|
||||
CavePosition actionPosition = new CavePosition(1, 20, 3);
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyCavePlan(
|
||||
source(),
|
||||
HydrologyCaveRejection.NONE,
|
||||
Map.of(actionPosition, HydrologyCaveAction.WET_SOURCE),
|
||||
Map.of(),
|
||||
OptionalLong.empty()
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void extraDryBoundaryPreconditionsAreAllowed() {
|
||||
CavePosition actionPosition = new CavePosition(1, 20, 3);
|
||||
CavePosition dryBoundaryPosition = new CavePosition(2, 20, 3);
|
||||
CaveVoxelPrecondition solid = new CaveVoxelPrecondition(CaveVoxel.SOLID, false);
|
||||
CaveVoxelPrecondition dryBoundary = new CaveVoxelPrecondition(CaveVoxel.CAVE_AIR, false);
|
||||
HydrologyCavePlan plan = new HydrologyCavePlan(
|
||||
source(),
|
||||
HydrologyCaveRejection.NONE,
|
||||
Map.of(actionPosition, HydrologyCaveAction.WET_SOURCE),
|
||||
Map.of(actionPosition, solid, dryBoundaryPosition, dryBoundary),
|
||||
OptionalLong.empty()
|
||||
);
|
||||
|
||||
assertEquals(1, plan.actions().size());
|
||||
assertEquals(2, plan.baselinePreconditions().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void boundedQueriesVisitOnlyExactPositionsAcrossNegativeChunkBoundaries() {
|
||||
CavePosition inside = new CavePosition(-16, 20, 31);
|
||||
CavePosition outsideX = new CavePosition(-17, 21, 31);
|
||||
CavePosition outsideZ = new CavePosition(-16, 22, 32);
|
||||
CavePosition preconditionOnly = new CavePosition(-15, 23, 30);
|
||||
CaveVoxelPrecondition solid = new CaveVoxelPrecondition(CaveVoxel.SOLID, false);
|
||||
LinkedHashMap<CavePosition, HydrologyCaveAction> actions = new LinkedHashMap<>();
|
||||
actions.put(outsideX, HydrologyCaveAction.DRY_AIR);
|
||||
actions.put(inside, HydrologyCaveAction.WET_SOURCE);
|
||||
actions.put(outsideZ, HydrologyCaveAction.SEAL_GUARD);
|
||||
LinkedHashMap<CavePosition, CaveVoxelPrecondition> preconditions = new LinkedHashMap<>();
|
||||
preconditions.put(outsideX, solid);
|
||||
preconditions.put(inside, solid);
|
||||
preconditions.put(outsideZ, solid);
|
||||
preconditions.put(preconditionOnly, solid);
|
||||
HydrologyCavePlan plan = new HydrologyCavePlan(
|
||||
source(),
|
||||
HydrologyCaveRejection.NONE,
|
||||
actions,
|
||||
preconditions,
|
||||
OptionalLong.empty()
|
||||
);
|
||||
|
||||
assertTrue(plan.intersectsActions(-16, 30, 0, 32));
|
||||
assertFalse(plan.intersectsActions(-15, 30, 0, 31));
|
||||
ArrayList<CavePosition> visitedActions = new ArrayList<>();
|
||||
plan.forEachActionIn(-16, 30, 0, 32,
|
||||
(CavePosition position, HydrologyCaveAction action) -> visitedActions.add(position));
|
||||
assertEquals(List.of(inside), visitedActions);
|
||||
ArrayList<CavePosition> visitedPreconditions = new ArrayList<>();
|
||||
assertTrue(plan.allPreconditionsIn(-16, 30, 0, 32,
|
||||
(CavePosition position, CaveVoxelPrecondition expected) -> {
|
||||
visitedPreconditions.add(position);
|
||||
return true;
|
||||
}));
|
||||
assertEquals(List.of(inside, preconditionOnly), visitedPreconditions);
|
||||
}
|
||||
|
||||
private HydrologyCaveSource source() {
|
||||
return new HydrologyCaveSource(
|
||||
1L,
|
||||
new CavePosition(1, 20, 3),
|
||||
new CavePosition(2, 20, 3),
|
||||
20,
|
||||
HydrologyCaveMode.CLOSED_COMPONENT
|
||||
);
|
||||
}
|
||||
}
|
||||
+11
-11
@@ -1,4 +1,4 @@
|
||||
package art.arcane.iris.engine.river.cave;
|
||||
package art.arcane.iris.engine.hydrology.cave;
|
||||
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
@@ -12,23 +12,23 @@ import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class RiverCaveHydrologyStorageTest {
|
||||
public class HydrologyCaveStorageTest {
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void presentReadDoesNotMaterializeASectionOrSlice() {
|
||||
MantleChunk<Matter> chunk = mock(MantleChunk.class);
|
||||
Matter matter = mock(Matter.class);
|
||||
MatterSlice<RiverCaveHydrology> slice = mock(MatterSlice.class);
|
||||
RiverCaveHydrology expected = RiverCaveHydrology.of(RiverCaveAction.WET_SOURCE);
|
||||
MatterSlice<HydrologyCaveCell> slice = mock(MatterSlice.class);
|
||||
HydrologyCaveCell expected = HydrologyCaveCell.of(HydrologyCaveAction.WET_SOURCE);
|
||||
when(chunk.exists(2)).thenReturn(true);
|
||||
when(chunk.get(2)).thenReturn(matter);
|
||||
when(matter.hasSlice(RiverCaveHydrology.class)).thenReturn(true);
|
||||
when(matter.getSlice(RiverCaveHydrology.class)).thenReturn(slice);
|
||||
when(matter.hasSlice(HydrologyCaveCell.class)).thenReturn(true);
|
||||
when(matter.getSlice(HydrologyCaveCell.class)).thenReturn(slice);
|
||||
when(slice.get(3, 1, 5)).thenReturn(expected);
|
||||
|
||||
assertSame(expected, RiverCaveHydrologyStorage.getIfPresent(chunk, 3, 33, 5));
|
||||
assertSame(expected, HydrologyCaveStorage.getIfPresent(chunk, 3, 33, 5));
|
||||
verify(chunk, never()).getOrCreate(2);
|
||||
verify(matter, never()).slice(RiverCaveHydrology.class);
|
||||
verify(matter, never()).slice(HydrologyCaveCell.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -39,8 +39,8 @@ public class RiverCaveHydrologyStorageTest {
|
||||
when(chunk.exists(2)).thenReturn(true);
|
||||
when(chunk.get(2)).thenReturn(matter);
|
||||
|
||||
assertNull(RiverCaveHydrologyStorage.getIfPresent(chunk, 3, 33, 5));
|
||||
verify(matter, never()).slice(RiverCaveHydrology.class);
|
||||
verify(matter, never()).getSlice(RiverCaveHydrology.class);
|
||||
assertNull(HydrologyCaveStorage.getIfPresent(chunk, 3, 33, 5));
|
||||
verify(matter, never()).slice(HydrologyCaveCell.class);
|
||||
verify(matter, never()).getSlice(HydrologyCaveCell.class);
|
||||
}
|
||||
}
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
package art.arcane.iris.engine.hydrology.policy;
|
||||
|
||||
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.IrisRiverPlacementMode;
|
||||
import art.arcane.iris.engine.object.IrisRiverPolicy;
|
||||
import art.arcane.iris.engine.object.IrisRiverRoutingMode;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class RiverPolicyResolverTest {
|
||||
@Test
|
||||
public void suppliesCanonicalDefaultsWithoutConfiguredPolicies() {
|
||||
EffectiveRiverPolicy policy = RiverPolicyResolver.resolve(
|
||||
(IrisRiverPolicy) null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
assertEquals(IrisRiverPlacementMode.NATURAL, policy.placement());
|
||||
assertEquals(IrisRiverRoutingMode.ALLOW, policy.routing());
|
||||
assertTrue(policy.outletAdmission());
|
||||
assertTrue(policy.profiles().isEmpty());
|
||||
assertEquals(1D, policy.widthMultiplier(), 0D);
|
||||
assertEquals(1D, policy.depthMultiplier(), 0D);
|
||||
assertEquals(1D, policy.incisionMultiplier(), 0D);
|
||||
assertEquals(1D, policy.routingMultiplier(), 0D);
|
||||
assertTrue(policy.allowsSources());
|
||||
assertTrue(policy.allowsTransit());
|
||||
assertTrue(policy.allowsRouting());
|
||||
assertFalse(policy.prefersHeadwaters());
|
||||
assertFalse(policy.requiresHeadwaters());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void overlaysDimensionThenRegionThenBiomeIncludingExplicitEmptyLists() {
|
||||
IrisRiverPolicy dimension = new IrisRiverPolicy()
|
||||
.setPlacement(IrisRiverPlacementMode.NATURAL)
|
||||
.setRouting(IrisRiverRoutingMode.AVOID)
|
||||
.setOutletAdmission(false)
|
||||
.setProfiles(new KList<>("water"))
|
||||
.setSurfaceBiomes(new KList<>("river/dimension"))
|
||||
.setMouthBiomes(new KList<>("river/mouth"))
|
||||
.setWidthMultiplier(1.5D)
|
||||
.setDepthMultiplier(1.25D);
|
||||
IrisRiverPolicy region = new IrisRiverPolicy()
|
||||
.setRouting(IrisRiverRoutingMode.PREFER)
|
||||
.setProfiles(new KList<>())
|
||||
.setSurfaceBiomes(new KList<>("river/region"))
|
||||
.setShoreBiomes(new KList<>("river/shore"))
|
||||
.setWidthMultiplier(0.8D)
|
||||
.setRoutingMultiplier(2D);
|
||||
IrisRiverPolicy biome = new IrisRiverPolicy()
|
||||
.setPlacement(IrisRiverPlacementMode.REQUIRED_HEADWATER)
|
||||
.setRouting(IrisRiverRoutingMode.BLOCK)
|
||||
.setOutletAdmission(true)
|
||||
.setSurfaceBiomes(new KList<>())
|
||||
.setDryBiomes(new KList<>("river/dry"))
|
||||
.setFloodedCaveBiomes(new KList<>("river/flooded"))
|
||||
.setDepthMultiplier(0.75D)
|
||||
.setIncisionMultiplier(0.5D);
|
||||
|
||||
EffectiveRiverPolicy policy = RiverPolicyResolver.resolve(dimension, region, biome);
|
||||
|
||||
assertEquals(IrisRiverPlacementMode.REQUIRED_HEADWATER, policy.placement());
|
||||
assertEquals(IrisRiverRoutingMode.BLOCK, policy.routing());
|
||||
assertTrue(policy.outletAdmission());
|
||||
assertTrue(policy.profiles().isEmpty());
|
||||
assertTrue(policy.surfaceBiomes().isEmpty());
|
||||
assertEquals(List.of("river/mouth"), policy.mouthBiomes());
|
||||
assertEquals(List.of("river/shore"), policy.shoreBiomes());
|
||||
assertEquals(List.of("river/dry"), policy.dryBiomes());
|
||||
assertEquals(List.of("river/flooded"), policy.floodedCaveBiomes());
|
||||
assertEquals(0.8D, policy.widthMultiplier(), 0D);
|
||||
assertEquals(0.75D, policy.depthMultiplier(), 0D);
|
||||
assertEquals(0.5D, policy.incisionMultiplier(), 0D);
|
||||
assertEquals(2D, policy.routingMultiplier(), 0D);
|
||||
assertTrue(policy.allowsSources());
|
||||
assertTrue(policy.allowsTransit());
|
||||
assertFalse(policy.allowsRouting());
|
||||
assertTrue(policy.prefersHeadwaters());
|
||||
assertTrue(policy.requiresHeadwaters());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolvesOwnerPoliciesAndSnapshotsMutableLists() {
|
||||
KList<String> profiles = new KList<>("water");
|
||||
IrisDimension dimension = new IrisDimension().setRiverPolicy(new IrisRiverPolicy()
|
||||
.setProfiles(profiles)
|
||||
.setPlacement(IrisRiverPlacementMode.PREFERRED_HEADWATER));
|
||||
IrisRegion region = new IrisRegion().setRiverPolicy(new IrisRiverPolicy()
|
||||
.setRouting(IrisRiverRoutingMode.PREFER));
|
||||
IrisBiome biome = new IrisBiome().setRiverPolicy(new IrisRiverPolicy()
|
||||
.setMouthBiomes(new KList<>("river/mouth")));
|
||||
|
||||
EffectiveRiverPolicy policy = RiverPolicyResolver.resolve(dimension, region, biome);
|
||||
profiles.add("lava");
|
||||
|
||||
assertEquals(List.of("water"), policy.profiles());
|
||||
assertEquals(List.of("river/mouth"), policy.mouthBiomes());
|
||||
assertEquals(IrisRiverPlacementMode.PREFERRED_HEADWATER, policy.placement());
|
||||
assertEquals(IrisRiverRoutingMode.PREFER, policy.routing());
|
||||
assertThrows(UnsupportedOperationException.class, () -> policy.profiles().add("other"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void placementModesExposeDistinctAdmissionSemantics() {
|
||||
EffectiveRiverPolicy disabled = resolvePlacement(IrisRiverPlacementMode.DISABLED);
|
||||
EffectiveRiverPolicy transitOnly = resolvePlacement(IrisRiverPlacementMode.TRANSIT_ONLY);
|
||||
EffectiveRiverPolicy natural = resolvePlacement(IrisRiverPlacementMode.NATURAL);
|
||||
EffectiveRiverPolicy preferred = resolvePlacement(IrisRiverPlacementMode.PREFERRED_HEADWATER);
|
||||
EffectiveRiverPolicy required = resolvePlacement(IrisRiverPlacementMode.REQUIRED_HEADWATER);
|
||||
|
||||
assertFalse(disabled.allowsSources());
|
||||
assertFalse(disabled.allowsTransit());
|
||||
assertFalse(transitOnly.allowsSources());
|
||||
assertTrue(transitOnly.allowsTransit());
|
||||
assertTrue(natural.allowsSources());
|
||||
assertFalse(natural.prefersHeadwaters());
|
||||
assertTrue(preferred.prefersHeadwaters());
|
||||
assertFalse(preferred.requiresHeadwaters());
|
||||
assertTrue(required.requiresHeadwaters());
|
||||
}
|
||||
|
||||
private EffectiveRiverPolicy resolvePlacement(IrisRiverPlacementMode placement) {
|
||||
return RiverPolicyResolver.resolve(new IrisRiverPolicy().setPlacement(placement), null, null);
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package art.arcane.iris.engine.hydrology.runtime;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class HydrologyFeatureSearchBoundsTest {
|
||||
@Test
|
||||
public void preservesRequestedDistanceWhenPublicationHaloFits() {
|
||||
int accepted = HydrologyFeatureSearchBounds.maximumDistance(
|
||||
0, 0, 1024, 256, 128, 1_089);
|
||||
|
||||
assertEquals(1024, accepted);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reducesDistanceToTheLargestSafeTileWindow() {
|
||||
int requested = 8192;
|
||||
int accepted = HydrologyFeatureSearchBounds.maximumDistance(
|
||||
255, -129, requested, 256, 2_730, 1_089);
|
||||
|
||||
assertTrue(accepted < requested);
|
||||
assertTrue(HydrologyFeatureSearchBounds.tileCount(
|
||||
255, -129, accepted, 256, 2_730) <= 1_089L);
|
||||
assertTrue(HydrologyFeatureSearchBounds.tileCount(
|
||||
255, -129, accepted + 1, 256, 2_730) > 1_089L);
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
package art.arcane.iris.engine.hydrology.runtime;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class IrisHydrologyBiomePatchTest {
|
||||
@Test
|
||||
public void twoKeysFormCoherentDeterministicPatchesWithoutCheckerCells() {
|
||||
int minimum = -64;
|
||||
int maximum = 64;
|
||||
int adjacentPairs = 0;
|
||||
int adjacentMatches = 0;
|
||||
int checkerCells = 0;
|
||||
Set<Integer> selected = new HashSet<>();
|
||||
|
||||
for (int z = minimum; z <= maximum; z++) {
|
||||
for (int x = minimum; x <= maximum; x++) {
|
||||
int value = IrisHydrologyRuntime.coherentKeyIndex(1337L, x, z, 3, 2);
|
||||
assertEquals(value, IrisHydrologyRuntime.coherentKeyIndex(1337L, x, z, 3, 2));
|
||||
selected.add(value);
|
||||
if (x < maximum) {
|
||||
adjacentPairs++;
|
||||
adjacentMatches += value == IrisHydrologyRuntime.coherentKeyIndex(
|
||||
1337L,
|
||||
x + 1,
|
||||
z,
|
||||
3,
|
||||
2
|
||||
) ? 1 : 0;
|
||||
}
|
||||
if (z < maximum) {
|
||||
adjacentPairs++;
|
||||
adjacentMatches += value == IrisHydrologyRuntime.coherentKeyIndex(
|
||||
1337L,
|
||||
x,
|
||||
z + 1,
|
||||
3,
|
||||
2
|
||||
) ? 1 : 0;
|
||||
}
|
||||
if (x == maximum || z == maximum) {
|
||||
continue;
|
||||
}
|
||||
int east = IrisHydrologyRuntime.coherentKeyIndex(1337L, x + 1, z, 3, 2);
|
||||
int south = IrisHydrologyRuntime.coherentKeyIndex(1337L, x, z + 1, 3, 2);
|
||||
int southEast = IrisHydrologyRuntime.coherentKeyIndex(1337L, x + 1, z + 1, 3, 2);
|
||||
if (value == southEast && east == south && value != east) {
|
||||
checkerCells++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(Set.of(0, 1), selected);
|
||||
assertTrue(adjacentMatches > adjacentPairs * 0.9D);
|
||||
assertEquals(0, checkerCells);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void oneKeyAlwaysSelectsTheOnlyEntry() {
|
||||
assertEquals(0, IrisHydrologyRuntime.coherentKeyIndex(9L, Integer.MIN_VALUE, Integer.MAX_VALUE, 5, 1));
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void emptyKeySetIsRejected() {
|
||||
IrisHydrologyRuntime.coherentKeyIndex(9L, 0, 0, 5, 0);
|
||||
}
|
||||
}
|
||||
+594
@@ -0,0 +1,594 @@
|
||||
package art.arcane.iris.engine.hydrology.runtime;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.HydrologyRoutingTerrainSampler;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyTerrainSample;
|
||||
import art.arcane.iris.util.common.parallel.MultiBurst;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executor;
|
||||
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.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class IrisHydrologyRoutingTerrainSamplerTest {
|
||||
@Test
|
||||
public void guardedGridSamplesOneAlignedBasisAtEachCoordinate() {
|
||||
int[] spacings = new int[]{64, 128, 256};
|
||||
for (int spacing : spacings) {
|
||||
int minimumX = -4096;
|
||||
int minimumZ = -2048;
|
||||
int width = 4096 / spacing + 1;
|
||||
AtomicInteger basisCalls = new AtomicInteger();
|
||||
AtomicInteger heightCalls = new AtomicInteger();
|
||||
Map<Long, Integer> coordinateCalls = new HashMap<>();
|
||||
IrisHydrologyRoutingTerrainSampler sampler = new IrisHydrologyRoutingTerrainSampler(
|
||||
new IrisHydrologyRoutingTerrainSampler.Sources(
|
||||
(int x, int z, double naturalHeight) -> {
|
||||
basisCalls.incrementAndGet();
|
||||
return basis(x, z, naturalHeight);
|
||||
},
|
||||
(int x, int z) -> {
|
||||
assertEquals(0, Math.floorMod(x - minimumX, spacing));
|
||||
assertEquals(0, Math.floorMod(z - minimumZ, spacing));
|
||||
heightCalls.incrementAndGet();
|
||||
coordinateCalls.merge(pack(x, z), 1, Integer::sum);
|
||||
return height(x, z);
|
||||
},
|
||||
(int x, int z) -> false
|
||||
),
|
||||
IrisHydrologyRoutingTerrainSampler.SamplingOptions.serial(65_536)
|
||||
);
|
||||
|
||||
HydrologyTerrainSample[] samples = sampler.sampleGrid(new HydrologyRoutingTerrainSampler.GridRequest(
|
||||
minimumX,
|
||||
minimumZ,
|
||||
width,
|
||||
spacing
|
||||
));
|
||||
|
||||
assertEquals(width * width, samples.length);
|
||||
assertEquals((width + 1) * (width + 1), basisCalls.get());
|
||||
assertEquals(basisCalls.get(), heightCalls.get());
|
||||
assertEquals(heightCalls.get(), coordinateCalls.size());
|
||||
for (int count : coordinateCalls.values()) {
|
||||
assertEquals(1, count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void guardedForwardSlopeMatchesRawBitsAcrossNegativeBoundaries() {
|
||||
int minimumX = -129;
|
||||
int minimumZ = -257;
|
||||
int width = 3;
|
||||
int spacing = 64;
|
||||
IrisHydrologyRoutingTerrainSampler sampler = sampler(64);
|
||||
|
||||
HydrologyTerrainSample[] samples = sampler.sampleGrid(
|
||||
new HydrologyRoutingTerrainSampler.GridRequest(minimumX, minimumZ, width, spacing)
|
||||
);
|
||||
|
||||
for (int gridZ = 0; gridZ < width; gridZ++) {
|
||||
int z = minimumZ + gridZ * spacing;
|
||||
for (int gridX = 0; gridX < width; gridX++) {
|
||||
int x = minimumX + gridX * spacing;
|
||||
double scale = 3D / spacing;
|
||||
double deltaX = (height(x + spacing, z) - height(x, z)) * scale;
|
||||
double deltaZ = (height(x, z + spacing) - height(x, z)) * scale;
|
||||
double expected = StrictMath.sqrt(deltaX * deltaX + deltaZ * deltaZ);
|
||||
double actual = samples[gridZ * width + gridX].slope();
|
||||
assertEquals(Double.doubleToRawLongBits(expected), Double.doubleToRawLongBits(actual));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void adjacentGridsShareBasesAndProduceIdenticalOverlap() {
|
||||
AtomicInteger calls = new AtomicInteger();
|
||||
IrisHydrologyRoutingTerrainSampler sampler = new IrisHydrologyRoutingTerrainSampler(
|
||||
new IrisHydrologyRoutingTerrainSampler.Sources(
|
||||
(int x, int z, double naturalHeight) -> basis(x, z, naturalHeight),
|
||||
(int x, int z) -> {
|
||||
calls.incrementAndGet();
|
||||
return height(x, z);
|
||||
},
|
||||
(int x, int z) -> false
|
||||
),
|
||||
IrisHydrologyRoutingTerrainSampler.SamplingOptions.serial(64)
|
||||
);
|
||||
|
||||
HydrologyTerrainSample[] first = sampler.sampleGrid(
|
||||
new HydrologyRoutingTerrainSampler.GridRequest(-128, -128, 3, 64)
|
||||
);
|
||||
assertEquals(16, calls.get());
|
||||
HydrologyTerrainSample[] second = sampler.sampleGrid(
|
||||
new HydrologyRoutingTerrainSampler.GridRequest(0, -128, 3, 64)
|
||||
);
|
||||
|
||||
assertEquals(24, calls.get());
|
||||
for (int gridZ = 0; gridZ < 3; gridZ++) {
|
||||
assertEquals(first[gridZ * 3 + 2], second[gridZ * 3]);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void serialGridKeepsRowMajorProviderCallOrder() {
|
||||
List<String> calls = new ArrayList<>();
|
||||
IrisHydrologyRoutingTerrainSampler sampler = new IrisHydrologyRoutingTerrainSampler(
|
||||
new IrisHydrologyRoutingTerrainSampler.Sources(
|
||||
(int x, int z, double naturalHeight) -> {
|
||||
calls.add("basis:" + x + "," + z);
|
||||
return basis(x, z, naturalHeight);
|
||||
},
|
||||
(int x, int z) -> {
|
||||
calls.add("height:" + x + "," + z);
|
||||
return height(x, z);
|
||||
},
|
||||
(int x, int z) -> false
|
||||
),
|
||||
IrisHydrologyRoutingTerrainSampler.SamplingOptions.serial(64)
|
||||
);
|
||||
|
||||
sampler.sampleGrid(new HydrologyRoutingTerrainSampler.GridRequest(-16, 32, 2, 16));
|
||||
|
||||
assertEquals(
|
||||
List.of(
|
||||
"height:-16,32", "basis:-16,32",
|
||||
"height:0,32", "basis:0,32",
|
||||
"height:16,32", "basis:16,32",
|
||||
"height:-16,48", "basis:-16,48",
|
||||
"height:0,48", "basis:0,48",
|
||||
"height:16,48", "basis:16,48",
|
||||
"height:-16,64", "basis:-16,64",
|
||||
"height:0,64", "basis:0,64",
|
||||
"height:16,64", "basis:16,64"
|
||||
),
|
||||
calls
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void classifierUsesBasisAndCachesExactUnalignedResults() {
|
||||
AtomicInteger classifierCalls = new AtomicInteger();
|
||||
IrisHydrologyRoutingTerrainSampler sampler = new IrisHydrologyRoutingTerrainSampler(
|
||||
new IrisHydrologyRoutingTerrainSampler.Sources(
|
||||
(int x, int z, double naturalHeight) -> oceanBasis(x, z, naturalHeight),
|
||||
(int x, int z) -> height(x, z),
|
||||
(int x, int z) -> {
|
||||
classifierCalls.incrementAndGet();
|
||||
return x >= 0;
|
||||
}
|
||||
),
|
||||
IrisHydrologyRoutingTerrainSampler.SamplingOptions.serial(16)
|
||||
);
|
||||
sampler.sampleGrid(new HydrologyRoutingTerrainSampler.GridRequest(0, 0, 1, 64));
|
||||
|
||||
assertEquals(
|
||||
HydrologyRoutingTerrainSampler.NaturalClassification.OCEAN,
|
||||
sampler.classifyNatural(0, 0)
|
||||
);
|
||||
assertEquals(0, classifierCalls.get());
|
||||
assertEquals(
|
||||
HydrologyRoutingTerrainSampler.NaturalClassification.OCEAN,
|
||||
sampler.classifyNatural(1, -1)
|
||||
);
|
||||
assertEquals(
|
||||
HydrologyRoutingTerrainSampler.NaturalClassification.OCEAN,
|
||||
sampler.classifyNatural(1, -1)
|
||||
);
|
||||
assertEquals(1, classifierCalls.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void detailedSlopeKeepsExactPositiveThreeStencil() {
|
||||
Map<Long, Integer> calls = new HashMap<>();
|
||||
IrisHydrologyRoutingTerrainSampler sampler = new IrisHydrologyRoutingTerrainSampler(
|
||||
new IrisHydrologyRoutingTerrainSampler.Sources(
|
||||
(int x, int z, double naturalHeight) -> basis(x, z, naturalHeight),
|
||||
(int x, int z) -> {
|
||||
calls.merge(pack(x, z), 1, Integer::sum);
|
||||
return height(x, z);
|
||||
},
|
||||
(int x, int z) -> false
|
||||
),
|
||||
IrisHydrologyRoutingTerrainSampler.SamplingOptions.serial(16)
|
||||
);
|
||||
int x = -65;
|
||||
int z = -127;
|
||||
double naturalHeight = sampler.basis(x, z).naturalHeight();
|
||||
|
||||
double actual = sampler.localSlope(x, z, naturalHeight);
|
||||
double deltaX = height(x + 3, z) - naturalHeight;
|
||||
double deltaZ = height(x, z + 3) - naturalHeight;
|
||||
double expected = Math.sqrt(deltaX * deltaX + deltaZ * deltaZ);
|
||||
|
||||
assertEquals(Double.doubleToRawLongBits(expected), Double.doubleToRawLongBits(actual));
|
||||
assertEquals(Map.of(pack(x, z), 1, pack(x + 3, z), 1, pack(x, z + 3), 1), calls);
|
||||
sampler.basis(x + 3, z);
|
||||
sampler.localSlope(x + 3, z, height(x + 3, z));
|
||||
assertEquals(1, calls.get(pack(x + 3, z)).intValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void denseDetailedStencilsShareRawNaturalHeights() {
|
||||
AtomicInteger basisCalls = new AtomicInteger();
|
||||
AtomicInteger heightCalls = new AtomicInteger();
|
||||
IrisHydrologyRoutingTerrainSampler sampler = new IrisHydrologyRoutingTerrainSampler(
|
||||
new IrisHydrologyRoutingTerrainSampler.Sources(
|
||||
(int x, int z, double naturalHeight) -> {
|
||||
basisCalls.incrementAndGet();
|
||||
return basis(x, z, naturalHeight);
|
||||
},
|
||||
(int x, int z) -> {
|
||||
heightCalls.incrementAndGet();
|
||||
return height(x, z);
|
||||
},
|
||||
(int x, int z) -> false
|
||||
),
|
||||
IrisHydrologyRoutingTerrainSampler.SamplingOptions.serial(64)
|
||||
);
|
||||
|
||||
for (int z = 0; z <= 6; z += 3) {
|
||||
for (int x = 0; x <= 6; x += 3) {
|
||||
IrisHydrologyRoutingTerrainSampler.TerrainBasis basis = sampler.basis(x, z);
|
||||
double actual = sampler.localSlope(x, z, basis.naturalHeight());
|
||||
double deltaX = height(x + 3, z) - height(x, z);
|
||||
double deltaZ = height(x, z + 3) - height(x, z);
|
||||
double expected = Math.sqrt(deltaX * deltaX + deltaZ * deltaZ);
|
||||
assertEquals(Double.doubleToRawLongBits(expected), Double.doubleToRawLongBits(actual));
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(9, basisCalls.get());
|
||||
assertEquals(15, heightCalls.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cachesRemainBoundedAndCloseClearsThem() {
|
||||
IrisHydrologyRoutingTerrainSampler sampler = sampler(3);
|
||||
|
||||
sampler.sampleGrid(new HydrologyRoutingTerrainSampler.GridRequest(0, 0, 1, 64));
|
||||
for (int coordinate = 1; coordinate <= 5; coordinate++) {
|
||||
sampler.classifyNatural(coordinate, -coordinate);
|
||||
}
|
||||
|
||||
assertEquals(3, sampler.basisCacheSize());
|
||||
assertEquals(3, sampler.naturalHeightCacheSize());
|
||||
assertEquals(3, sampler.oceanClassificationCacheSize());
|
||||
sampler.close();
|
||||
assertEquals(0, sampler.basisCacheSize());
|
||||
assertEquals(0, sampler.naturalHeightCacheSize());
|
||||
assertEquals(0, sampler.oceanClassificationCacheSize());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void primitiveCachesPreserveAccessOrderedEviction() {
|
||||
Map<Long, Integer> basisCalls = new HashMap<>();
|
||||
Map<Long, Integer> heightCalls = new HashMap<>();
|
||||
Map<Long, Integer> classifierCalls = new HashMap<>();
|
||||
IrisHydrologyRoutingTerrainSampler sampler = new IrisHydrologyRoutingTerrainSampler(
|
||||
new IrisHydrologyRoutingTerrainSampler.Sources(
|
||||
(int x, int z, double naturalHeight) -> {
|
||||
basisCalls.merge(pack(x, z), 1, Integer::sum);
|
||||
return basis(x, z, naturalHeight);
|
||||
},
|
||||
(int x, int z) -> {
|
||||
heightCalls.merge(pack(x, z), 1, Integer::sum);
|
||||
return height(x, z);
|
||||
},
|
||||
(int x, int z) -> {
|
||||
classifierCalls.merge(pack(x, z), 1, Integer::sum);
|
||||
return false;
|
||||
}
|
||||
),
|
||||
IrisHydrologyRoutingTerrainSampler.SamplingOptions.serial(2)
|
||||
);
|
||||
|
||||
sampler.basis(0, 0);
|
||||
sampler.basis(1, 1);
|
||||
sampler.basis(0, 0);
|
||||
sampler.basis(2, 2);
|
||||
sampler.basis(1, 1);
|
||||
|
||||
assertEquals(1, basisCalls.get(pack(0, 0)).intValue());
|
||||
assertEquals(2, basisCalls.get(pack(1, 1)).intValue());
|
||||
assertEquals(1, basisCalls.get(pack(2, 2)).intValue());
|
||||
assertEquals(1, heightCalls.get(pack(0, 0)).intValue());
|
||||
assertEquals(1, heightCalls.get(pack(1, 1)).intValue());
|
||||
assertEquals(1, heightCalls.get(pack(2, 2)).intValue());
|
||||
|
||||
sampler.classifyNatural(10, 10);
|
||||
sampler.classifyNatural(11, 11);
|
||||
sampler.classifyNatural(10, 10);
|
||||
sampler.classifyNatural(12, 12);
|
||||
sampler.classifyNatural(11, 11);
|
||||
|
||||
assertEquals(1, classifierCalls.get(pack(10, 10)).intValue());
|
||||
assertEquals(2, classifierCalls.get(pack(11, 11)).intValue());
|
||||
assertEquals(1, classifierCalls.get(pack(12, 12)).intValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void primitiveHeightCacheRetainsNanValues() {
|
||||
AtomicInteger heightCalls = new AtomicInteger();
|
||||
IrisHydrologyRoutingTerrainSampler sampler = new IrisHydrologyRoutingTerrainSampler(
|
||||
new IrisHydrologyRoutingTerrainSampler.Sources(
|
||||
(int x, int z, double naturalHeight) -> basis(x, z, naturalHeight),
|
||||
(int x, int z) -> {
|
||||
heightCalls.incrementAndGet();
|
||||
return Double.NaN;
|
||||
},
|
||||
(int x, int z) -> false
|
||||
),
|
||||
IrisHydrologyRoutingTerrainSampler.SamplingOptions.serial(16)
|
||||
);
|
||||
|
||||
assertTrue(Double.isNaN(sampler.localSlope(0, 0, 0D)));
|
||||
assertTrue(Double.isNaN(sampler.localSlope(0, 0, 0D)));
|
||||
assertEquals(2, heightCalls.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidBoundsAreRejected() {
|
||||
assertThrows(IllegalArgumentException.class, () -> sampler(0));
|
||||
IrisHydrologyRoutingTerrainSampler sampler = sampler(16);
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> new HydrologyRoutingTerrainSampler.GridRequest(0, 0, 0, 64)
|
||||
);
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> new HydrologyRoutingTerrainSampler.GridRequest(0, 0, 1, 0)
|
||||
);
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> IrisHydrologyRoutingTerrainSampler.routingSlope(1D, 2D, 3D, 0)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parallelGridMatchesSerialRawBitsAndSamplesEachProviderOnce() throws Exception {
|
||||
int minimumX = -2048;
|
||||
int minimumZ = 4096;
|
||||
int width = 33;
|
||||
int spacing = 64;
|
||||
int expectedCalls = (width + 1) * (width + 1);
|
||||
AtomicInteger serialBasisCalls = new AtomicInteger();
|
||||
AtomicInteger serialHeightCalls = new AtomicInteger();
|
||||
AtomicInteger parallelBasisCalls = new AtomicInteger();
|
||||
AtomicInteger parallelHeightCalls = new AtomicInteger();
|
||||
AtomicInteger submissions = new AtomicInteger();
|
||||
Map<Long, Integer> serialCoordinateCalls = new HashMap<>();
|
||||
Map<Long, Integer> parallelCoordinateCalls = new ConcurrentHashMap<>();
|
||||
ExecutorService executor = Executors.newFixedThreadPool(4);
|
||||
Executor recordingExecutor = (Runnable command) -> {
|
||||
submissions.incrementAndGet();
|
||||
executor.execute(command);
|
||||
};
|
||||
try {
|
||||
IrisHydrologyRoutingTerrainSampler serial = countedSampler(
|
||||
65_536,
|
||||
1,
|
||||
Runnable::run,
|
||||
serialBasisCalls,
|
||||
serialHeightCalls,
|
||||
serialCoordinateCalls
|
||||
);
|
||||
IrisHydrologyRoutingTerrainSampler parallel = countedSampler(
|
||||
65_536,
|
||||
4,
|
||||
recordingExecutor,
|
||||
parallelBasisCalls,
|
||||
parallelHeightCalls,
|
||||
parallelCoordinateCalls
|
||||
);
|
||||
HydrologyRoutingTerrainSampler.GridRequest request = new HydrologyRoutingTerrainSampler.GridRequest(
|
||||
minimumX,
|
||||
minimumZ,
|
||||
width,
|
||||
spacing
|
||||
);
|
||||
|
||||
HydrologyTerrainSample[] serialSamples = serial.sampleGrid(request);
|
||||
HydrologyTerrainSample[] parallelSamples = parallel.sampleGrid(request);
|
||||
|
||||
assertEquals(serialSamples.length, parallelSamples.length);
|
||||
for (int index = 0; index < serialSamples.length; index++) {
|
||||
assertSampleRawBits(serialSamples[index], parallelSamples[index]);
|
||||
}
|
||||
assertEquals(expectedCalls, serialBasisCalls.get());
|
||||
assertEquals(expectedCalls, serialHeightCalls.get());
|
||||
assertEquals(expectedCalls, parallelBasisCalls.get());
|
||||
assertEquals(expectedCalls, parallelHeightCalls.get());
|
||||
assertCoordinateCalls(serialCoordinateCalls, expectedCalls);
|
||||
assertCoordinateCalls(parallelCoordinateCalls, expectedCalls);
|
||||
assertTrue(submissions.get() > 1);
|
||||
} finally {
|
||||
executor.shutdownNow();
|
||||
assertTrue(executor.awaitTermination(5L, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void burstWorkerUsesAvailableParallelRows() throws Exception {
|
||||
AtomicInteger submissions = new AtomicInteger();
|
||||
AtomicInteger heightCalls = new AtomicInteger();
|
||||
MultiBurst burst = new MultiBurst("Iris Hydrology Test", Thread.NORM_PRIORITY, () -> 2);
|
||||
Executor recordingExecutor = (Runnable command) -> {
|
||||
submissions.incrementAndGet();
|
||||
burst.execute(command);
|
||||
};
|
||||
try {
|
||||
IrisHydrologyRoutingTerrainSampler sampler = new IrisHydrologyRoutingTerrainSampler(
|
||||
new IrisHydrologyRoutingTerrainSampler.Sources(
|
||||
(int x, int z, double naturalHeight) -> basis(x, z, naturalHeight),
|
||||
(int x, int z) -> {
|
||||
heightCalls.incrementAndGet();
|
||||
return height(x, z);
|
||||
},
|
||||
(int x, int z) -> false
|
||||
),
|
||||
new IrisHydrologyRoutingTerrainSampler.SamplingOptions(
|
||||
65_536,
|
||||
4,
|
||||
recordingExecutor
|
||||
)
|
||||
);
|
||||
int width = 21;
|
||||
|
||||
Future<?> completion = burst.submit(
|
||||
() -> sampler.sampleGrid(new HydrologyRoutingTerrainSampler.GridRequest(0, 0, width, 64))
|
||||
);
|
||||
completion.get(5L, TimeUnit.SECONDS);
|
||||
|
||||
assertTrue(submissions.get() > 1);
|
||||
assertEquals((width + 1) * (width + 1), heightCalls.get());
|
||||
} finally {
|
||||
burst.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parallelGridPropagatesProviderFailure() throws Exception {
|
||||
ExecutorService executor = Executors.newFixedThreadPool(2);
|
||||
try {
|
||||
IrisHydrologyRoutingTerrainSampler sampler = new IrisHydrologyRoutingTerrainSampler(
|
||||
new IrisHydrologyRoutingTerrainSampler.Sources(
|
||||
(int x, int z, double naturalHeight) -> basis(x, z, naturalHeight),
|
||||
(int x, int z) -> {
|
||||
if (x == 0 && z == 0) {
|
||||
throw new IllegalStateException("expected sampling failure");
|
||||
}
|
||||
return height(x, z);
|
||||
},
|
||||
(int x, int z) -> false
|
||||
),
|
||||
new IrisHydrologyRoutingTerrainSampler.SamplingOptions(
|
||||
65_536,
|
||||
2,
|
||||
executor
|
||||
)
|
||||
);
|
||||
|
||||
IllegalStateException failure = assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> sampler.sampleGrid(new HydrologyRoutingTerrainSampler.GridRequest(0, 0, 33, 64))
|
||||
);
|
||||
|
||||
assertEquals("expected sampling failure", failure.getMessage());
|
||||
} finally {
|
||||
executor.shutdownNow();
|
||||
assertTrue(executor.awaitTermination(5L, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
|
||||
private IrisHydrologyRoutingTerrainSampler sampler(int maximumEntries) {
|
||||
return new IrisHydrologyRoutingTerrainSampler(
|
||||
new IrisHydrologyRoutingTerrainSampler.Sources(
|
||||
(int x, int z, double naturalHeight) -> basis(x, z, naturalHeight),
|
||||
(int x, int z) -> height(x, z),
|
||||
(int x, int z) -> x >= 1
|
||||
),
|
||||
IrisHydrologyRoutingTerrainSampler.SamplingOptions.serial(maximumEntries)
|
||||
);
|
||||
}
|
||||
|
||||
private IrisHydrologyRoutingTerrainSampler countedSampler(
|
||||
int maximumEntries,
|
||||
int maximumWorkers,
|
||||
Executor executor,
|
||||
AtomicInteger basisCalls,
|
||||
AtomicInteger heightCalls,
|
||||
Map<Long, Integer> coordinateCalls
|
||||
) {
|
||||
return new IrisHydrologyRoutingTerrainSampler(
|
||||
new IrisHydrologyRoutingTerrainSampler.Sources(
|
||||
(int x, int z, double naturalHeight) -> {
|
||||
basisCalls.incrementAndGet();
|
||||
return basis(x, z, naturalHeight);
|
||||
},
|
||||
(int x, int z) -> {
|
||||
heightCalls.incrementAndGet();
|
||||
coordinateCalls.merge(pack(x, z), 1, Integer::sum);
|
||||
return height(x, z);
|
||||
},
|
||||
(int x, int z) -> false
|
||||
),
|
||||
new IrisHydrologyRoutingTerrainSampler.SamplingOptions(
|
||||
maximumEntries,
|
||||
maximumWorkers,
|
||||
executor
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private void assertCoordinateCalls(Map<Long, Integer> coordinateCalls, int expectedCalls) {
|
||||
assertEquals(expectedCalls, coordinateCalls.size());
|
||||
for (int count : coordinateCalls.values()) {
|
||||
assertEquals(1, count);
|
||||
}
|
||||
}
|
||||
|
||||
private void assertSampleRawBits(HydrologyTerrainSample expected, HydrologyTerrainSample actual) {
|
||||
assertEquals(expected, actual);
|
||||
assertEquals(Double.doubleToRawLongBits(expected.slope()), Double.doubleToRawLongBits(actual.slope()));
|
||||
assertEquals(Double.doubleToRawLongBits(expected.routingCost()), Double.doubleToRawLongBits(actual.routingCost()));
|
||||
assertEquals(
|
||||
Double.doubleToRawLongBits(expected.surfaceSourceWeight()),
|
||||
Double.doubleToRawLongBits(actual.surfaceSourceWeight())
|
||||
);
|
||||
assertEquals(
|
||||
Double.doubleToRawLongBits(expected.undergroundSourceWeight()),
|
||||
Double.doubleToRawLongBits(actual.undergroundSourceWeight())
|
||||
);
|
||||
assertEquals(
|
||||
Double.doubleToRawLongBits(expected.widthMultiplier()),
|
||||
Double.doubleToRawLongBits(actual.widthMultiplier())
|
||||
);
|
||||
assertEquals(
|
||||
Double.doubleToRawLongBits(expected.depthMultiplier()),
|
||||
Double.doubleToRawLongBits(actual.depthMultiplier())
|
||||
);
|
||||
assertEquals(
|
||||
Double.doubleToRawLongBits(expected.incisionMultiplier()),
|
||||
Double.doubleToRawLongBits(actual.incisionMultiplier())
|
||||
);
|
||||
assertEquals(
|
||||
Double.doubleToRawLongBits(expected.routingMultiplier()),
|
||||
Double.doubleToRawLongBits(actual.routingMultiplier())
|
||||
);
|
||||
}
|
||||
|
||||
private IrisHydrologyRoutingTerrainSampler.TerrainBasis basis(int x, int z, double height) {
|
||||
return new IrisHydrologyRoutingTerrainSampler.TerrainBasis(
|
||||
height,
|
||||
HydrologyTerrainSample.openLand((int) StrictMath.round(height), 0D, "parent")
|
||||
);
|
||||
}
|
||||
|
||||
private IrisHydrologyRoutingTerrainSampler.TerrainBasis oceanBasis(int x, int z, double height) {
|
||||
return new IrisHydrologyRoutingTerrainSampler.TerrainBasis(
|
||||
height,
|
||||
HydrologyTerrainSample.ocean((int) StrictMath.round(height), "parent")
|
||||
);
|
||||
}
|
||||
|
||||
private double height(int x, int z) {
|
||||
return x * 0.125D + z * 0.0625D + Math.floorMod(x * 31 + z * 17, 11) * 0.03125D;
|
||||
}
|
||||
|
||||
private long pack(int x, int z) {
|
||||
return ((long) x << 32) ^ (z & 0xffffffffL);
|
||||
}
|
||||
}
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
package art.arcane.iris.engine.hydrology.runtime;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.HydrologyPlannerSettings;
|
||||
import art.arcane.iris.engine.object.IrisDeepFluidConfig;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisRiverHydrology;
|
||||
import art.arcane.iris.engine.object.IrisRiverInlandOutlet;
|
||||
import art.arcane.iris.engine.object.IrisRiverRoutingConfig;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class IrisHydrologyRuntimeSettingsTest {
|
||||
@Test
|
||||
public void deepChannelsRemainLocalWhenConfiguredSpacingIsLarge() {
|
||||
IrisDeepFluidConfig deepFluid = new IrisDeepFluidConfig()
|
||||
.setShortChannels(true)
|
||||
.setSpacing(8192);
|
||||
IrisRiverRoutingConfig routing = new IrisRiverRoutingConfig()
|
||||
.setTileSize(256)
|
||||
.setRefinementSpacing(8);
|
||||
|
||||
assertEquals(128, IrisHydrologyRuntime.maximumDeepChannelLength(deepFluid, routing));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void disabledDeepChannelsHaveNoPublicationReach() {
|
||||
IrisDeepFluidConfig deepFluid = new IrisDeepFluidConfig()
|
||||
.setShortChannels(false)
|
||||
.setSpacing(8192);
|
||||
IrisRiverRoutingConfig routing = new IrisRiverRoutingConfig()
|
||||
.setTileSize(256)
|
||||
.setRefinementSpacing(8);
|
||||
|
||||
assertEquals(0, IrisHydrologyRuntime.maximumDeepChannelLength(deepFluid, routing));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void surfaceSinkholeSettingRequiresTheCanonicalEnabledInlandOutlet() {
|
||||
IrisDimension dimension = new IrisDimension();
|
||||
IrisRiverHydrology rivers = dimension.getHydrology().getRivers();
|
||||
rivers.setEnabled(true);
|
||||
rivers.getGrottos().getInland().setEnabled(true).setConnectSurfaceRivers(true);
|
||||
rivers.getRouting().getInlandOutlets().add(IrisRiverInlandOutlet.SINKHOLE_GROTTO);
|
||||
|
||||
assertTrue(IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology())
|
||||
.outlets().surfaceSinkholesEnabled());
|
||||
|
||||
rivers.getRouting().getInlandOutlets().clear();
|
||||
|
||||
assertFalse(IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology())
|
||||
.outlets().surfaceSinkholesEnabled());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void denseRiverSourcesRemainDistinctAndCanFillTheirTileQuota() {
|
||||
IrisDimension dimension = new IrisDimension();
|
||||
IrisRiverHydrology rivers = dimension.getHydrology().getRivers();
|
||||
rivers.setEnabled(true);
|
||||
rivers.getSurface().setEnabled(true);
|
||||
rivers.getSurface().getSources().setDensity(4.5D);
|
||||
rivers.getUnderground().setEnabled(true);
|
||||
rivers.getUnderground().getSources().setDensity(4.5D);
|
||||
rivers.getRouting().setSampleSpacing(64);
|
||||
|
||||
assertEquals(5, IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology())
|
||||
.surface().sources().maximumPerTile());
|
||||
assertEquals(384, IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology())
|
||||
.surface().sources().minimumSpacing());
|
||||
assertEquals(5, IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology())
|
||||
.underground().sources().maximumPerTile());
|
||||
assertEquals(512, IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology())
|
||||
.underground().sources().minimumSpacing());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void routingOutletLimitControlsDrainageRootCount() {
|
||||
IrisDimension dimension = new IrisDimension();
|
||||
dimension.getHydrology().getRivers().getRouting().setMaximumOutletsPerTile(2);
|
||||
|
||||
assertEquals(2, IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology())
|
||||
.outlets().maximumPerTile());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void organicGeometryConfigurationMapsWithoutLosingPrecision() {
|
||||
IrisDimension dimension = new IrisDimension();
|
||||
IrisRiverHydrology rivers = dimension.getHydrology().getRivers();
|
||||
rivers.getGeometry().getMeanders()
|
||||
.setPrimaryWavelength(47)
|
||||
.setDetailWavelength(9)
|
||||
.setPrimaryStrength(0.31D)
|
||||
.setDetailStrength(0.53D)
|
||||
.setMaximumOffsetRatio(0.44D)
|
||||
.setSmoothingPasses(2)
|
||||
.setMaximumTurnDegrees(76D);
|
||||
rivers.getGeometry().getSurface()
|
||||
.setBedRoundness(3.1D)
|
||||
.setBedRoughness(0.37D)
|
||||
.setWallRoughness(0.29D)
|
||||
.setRoughnessWavelength(13);
|
||||
rivers.getGeometry().getDrops()
|
||||
.setCascadeRunPerBlock(4)
|
||||
.setCascadeExponent(2.2D)
|
||||
.setMaximumCascadeStep(1)
|
||||
.setFlowWidthRatio(0.81D)
|
||||
.setMaximumFlowDepth(3)
|
||||
.setBasinWidthRatio(2.1D)
|
||||
.setMaximumBasinDepth(9);
|
||||
|
||||
HydrologyPlannerSettings.Geometry geometry = IrisHydrologyRuntime
|
||||
.createSettings(dimension, dimension.getHydrology())
|
||||
.geometry();
|
||||
|
||||
assertEquals(47, geometry.meanders().primaryWavelength());
|
||||
assertEquals(9, geometry.meanders().detailWavelength());
|
||||
assertEquals(0.31D, geometry.meanders().primaryStrength(), 0D);
|
||||
assertEquals(0.53D, geometry.meanders().detailStrength(), 0D);
|
||||
assertEquals(0.44D, geometry.meanders().maximumOffsetRatio(), 0D);
|
||||
assertEquals(2, geometry.meanders().smoothingPasses());
|
||||
assertEquals(76D, geometry.meanders().maximumTurnDegrees(), 0D);
|
||||
assertEquals(3.1D, geometry.surface().bedRoundness(), 0D);
|
||||
assertEquals(0.37D, geometry.surface().bedRoughness(), 0D);
|
||||
assertEquals(0.29D, geometry.surface().wallRoughness(), 0D);
|
||||
assertEquals(13, geometry.surface().roughnessWavelength());
|
||||
assertEquals(4, geometry.drops().cascadeRunPerBlock());
|
||||
assertEquals(2.2D, geometry.drops().cascadeExponent(), 0D);
|
||||
assertEquals(1, geometry.drops().maximumCascadeStep());
|
||||
assertEquals(0.81D, geometry.drops().flowWidthRatio(), 0D);
|
||||
assertEquals(3, geometry.drops().maximumFlowDepth());
|
||||
assertEquals(2.1D, geometry.drops().basinWidthRatio(), 0D);
|
||||
assertEquals(9, geometry.drops().maximumBasinDepth());
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,12 @@ package art.arcane.iris.engine.mantle;
|
||||
|
||||
import art.arcane.iris.core.link.Identifier;
|
||||
import art.arcane.iris.engine.framework.TreeBlockMaterial;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveCell;
|
||||
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.matter.PreObjectMatterCell;
|
||||
import art.arcane.iris.util.project.matter.TileWrapper;
|
||||
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
@@ -18,6 +19,12 @@ import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.InOrder;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Answers.CALLS_REAL_METHODS;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
@@ -98,6 +105,16 @@ public class EngineMantleCleanupTest {
|
||||
verify(fixture.chunk()).deleteSlices(PlatformBlockState.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preObjectJournalIsReclaimedEvenWhenRetentionWasRequested() {
|
||||
MantleSliceRetention.retain(PreObjectMatterCell.class.getCanonicalName());
|
||||
CleanupFixture fixture = cleanupFixture();
|
||||
|
||||
assertTrue(fixture.engineMantle().forceCleanupChunk(7, -4));
|
||||
|
||||
verify(fixture.chunk()).deleteSlices(PreObjectMatterCell.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void coveredCleanupRetainsDeferredMaterializationSlices() {
|
||||
CleanupFixture fixture = cleanupFixture();
|
||||
@@ -117,12 +134,53 @@ public class EngineMantleCleanupTest {
|
||||
verifyCleanup(fixture.chunk());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void forceCleanupWaitsForTheFullGenerationHalo() {
|
||||
CleanupFixture fixture = cleanupFixture();
|
||||
doReturn(false).when(fixture.engineMantle()).isCovered(7, -4);
|
||||
|
||||
assertFalse(fixture.engineMantle().forceCleanupChunk(7, -4));
|
||||
|
||||
verify(fixture.chunk(), never()).raiseFlagUnchecked(eq(MantleFlag.CLEANED), any());
|
||||
verify(fixture.chunk(), never()).deleteSlices(PreObjectMatterCell.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void laterRealNeighborRetriesCoveredFrontierAndReportsOnlyActualCleanup() {
|
||||
CleanupFixture fixture = cleanupFixture();
|
||||
doReturn(1).when(fixture.engineMantle()).getRadius();
|
||||
doReturn(false).when(fixture.engineMantle()).isCovered(7, -4);
|
||||
when(fixture.mantle().hasFlag(6, -4, MantleFlag.CLEANED)).thenReturn(true);
|
||||
List<String> cleaned = new ArrayList<>();
|
||||
|
||||
fixture.engineMantle().cleanupChunksCoveredBy(
|
||||
6,
|
||||
-4,
|
||||
false,
|
||||
(x, z) -> cleaned.add(x + "," + z)
|
||||
);
|
||||
assertTrue(cleaned.isEmpty());
|
||||
verify(fixture.engineMantle(), never()).isCovered(6, -4);
|
||||
|
||||
doReturn(true).when(fixture.engineMantle()).isCovered(7, -4);
|
||||
fixture.engineMantle().cleanupChunksCoveredBy(
|
||||
8,
|
||||
-4,
|
||||
false,
|
||||
(x, z) -> cleaned.add(x + "," + z)
|
||||
);
|
||||
|
||||
assertEquals(List.of("7,-4"), cleaned);
|
||||
verify(fixture.chunk()).deleteSlices(PreObjectMatterCell.class);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private CleanupFixture cleanupFixture() {
|
||||
EngineMantle engineMantle = mock(EngineMantle.class, CALLS_REAL_METHODS);
|
||||
Mantle<Matter> mantle = mock(Mantle.class);
|
||||
MantleChunk<Matter> chunk = mock(MantleChunk.class);
|
||||
when(engineMantle.getMantle()).thenReturn(mantle);
|
||||
doReturn(true).when(engineMantle).isCovered(7, -4);
|
||||
when(mantle.getChunk(7, -4)).thenReturn(chunk);
|
||||
when(chunk.use()).thenReturn(chunk);
|
||||
doAnswer(invocation -> {
|
||||
@@ -130,20 +188,25 @@ public class EngineMantleCleanupTest {
|
||||
cleanup.run();
|
||||
return null;
|
||||
}).when(chunk).raiseFlagUnchecked(eq(MantleFlag.CLEANED), any());
|
||||
return new CleanupFixture(engineMantle, chunk);
|
||||
return new CleanupFixture(engineMantle, mantle, chunk);
|
||||
}
|
||||
|
||||
private void verifyCleanup(MantleChunk<Matter> chunk) {
|
||||
verify(chunk).deleteSlices(PlatformBlockState.class);
|
||||
verify(chunk).deleteSlices(MatterCavern.class);
|
||||
verify(chunk).deleteSlices(PreObjectMatterCell.class);
|
||||
verify(chunk, never()).deleteSlices(TileWrapper.class);
|
||||
verify(chunk, never()).deleteSlices(Identifier.class);
|
||||
verify(chunk, never()).deleteSlices(RiverCaveHydrology.class);
|
||||
verify(chunk, never()).deleteSlices(HydrologyCaveCell.class);
|
||||
InOrder order = inOrder(chunk);
|
||||
order.verify(chunk).raiseFlagUnchecked(eq(MantleFlag.CLEANED), any());
|
||||
order.verify(chunk).release();
|
||||
}
|
||||
|
||||
private record CleanupFixture(EngineMantle engineMantle, MantleChunk<Matter> chunk) {
|
||||
private record CleanupFixture(
|
||||
EngineMantle engineMantle,
|
||||
Mantle<Matter> mantle,
|
||||
MantleChunk<Matter> chunk
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
package art.arcane.iris.engine.mantle;
|
||||
|
||||
import art.arcane.iris.core.link.Identifier;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveAction;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveAction;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveCell;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
@@ -19,6 +21,8 @@ import org.junit.Test;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@@ -37,7 +41,8 @@ public class MantleWriterOverlayTest {
|
||||
private MatterSlice<PlatformBlockState> blockSlice;
|
||||
private MatterSlice<Identifier> identifierSlice;
|
||||
private MatterSlice<MatterCavern> cavernSlice;
|
||||
private MatterSlice<RiverCaveHydrology> hydrologySlice;
|
||||
private MatterSlice<HydrologyCaveCell> hydrologySlice;
|
||||
private IrisDimension dimension;
|
||||
|
||||
@Before
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -51,6 +56,8 @@ public class MantleWriterOverlayTest {
|
||||
IrisPlatforms.bind(platform);
|
||||
|
||||
EngineMantle engineMantle = mock(EngineMantle.class);
|
||||
Engine engine = mock(Engine.class);
|
||||
dimension = mock(IrisDimension.class);
|
||||
Mantle<Matter> mantle = mock(Mantle.class);
|
||||
chunk = mock(MantleChunk.class);
|
||||
matter = mock(Matter.class);
|
||||
@@ -60,6 +67,8 @@ public class MantleWriterOverlayTest {
|
||||
hydrologySlice = mock(MatterSlice.class);
|
||||
|
||||
when(mantle.getWorldHeight()).thenReturn(64);
|
||||
when(engineMantle.getEngine()).thenReturn(engine);
|
||||
when(engine.getDimension()).thenReturn(dimension);
|
||||
when(mantle.getChunk(0, 0)).thenReturn(chunk);
|
||||
when(chunk.use()).thenReturn(chunk);
|
||||
when(chunk.getOrCreate(0)).thenReturn(matter);
|
||||
@@ -72,8 +81,9 @@ public class MantleWriterOverlayTest {
|
||||
when(matter.getSlice(Identifier.class)).thenReturn(identifierSlice);
|
||||
when(matter.getSlice(MatterCavern.class)).thenReturn(cavernSlice);
|
||||
doReturn(blockSlice).when(matter).slice(PlatformBlockState.class);
|
||||
doReturn(identifierSlice).when(matter).slice(Identifier.class);
|
||||
doReturn(cavernSlice).when(matter).slice(MatterCavern.class);
|
||||
doReturn(hydrologySlice).when(matter).getSlice(RiverCaveHydrology.class);
|
||||
doReturn(hydrologySlice).when(matter).getSlice(HydrologyCaveCell.class);
|
||||
|
||||
writer = new MantleWriter(engineMantle, mantle, 0, 0, 0, false);
|
||||
}
|
||||
@@ -117,13 +127,52 @@ public class MantleWriterOverlayTest {
|
||||
verify(blockSlice).set(X, Y, Z, replacement);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void customBlockWritesBaseAndIdentifierTogether() {
|
||||
PlatformBlockState base = mock(PlatformBlockState.class);
|
||||
PlatformBlockState custom = customState(base);
|
||||
Identifier identifier = Identifier.fromString("iris:custom_block");
|
||||
|
||||
writer.set(X, Y, Z, custom);
|
||||
|
||||
verify(blockSlice).set(X, Y, Z, base);
|
||||
verify(identifierSlice).set(X, Y, Z, identifier);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void protectedHydrologyRejectsBothPartsOfCustomBlock() {
|
||||
PlatformBlockState base = mock(PlatformBlockState.class);
|
||||
PlatformBlockState custom = customState(base);
|
||||
when(matter.hasSlice(HydrologyCaveCell.class)).thenReturn(true);
|
||||
when(hydrologySlice.get(X, Y, Z))
|
||||
.thenReturn(HydrologyCaveCell.of(HydrologyCaveAction.SEAL_GUARD));
|
||||
|
||||
writer.set(X, Y, Z, custom);
|
||||
|
||||
verify(blockSlice, never()).set(X, Y, Z, base);
|
||||
verify(identifierSlice, never()).set(anyInt(), anyInt(), anyInt(), any(Identifier.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bedrockAndWorldBoundsRejectBothPartsOfCustomBlock() {
|
||||
PlatformBlockState base = mock(PlatformBlockState.class);
|
||||
PlatformBlockState custom = customState(base);
|
||||
when(dimension.isBedrock()).thenReturn(true);
|
||||
|
||||
writer.set(X, 0, Z, custom);
|
||||
writer.set(X, 64, Z, custom);
|
||||
|
||||
verify(blockSlice, never()).set(anyInt(), anyInt(), anyInt(), any(PlatformBlockState.class));
|
||||
verify(identifierSlice, never()).set(anyInt(), anyInt(), anyInt(), any(Identifier.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void protectedHydrologyRejectsLaterBlockAndCavernWrites() {
|
||||
PlatformBlockState replacement = mock(PlatformBlockState.class);
|
||||
MatterCavern cavern = new MatterCavern(true, "", (byte) 3);
|
||||
when(matter.hasSlice(RiverCaveHydrology.class)).thenReturn(true);
|
||||
when(matter.hasSlice(HydrologyCaveCell.class)).thenReturn(true);
|
||||
when(hydrologySlice.get(X, Y, Z))
|
||||
.thenReturn(RiverCaveHydrology.of(RiverCaveAction.SEAL_GUARD));
|
||||
.thenReturn(HydrologyCaveCell.of(HydrologyCaveAction.SEAL_GUARD));
|
||||
|
||||
writer.setData(X, Y, Z, replacement);
|
||||
assertFalse(writer.carveDataIfAbsent(X, Y, Z, cavern));
|
||||
@@ -138,12 +187,12 @@ public class MantleWriterOverlayTest {
|
||||
@Test
|
||||
public void hydrologyOverridesBaselineCarvedQueriesWithoutChangingIt() {
|
||||
MatterCavern baseline = new MatterCavern(true, "", (byte) 0);
|
||||
when(matter.hasSlice(RiverCaveHydrology.class)).thenReturn(true);
|
||||
when(matter.hasSlice(HydrologyCaveCell.class)).thenReturn(true);
|
||||
when(cavernSlice.get(X, Y, Z)).thenReturn(baseline);
|
||||
when(hydrologySlice.get(X, Y, Z))
|
||||
.thenReturn(RiverCaveHydrology.of(RiverCaveAction.SEAL_GUARD));
|
||||
.thenReturn(HydrologyCaveCell.of(HydrologyCaveAction.SEAL_GUARD));
|
||||
when(hydrologySlice.get(X, Y + 1, Z))
|
||||
.thenReturn(RiverCaveHydrology.of(RiverCaveAction.DRY_AIR));
|
||||
.thenReturn(HydrologyCaveCell.of(HydrologyCaveAction.DRY_AIR));
|
||||
|
||||
assertFalse(writer.isCarved(X, Y, Z));
|
||||
assertTrue(writer.isCarved(X, Y + 1, Z));
|
||||
@@ -153,4 +202,12 @@ public class MantleWriterOverlayTest {
|
||||
assertEquals(1, column[Y + 1]);
|
||||
assertEquals(0, baseline.getLiquid());
|
||||
}
|
||||
|
||||
private static PlatformBlockState customState(PlatformBlockState base) {
|
||||
PlatformBlockState custom = mock(PlatformBlockState.class);
|
||||
when(custom.isCustom()).thenReturn(true);
|
||||
when(custom.deferredPlacementKey()).thenReturn("iris:custom_block");
|
||||
when(custom.placementBaseState()).thenReturn(base);
|
||||
return custom;
|
||||
}
|
||||
}
|
||||
+238
@@ -0,0 +1,238 @@
|
||||
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.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.matter.IrisMatterSupport;
|
||||
import art.arcane.iris.util.project.matter.PreObjectMatterCell;
|
||||
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 art.arcane.volmlib.util.matter.MatterSlice;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Server;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
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.Locale;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class MantleWriterPreObjectJournalTest {
|
||||
private static final int X = 2;
|
||||
private static final int Y = 5;
|
||||
private static final int Z = 4;
|
||||
|
||||
private MantleWriter writer;
|
||||
private Matter matter;
|
||||
|
||||
@BeforeClass
|
||||
public static void setUpBukkit() {
|
||||
if (Bukkit.getServer() != null) {
|
||||
return;
|
||||
}
|
||||
Server server = mock(Server.class);
|
||||
doReturn(Logger.getLogger("IrisTest")).when(server).getLogger();
|
||||
doReturn("IrisTestServer").when(server).getName();
|
||||
doReturn("1.0").when(server).getVersion();
|
||||
doReturn("1.0").when(server).getBukkitVersion();
|
||||
doAnswer(invocation -> blockData(invocation.getArgument(0, Material.class).name()
|
||||
.toLowerCase(Locale.ROOT))).when(server).createBlockData(any(Material.class));
|
||||
try {
|
||||
Bukkit.setServer(server);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@Before
|
||||
@SuppressWarnings("unchecked")
|
||||
public void setUp() {
|
||||
IrisPlatforms.unbind();
|
||||
PlatformBlockState air = mock(PlatformBlockState.class);
|
||||
PlatformRegistries registries = mock(PlatformRegistries.class);
|
||||
IrisPlatform platform = mock(IrisPlatform.class);
|
||||
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(mantle.getWorldHeight()).thenReturn(32);
|
||||
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);
|
||||
writer = new MantleWriter(engineMantle, mantle, 0, 0, 0, false);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
if (writer != null) {
|
||||
writer.close();
|
||||
}
|
||||
IrisPlatforms.unbind();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void firstMutationCapturesEachSupportedOriginalIndependently() {
|
||||
PlatformBlockState originalBlock = mock(PlatformBlockState.class);
|
||||
PlatformBlockState firstBlock = mock(PlatformBlockState.class);
|
||||
PlatformBlockState secondBlock = mock(PlatformBlockState.class);
|
||||
MatterCavern originalCavern = new MatterCavern(true, "old", (byte) 1);
|
||||
MatterCavern replacementCavern = new MatterCavern(true, "new", (byte) 2);
|
||||
matter.<PlatformBlockState>slice(PlatformBlockState.class).set(X, Y, Z, originalBlock);
|
||||
matter.<MatterCavern>slice(MatterCavern.class).set(X, Y, Z, originalCavern);
|
||||
|
||||
writer.withComponentPriority(2, () -> {
|
||||
writer.setData(X, Y, Z, firstBlock);
|
||||
writer.setData(X, Y, Z, "new-marker");
|
||||
writer.setData(X, Y, Z, replacementCavern);
|
||||
writer.setData(X, Y, Z, secondBlock);
|
||||
writer.setData(X, Y, Z, "newer-marker");
|
||||
});
|
||||
|
||||
assertSame(originalBlock, writer.getPrerequisiteDataIfPresent(X, Y, Z, PlatformBlockState.class));
|
||||
assertNull(writer.getPrerequisiteDataIfPresent(X, Y, Z, String.class));
|
||||
assertEquals(originalCavern, writer.getPrerequisiteDataIfPresent(X, Y, Z, MatterCavern.class));
|
||||
assertSame(secondBlock, writer.getDataIfPresent(X, Y, Z, PlatformBlockState.class));
|
||||
assertEquals("newer-marker", writer.getDataIfPresent(X, Y, Z, String.class));
|
||||
assertEquals(replacementCavern, writer.getDataIfPresent(X, Y, Z, MatterCavern.class));
|
||||
|
||||
PreObjectMatterCell journal = matter.<PreObjectMatterCell>getSlice(PreObjectMatterCell.class)
|
||||
.get(X, Y, Z);
|
||||
assertTrue(journal.blockCaptured());
|
||||
assertTrue(journal.stringCaptured());
|
||||
assertTrue(journal.cavernCaptured());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void priorityScopeIsNestedAndThreadLocal() throws Exception {
|
||||
writer.withComponentPriority(2, () -> {
|
||||
writer.withComponentPriority(1, () -> writer.setData(X, Y, Z, "uncaptured-inner"));
|
||||
writer.setData(X + 1, Y, Z, "captured-outer");
|
||||
});
|
||||
assertThrows(IllegalStateException.class, () -> writer.withComponentPriority(2, () -> {
|
||||
throw new IllegalStateException("scope failure");
|
||||
}));
|
||||
writer.setData(X + 2, Y, Z, "uncaptured-after-failure");
|
||||
|
||||
ExecutorService executor = Executors.newFixedThreadPool(2);
|
||||
CountDownLatch start = new CountDownLatch(1);
|
||||
try {
|
||||
Future<?> scoped = executor.submit(() -> {
|
||||
await(start);
|
||||
writer.withComponentPriority(2, () -> writer.setData(X + 3, Y, Z, "captured-thread"));
|
||||
});
|
||||
Future<?> unscoped = executor.submit(() -> {
|
||||
await(start);
|
||||
writer.setData(X + 4, Y, Z, "uncaptured-thread");
|
||||
});
|
||||
start.countDown();
|
||||
scoped.get(5, TimeUnit.SECONDS);
|
||||
unscoped.get(5, TimeUnit.SECONDS);
|
||||
} finally {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
|
||||
MatterSlice<PreObjectMatterCell> journal = matter.getSlice(PreObjectMatterCell.class);
|
||||
assertNull(journal.get(X, Y, Z));
|
||||
assertTrue(journal.get(X + 1, Y, Z).stringCaptured());
|
||||
assertNull(journal.get(X + 2, Y, Z));
|
||||
assertTrue(journal.get(X + 3, Y, Z).stringCaptured());
|
||||
assertNull(journal.get(X + 4, Y, Z));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void prerequisiteCarvingUsesJournalTombstonesAndHydrology() {
|
||||
MatterCavern original = new MatterCavern(true, "", (byte) 0);
|
||||
MatterCavern replacement = new MatterCavern(true, "", (byte) 1);
|
||||
matter.<MatterCavern>slice(MatterCavern.class).set(X, Y + 1, Z, original);
|
||||
writer.withComponentPriority(2, () -> {
|
||||
writer.setData(X, Y, Z, replacement);
|
||||
writer.clearData(X, Y + 1, Z, MatterCavern.class);
|
||||
});
|
||||
matter.<HydrologyCaveCell>slice(HydrologyCaveCell.class)
|
||||
.set(X, Y + 2, Z, HydrologyCaveCell.of(HydrologyCaveAction.SEAL_GUARD));
|
||||
matter.<MatterCavern>slice(MatterCavern.class).set(X, Y + 2, Z, original);
|
||||
matter.<HydrologyCaveCell>slice(HydrologyCaveCell.class)
|
||||
.set(X, Y + 3, Z, HydrologyCaveCell.of(HydrologyCaveAction.DRY_AIR));
|
||||
|
||||
byte[] column = writer.getPrerequisiteCarvedColumn(X, Z, Y + 4);
|
||||
|
||||
assertEquals(0, column[Y]);
|
||||
assertEquals(1, column[Y + 1]);
|
||||
assertEquals(0, column[Y + 2]);
|
||||
assertEquals(1, column[Y + 3]);
|
||||
assertFalse(writer.isPrerequisiteCarved(X, Y, Z));
|
||||
assertTrue(writer.isPrerequisiteCarved(X, Y + 1, Z));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void restorationWritesCapturedValuesAndNullsWithoutRecapture() {
|
||||
PlatformBlockState originalBlock = mock(PlatformBlockState.class);
|
||||
PlatformBlockState replacementBlock = mock(PlatformBlockState.class);
|
||||
MatterCavern originalCavern = new MatterCavern(true, "old", (byte) 1);
|
||||
MatterCavern replacementCavern = new MatterCavern(true, "new", (byte) 2);
|
||||
matter.<PlatformBlockState>slice(PlatformBlockState.class).set(X, Y, Z, originalBlock);
|
||||
matter.<MatterCavern>slice(MatterCavern.class).set(X, Y, Z, originalCavern);
|
||||
writer.withComponentPriority(2, () -> {
|
||||
writer.setData(X, Y, Z, replacementBlock);
|
||||
writer.setData(X, Y, Z, "marker");
|
||||
writer.setData(X, Y, Z, replacementCavern);
|
||||
});
|
||||
|
||||
assertTrue(writer.restorePrerequisiteData(X, Y, Z, String.class));
|
||||
assertNull(writer.getDataIfPresent(X, Y, Z, String.class));
|
||||
assertTrue(writer.restorePrerequisiteCell(X, Y, Z));
|
||||
assertSame(originalBlock, writer.getDataIfPresent(X, Y, Z, PlatformBlockState.class));
|
||||
assertEquals(originalCavern, writer.getDataIfPresent(X, Y, Z, MatterCavern.class));
|
||||
assertFalse(writer.restorePrerequisiteData(X, Y, Z, Integer.class));
|
||||
assertFalse(writer.restorePrerequisiteCell(X + 1, Y, Z));
|
||||
}
|
||||
|
||||
private void await(CountDownLatch latch) {
|
||||
try {
|
||||
assertTrue(latch.await(5, TimeUnit.SECONDS));
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static BlockData blockData(String key) {
|
||||
BlockData data = mock(BlockData.class);
|
||||
doReturn("minecraft:" + key).when(data).getAsString();
|
||||
return data;
|
||||
}
|
||||
}
|
||||
+106
-3
@@ -1,6 +1,7 @@
|
||||
package art.arcane.iris.engine.mantle;
|
||||
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EnginePlatformHooks;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.util.project.context.ChunkContext;
|
||||
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
||||
@@ -23,6 +24,43 @@ import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class MatterGeneratorCarvePassRadiusTest {
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void platformFilterRemovesSkippedComponentFromGenerationReach() {
|
||||
IrisDimension dimension = mock(IrisDimension.class);
|
||||
when(dimension.isUseMantle()).thenReturn(true);
|
||||
Mantle<Matter> mantle = mock(Mantle.class);
|
||||
MantleChunk<Matter> chunk = mock(MantleChunk.class);
|
||||
when(mantle.getChunk(anyInt(), anyInt())).thenReturn(chunk);
|
||||
when(chunk.use()).thenReturn(chunk);
|
||||
doAnswer(invocation -> {
|
||||
Runnable task = invocation.getArgument(1);
|
||||
task.run();
|
||||
return null;
|
||||
}).when(chunk).raiseFlagSuspend(any(), any(Runnable.class));
|
||||
|
||||
Engine engine = mock(Engine.class);
|
||||
when(engine.getDimension()).thenReturn(dimension);
|
||||
when(engine.getPlatformHooks()).thenReturn(new EnginePlatformHooks() {
|
||||
@Override
|
||||
public boolean shouldGenerateMantleComponent(Engine target, MantleComponent component) {
|
||||
return component.getFlag() != ReservedFlag.OBJECT;
|
||||
}
|
||||
});
|
||||
|
||||
RecordingComponent carving = new RecordingComponent(ReservedFlag.CARVED, 0, 0);
|
||||
RecordingComponent objects = new RecordingComponent(ReservedFlag.OBJECT, 1, 160);
|
||||
TestMatterGenerator generator = new TestMatterGenerator(engine, mantle, List.of(
|
||||
new MantlePass(List.of(carving), 11, 160),
|
||||
new MantlePass(List.of(objects), 10, 0)
|
||||
));
|
||||
|
||||
generator.generateMatter(0, 0, false, mock(ChunkContext.class));
|
||||
|
||||
assertEquals(Set.of("0,0"), carving.visited);
|
||||
assertTrue(objects.visited.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void carvePassCoversEveryChunkTheObjectPassWritesObjectsInto() {
|
||||
@@ -131,6 +169,47 @@ public class MatterGeneratorCarvePassRadiusTest {
|
||||
assertTrue(conditional.accessSucceeded);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void destinationOwnedOutputGeneratesOnlyDestinationsAndPreparesItsInputHalo() {
|
||||
IrisDimension dimension = mock(IrisDimension.class);
|
||||
when(dimension.isUseMantle()).thenReturn(true);
|
||||
Mantle<Matter> mantle = mock(Mantle.class);
|
||||
MantleChunk<Matter> chunk = mock(MantleChunk.class);
|
||||
when(mantle.getChunk(anyInt(), anyInt())).thenReturn(chunk);
|
||||
when(chunk.use()).thenReturn(chunk);
|
||||
doAnswer(invocation -> {
|
||||
Runnable task = invocation.getArgument(1);
|
||||
task.run();
|
||||
return null;
|
||||
}).when(chunk).raiseFlagSuspend(any(), any(Runnable.class));
|
||||
|
||||
Engine engine = mock(Engine.class);
|
||||
when(engine.getDimension()).thenReturn(dimension);
|
||||
|
||||
RecordingComponent carving = new RecordingComponent(ReservedFlag.CARVED, 0, 1);
|
||||
RecordingComponent objects = new RecordingComponent(
|
||||
ReservedFlag.OBJECT,
|
||||
1,
|
||||
40,
|
||||
0,
|
||||
0,
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
89
|
||||
);
|
||||
TestMatterGenerator generator = new TestMatterGenerator(engine, mantle, List.of(
|
||||
new MantlePass(List.of(carving), 6, 89),
|
||||
new MantlePass(List.of(objects), 0, 0)
|
||||
));
|
||||
|
||||
generator.generateMatter(0, 0, false, mock(ChunkContext.class));
|
||||
|
||||
assertEquals(Set.of("0,0"), objects.visited);
|
||||
assertEquals(169, carving.visited.size());
|
||||
}
|
||||
|
||||
private static final class RecordingComponent implements MantleComponent {
|
||||
private final MantleFlag flag;
|
||||
private final int priority;
|
||||
@@ -139,11 +218,13 @@ public class MatterGeneratorCarvePassRadiusTest {
|
||||
private final int chunkInputRadius;
|
||||
private final boolean lazyInputGeneration;
|
||||
private final int accessProbeOffset;
|
||||
private final int outputRadius;
|
||||
private final int destinationInputRadius;
|
||||
private final Set<String> visited = new LinkedHashSet<>();
|
||||
private boolean accessSucceeded;
|
||||
|
||||
private RecordingComponent(MantleFlag flag, int priority, int radius) {
|
||||
this(flag, priority, radius, 0, 0, false, 0);
|
||||
this(flag, priority, radius, 0, 0, false, 0, radius, 0);
|
||||
}
|
||||
|
||||
private RecordingComponent(
|
||||
@@ -153,7 +234,7 @@ public class MatterGeneratorCarvePassRadiusTest {
|
||||
int inputRadius,
|
||||
int chunkInputRadius
|
||||
) {
|
||||
this(flag, priority, radius, inputRadius, chunkInputRadius, false, 0);
|
||||
this(flag, priority, radius, inputRadius, chunkInputRadius, false, 0, radius, 0);
|
||||
}
|
||||
|
||||
private RecordingComponent(
|
||||
@@ -164,6 +245,21 @@ public class MatterGeneratorCarvePassRadiusTest {
|
||||
int chunkInputRadius,
|
||||
boolean lazyInputGeneration,
|
||||
int accessProbeOffset
|
||||
) {
|
||||
this(flag, priority, radius, inputRadius, chunkInputRadius, lazyInputGeneration,
|
||||
accessProbeOffset, radius, 0);
|
||||
}
|
||||
|
||||
private RecordingComponent(
|
||||
MantleFlag flag,
|
||||
int priority,
|
||||
int radius,
|
||||
int inputRadius,
|
||||
int chunkInputRadius,
|
||||
boolean lazyInputGeneration,
|
||||
int accessProbeOffset,
|
||||
int outputRadius,
|
||||
int destinationInputRadius
|
||||
) {
|
||||
this.flag = flag;
|
||||
this.priority = priority;
|
||||
@@ -172,6 +268,8 @@ public class MatterGeneratorCarvePassRadiusTest {
|
||||
this.chunkInputRadius = chunkInputRadius;
|
||||
this.lazyInputGeneration = lazyInputGeneration;
|
||||
this.accessProbeOffset = accessProbeOffset;
|
||||
this.outputRadius = outputRadius;
|
||||
this.destinationInputRadius = destinationInputRadius;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -184,6 +282,11 @@ public class MatterGeneratorCarvePassRadiusTest {
|
||||
return radius;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOutputRadius() {
|
||||
return outputRadius;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getInputRadius() {
|
||||
return inputRadius;
|
||||
@@ -196,7 +299,7 @@ public class MatterGeneratorCarvePassRadiusTest {
|
||||
int invocationChunkRadius,
|
||||
ChunkContext context
|
||||
) {
|
||||
return chunkInputRadius;
|
||||
return destinationInputRadius > 0 ? destinationInputRadius : chunkInputRadius;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+5
-5
@@ -21,8 +21,8 @@ package art.arcane.iris.engine.mantle.components;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IObjectPlacer;
|
||||
import art.arcane.iris.engine.object.TileData;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveAction;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveAction;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveCell;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import org.junit.Test;
|
||||
|
||||
@@ -112,10 +112,10 @@ public class CaveObjectPlacementTransactionTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void protectedHydrologyRejectsTheWholePlacement() {
|
||||
public void dryHydrologyHeadroomRejectsTheWholePlacement() {
|
||||
IObjectPlacer delegate = createPlacer(128, 80, 20, 90);
|
||||
when(delegate.getData(4, 30, 7, RiverCaveHydrology.class))
|
||||
.thenReturn(RiverCaveHydrology.of(RiverCaveAction.WET_SOURCE));
|
||||
when(delegate.getData(4, 30, 7, HydrologyCaveCell.class))
|
||||
.thenReturn(HydrologyCaveCell.of(HydrologyCaveAction.DRY_AIR));
|
||||
CaveObjectPlacementTransaction transaction = new CaveObjectPlacementTransaction(delegate, 20, 10);
|
||||
|
||||
transaction.set(4, 30, 7, mock(PlatformBlockState.class));
|
||||
|
||||
+24
@@ -1,5 +1,6 @@
|
||||
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.mantle.MantleComponent;
|
||||
import art.arcane.iris.engine.mantle.MantlePass;
|
||||
@@ -11,6 +12,7 @@ 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.stream.ProceduralStream;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
@@ -23,11 +25,14 @@ import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyDouble;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class MantleCarvingComponentBoundaryRadiusTest {
|
||||
@@ -56,6 +61,25 @@ public class MantleCarvingComponentBoundaryRadiusTest {
|
||||
assertEquals(1, Math.ceilDiv(component.getRadius(), 16));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void carvingSurfaceInputUsesNaturalTerrainInsteadOfHydrologyHeight() {
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
ProceduralStream<Double> naturalHeight = mock(ProceduralStream.class);
|
||||
when(complex.getNaturalHeightStream()).thenReturn(naturalHeight);
|
||||
when(naturalHeight.getDouble(anyDouble(), anyDouble())).thenReturn(70D);
|
||||
ChunkContext context = mock(ChunkContext.class);
|
||||
when(context.getComplex()).thenReturn(complex);
|
||||
|
||||
int[] heights = MantleCarvingComponent.prepareChunkSurfaceHeights(2, -3, context, new int[256]);
|
||||
|
||||
for (int height : heights) {
|
||||
assertEquals(70, height);
|
||||
}
|
||||
verify(complex, never()).getHeightStream();
|
||||
verify(context, never()).getHeight();
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void matterGenerationRunsCarvingForAllAdjacentChunks() {
|
||||
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
package art.arcane.iris.engine.mantle.components;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.cave.CavePosition;
|
||||
import art.arcane.iris.engine.hydrology.cave.CaveVoxel;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.matter.MatterCavern;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
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.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class MantleHydrologyCaveVoxelViewTest {
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void authoritativeChunkFlagControlsWhetherCarvingInputIsRequired() {
|
||||
Mantle<Matter> mantle = mock(Mantle.class);
|
||||
when(mantle.hasFlag(2, -3, ReservedFlag.CARVED)).thenReturn(true);
|
||||
|
||||
assertFalse(MantleHydrologyCaveVoxelView.requiresCarvingInput(mantle, 2, -3));
|
||||
assertTrue(MantleHydrologyCaveVoxelView.requiresCarvingInput(mantle, 3, -3));
|
||||
|
||||
verify(mantle).hasFlag(2, -3, ReservedFlag.CARVED);
|
||||
verify(mantle).hasFlag(3, -3, ReservedFlag.CARVED);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void mapsCarvedAndStoredMatterAndLoadsEachChunkOnce() {
|
||||
Mantle<Matter> mantle = mock(Mantle.class);
|
||||
MatterCavern air = new MatterCavern(true, "", (byte) 0);
|
||||
MatterCavern water = new MatterCavern(true, "", (byte) 1);
|
||||
MatterCavern lava = new MatterCavern(true, "", (byte) 2);
|
||||
PlatformBlockState incompatibleFluid = mock(PlatformBlockState.class);
|
||||
PlatformBlockState lavaBlock = mock(PlatformBlockState.class);
|
||||
PlatformBlockState solid = mock(PlatformBlockState.class);
|
||||
doReturn(air).when(mantle).get(0, 20, 0, MatterCavern.class);
|
||||
doReturn(water).when(mantle).get(1, 20, 0, MatterCavern.class);
|
||||
doReturn(lava).when(mantle).get(2, 20, 0, MatterCavern.class);
|
||||
doReturn(incompatibleFluid).when(mantle).get(3, 20, 0, PlatformBlockState.class);
|
||||
doReturn(lavaBlock).when(mantle).get(4, 20, 0, PlatformBlockState.class);
|
||||
doReturn(solid).when(mantle).get(5, 20, 0, PlatformBlockState.class);
|
||||
when(incompatibleFluid.isFluid()).thenReturn(true);
|
||||
when(incompatibleFluid.materialKey()).thenReturn("minecraft:water");
|
||||
when(lavaBlock.isFluid()).thenReturn(true);
|
||||
when(lavaBlock.materialKey()).thenReturn("minecraft:lava");
|
||||
when(solid.isFluid()).thenReturn(false);
|
||||
List<String> loaded = new ArrayList<>();
|
||||
MantleHydrologyCaveVoxelView view = new MantleHydrologyCaveVoxelView(
|
||||
mantle,
|
||||
128,
|
||||
(x, z) -> 60,
|
||||
(chunkX, chunkZ) -> loaded.add(chunkX + "," + chunkZ)
|
||||
);
|
||||
|
||||
assertEquals(CaveVoxel.CAVE_AIR, view.voxelAt(new CavePosition(0, 20, 0)));
|
||||
assertEquals(CaveVoxel.COMPATIBLE_FLUID, view.voxelAt(new CavePosition(1, 20, 0)));
|
||||
assertEquals(CaveVoxel.LAVA, view.voxelAt(new CavePosition(2, 20, 0)));
|
||||
assertEquals(CaveVoxel.INCOMPATIBLE_FLUID, view.voxelAt(new CavePosition(3, 20, 0)));
|
||||
assertEquals(CaveVoxel.LAVA, view.voxelAt(new CavePosition(4, 20, 0)));
|
||||
assertEquals(CaveVoxel.SOLID, view.voxelAt(new CavePosition(5, 20, 0)));
|
||||
assertEquals(CaveVoxel.SOLID, view.voxelAt(new CavePosition(6, 20, 0)));
|
||||
assertEquals(CaveVoxel.CAVE_AIR, view.voxelAt(new CavePosition(7, 61, 0)));
|
||||
assertTrue(view.isOpenToSurface(new CavePosition(7, 61, 0)));
|
||||
assertEquals(CaveVoxel.SOLID, view.voxelAt(new CavePosition(16, 20, 0)));
|
||||
assertEquals(CaveVoxel.CAVE_AIR, view.voxelAt(new CavePosition(16, 61, 0)));
|
||||
|
||||
assertEquals(List.of("0,0", "1,0"), loaded);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void carvedVerticalShaftIsOpenToSurface() {
|
||||
Mantle<Matter> mantle = mock(Mantle.class);
|
||||
MatterCavern air = new MatterCavern(true, "", (byte) 0);
|
||||
doAnswer(invocation -> {
|
||||
int x = invocation.getArgument(0);
|
||||
int y = invocation.getArgument(1);
|
||||
int z = invocation.getArgument(2);
|
||||
return x == 4 && z == 4 && y >= 20 && y <= 60 ? air : null;
|
||||
}).when(mantle).get(anyInt(), anyInt(), anyInt(), eq(MatterCavern.class));
|
||||
List<String> loaded = new ArrayList<>();
|
||||
MantleHydrologyCaveVoxelView view = new MantleHydrologyCaveVoxelView(
|
||||
mantle,
|
||||
128,
|
||||
(x, z) -> 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);
|
||||
}
|
||||
}
|
||||
+1871
File diff suppressed because it is too large.
Load diff
+141
-8
@@ -2,10 +2,12 @@ package art.arcane.iris.engine.mantle.components;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.loader.ResourceLoader;
|
||||
import art.arcane.iris.engine.UpperDimensionContext;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.mantle.EngineMantle;
|
||||
import art.arcane.iris.engine.mantle.MantleComponent;
|
||||
import art.arcane.iris.engine.mantle.MantlePass;
|
||||
import art.arcane.iris.engine.mantle.MantleWriter;
|
||||
import art.arcane.iris.engine.mantle.MatterGenerator;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
@@ -42,9 +44,9 @@ import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
@@ -60,9 +62,16 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
||||
@Before
|
||||
public void bindPlatform() {
|
||||
IrisPlatforms.unbind();
|
||||
PlatformBlockState block = mock(PlatformBlockState.class);
|
||||
PlatformRegistries registries = mock(PlatformRegistries.class);
|
||||
when(registries.block(anyString())).thenReturn(block);
|
||||
when(registries.block(anyString())).thenAnswer(invocation -> {
|
||||
String key = invocation.getArgument(0);
|
||||
PlatformBlockState block = mock(PlatformBlockState.class);
|
||||
when(block.key()).thenReturn(key);
|
||||
when(block.materialKey()).thenReturn(key);
|
||||
when(block.isSolid()).thenReturn(!key.toLowerCase().contains("air"));
|
||||
when(block.isOccluding()).thenReturn(!key.toLowerCase().contains("air"));
|
||||
return block;
|
||||
});
|
||||
IrisPlatform platform = mock(IrisPlatform.class);
|
||||
when(platform.registries()).thenReturn(registries);
|
||||
IrisPlatforms.bind(platform);
|
||||
@@ -93,6 +102,49 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
||||
assertEquals(43, MantleObjectComponent.calculatePlacementReach(new IrisBlockVector(5, 4, 3), placement));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sourceReplayUsesAscendingSignedCoordinates() {
|
||||
List<String> replayedSources = new ArrayList<>();
|
||||
|
||||
MantleObjectComponent.replaySourceChunks(
|
||||
-2,
|
||||
-3,
|
||||
16,
|
||||
(chunkX, chunkZ) -> replayedSources.add(chunkX + "," + chunkZ)
|
||||
);
|
||||
|
||||
assertEquals(List.of(
|
||||
"-3,-4", "-3,-3", "-3,-2",
|
||||
"-2,-4", "-2,-3", "-2,-2",
|
||||
"-1,-4", "-1,-3", "-1,-2"
|
||||
), replayedSources);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sourcePredecessorReplayUsesTheStableAnchoredPrefix() {
|
||||
List<String> replayedSources = new ArrayList<>();
|
||||
|
||||
MantleObjectComponent.replaySourcePredecessors(
|
||||
-2,
|
||||
-3,
|
||||
16,
|
||||
(chunkX, chunkZ) -> replayedSources.add(chunkX + "," + chunkZ)
|
||||
);
|
||||
|
||||
assertEquals(List.of(
|
||||
"-3,-4", "-3,-3", "-3,-2",
|
||||
"-2,-4"
|
||||
), replayedSources);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void collisionInputRadiusCoversBothSourceTraversalLegs() {
|
||||
assertEquals(82, MantleObjectComponent.calculateInputRadius(33, false));
|
||||
assertEquals(130, MantleObjectComponent.calculateInputRadius(33, true));
|
||||
assertEquals(Integer.MAX_VALUE,
|
||||
MantleObjectComponent.calculateInputRadius(Integer.MAX_VALUE, true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void proceduralTreeTransformsContributeToRadius() {
|
||||
IrisObjectPlacement placement = new IrisObjectPlacement()
|
||||
@@ -120,6 +172,47 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
||||
assertEquals(53, new MantleObjectComponent(engineMantle).getRadius());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void upperDimensionObjectsDriveRadiusAndCollisionMode() {
|
||||
IrisObjectPlacement placement = new IrisObjectPlacement()
|
||||
.setRotation(IrisObjectRotation.of(90, 0, 0))
|
||||
.setTranslate(new IrisObjectTranslate().setX(3).setY(4));
|
||||
placement.getForbiddenCollisions().add("upper/blocker");
|
||||
IrisData baseData = mock(IrisData.class);
|
||||
IrisData upperData = mock(IrisData.class);
|
||||
IrisProceduralPlacement proceduralPlacement = mock(IrisProceduralPlacement.class);
|
||||
when(proceduralPlacement.getVariantObjects(upperData))
|
||||
.thenReturn(new KList<>(new IrisObject(5, 48, 7)));
|
||||
when(proceduralPlacement.asPlacement()).thenReturn(placement);
|
||||
IrisProceduralObjects proceduralObjects = mock(IrisProceduralObjects.class);
|
||||
when(proceduralObjects.isEmpty()).thenReturn(false);
|
||||
when(proceduralObjects.getAllPlacements()).thenReturn(new KList<>(proceduralPlacement));
|
||||
IrisRegion upperRegion = mock(IrisRegion.class);
|
||||
when(upperRegion.getObjects()).thenReturn(new KList<>());
|
||||
when(upperRegion.getProceduralObjects()).thenReturn(proceduralObjects);
|
||||
IrisDimension baseDimension = mock(IrisDimension.class);
|
||||
when(baseDimension.isUpperDimensionObjects()).thenReturn(true);
|
||||
when(baseDimension.getAllRegions(any())).thenReturn(new KList<>());
|
||||
when(baseDimension.getReachableBiomes(any())).thenReturn(new KList<>());
|
||||
IrisDimension upperDimension = mock(IrisDimension.class);
|
||||
when(upperDimension.getAllRegions(any())).thenReturn(new KList<>(upperRegion));
|
||||
when(upperDimension.getReachableBiomes(any())).thenReturn(new KList<>());
|
||||
UpperDimensionContext upperContext = mock(UpperDimensionContext.class);
|
||||
when(upperContext.getDimension()).thenReturn(upperDimension);
|
||||
when(upperContext.getData()).thenReturn(upperData);
|
||||
Engine engine = mock(Engine.class);
|
||||
when(engine.getDimension()).thenReturn(baseDimension);
|
||||
when(engine.getUpperContext()).thenReturn(upperContext);
|
||||
EngineMantle engineMantle = mock(EngineMantle.class);
|
||||
when(engineMantle.getEngine()).thenReturn(engine);
|
||||
when(engineMantle.getData()).thenReturn(baseData);
|
||||
|
||||
MantleObjectComponent component = new MantleObjectComponent(engineMantle);
|
||||
|
||||
assertEquals(53, component.getRadius());
|
||||
assertEquals(182, component.getInputRadius());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void translatedTreeSchedulesDistantOwnerChunks() throws Exception {
|
||||
@@ -153,11 +246,12 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
||||
|
||||
MantleObjectComponent component = spy(new MantleObjectComponent(engineMantle));
|
||||
assertEquals(33, component.getRadius());
|
||||
int chunkRadius = Math.ceilDiv(component.getRadius(), 16);
|
||||
assertEquals(3, chunkRadius);
|
||||
assertEquals(33, component.getOutputRadius());
|
||||
assertEquals(0, component.getInputRadius());
|
||||
|
||||
Mantle<Matter> mantle = mock(Mantle.class);
|
||||
MantleChunk<Matter> chunk = mock(MantleChunk.class);
|
||||
when(mantle.getWorldHeight()).thenReturn(64);
|
||||
when(mantle.getChunk(anyInt(), anyInt())).thenReturn(chunk);
|
||||
when(chunk.use()).thenReturn(chunk);
|
||||
doAnswer(invocation -> {
|
||||
@@ -166,6 +260,25 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
||||
return null;
|
||||
}).when(chunk).raiseFlagSuspend(any(), any(Runnable.class));
|
||||
|
||||
MantleWriter directWriter = mock(MantleWriter.class);
|
||||
when(directWriter.getMantle()).thenReturn(mantle);
|
||||
doAnswer(invocation -> {
|
||||
Runnable task = invocation.getArgument(2);
|
||||
task.run();
|
||||
return null;
|
||||
}).when(directWriter).withChunkFence(anyInt(), anyInt(), any(Runnable.class));
|
||||
AtomicInteger directOrigins = new AtomicInteger();
|
||||
doAnswer(invocation -> {
|
||||
directOrigins.incrementAndGet();
|
||||
return null;
|
||||
}).when(component).generateOrigin(any(), anyInt(), anyInt(), any());
|
||||
|
||||
component.generateLayer(directWriter, 0, 0, mock(ChunkContext.class));
|
||||
|
||||
assertEquals(1, directOrigins.get());
|
||||
component.generateLayer(directWriter, 0, 0, mock(ChunkContext.class));
|
||||
assertEquals(2, directOrigins.get());
|
||||
|
||||
List<String> generatedChunks = new ArrayList<>();
|
||||
doAnswer(invocation -> {
|
||||
int chunkX = invocation.getArgument(1);
|
||||
@@ -174,11 +287,31 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
||||
return null;
|
||||
}).when(component).generateLayer(any(), anyInt(), anyInt(), any());
|
||||
|
||||
TestMatterGenerator generator = new TestMatterGenerator(new GeneratorOptions(engine, mantle, component, chunkRadius));
|
||||
TestMatterGenerator generator = new TestMatterGenerator(new GeneratorOptions(
|
||||
engine,
|
||||
mantle,
|
||||
component,
|
||||
component.getOutputRadius()
|
||||
));
|
||||
generator.generateMatter(0, 0, false, mock(ChunkContext.class));
|
||||
|
||||
assertTrue(generatedChunks.contains("-2,0"));
|
||||
assertTrue(generatedChunks.contains("2,0"));
|
||||
assertEquals(49, generatedChunks.size());
|
||||
|
||||
List<String> replayedSources = new ArrayList<>();
|
||||
MantleObjectComponent.replaySourceChunks(
|
||||
0,
|
||||
0,
|
||||
component.getRadius(),
|
||||
(chunkX, chunkZ) -> replayedSources.add(chunkX + "," + chunkZ)
|
||||
);
|
||||
assertEquals(49, replayedSources.size());
|
||||
assertEquals("-3,-3", replayedSources.getFirst());
|
||||
assertEquals("-3,-2", replayedSources.get(1));
|
||||
assertEquals("3,3", replayedSources.getLast());
|
||||
|
||||
placement.getForbiddenCollisions().add("test/tree");
|
||||
component.hotload();
|
||||
assertEquals(130, component.getInputRadius());
|
||||
}
|
||||
|
||||
private static final class TestMatterGenerator implements MatterGenerator {
|
||||
|
||||
+9
-6
@@ -1,8 +1,8 @@
|
||||
package art.arcane.iris.engine.mantle.components;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveAction;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveAction;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveCell;
|
||||
import art.arcane.volmlib.util.matter.MatterCavern;
|
||||
import org.junit.Test;
|
||||
|
||||
@@ -49,15 +49,18 @@ public class MantleObjectComponentCaveAnchorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void protectedRiverHydrologyCannotBecomeAnObjectAnchor() {
|
||||
public void plannedHydrologyCannotBecomeAnObjectAnchor() {
|
||||
MatterCavern water = new MatterCavern(true, "", (byte) 1);
|
||||
MatterCavern forcedAir = new MatterCavern(true, "", (byte) 3);
|
||||
|
||||
assertFalse(MantleObjectComponent.acceptsCaveAnchorFluid(
|
||||
true, water, RiverCaveHydrology.of(RiverCaveAction.WET_SOURCE), 20, 8));
|
||||
true, water, HydrologyCaveCell.of(HydrologyCaveAction.WET_SOURCE), 20, 8));
|
||||
assertFalse(MantleObjectComponent.acceptsCaveAnchorFluid(
|
||||
true, water, RiverCaveHydrology.of(RiverCaveAction.FALLING_FLUID), 20, 8));
|
||||
true, water, HydrologyCaveCell.of(HydrologyCaveAction.FALLING_FLUID), 20, 8));
|
||||
assertFalse(MantleObjectComponent.acceptsCaveAnchorFluid(
|
||||
true, null, RiverCaveHydrology.of(RiverCaveAction.SEAL_GUARD), 20, 8));
|
||||
true, null, HydrologyCaveCell.of(HydrologyCaveAction.SEAL_GUARD), 20, 8));
|
||||
assertFalse(MantleObjectComponent.acceptsCaveAnchorFluid(
|
||||
false, forcedAir, HydrologyCaveCell.of(HydrologyCaveAction.DRY_AIR), 20, 8));
|
||||
}
|
||||
|
||||
private static IrisBiome biome(String loadKey) {
|
||||
|
||||
+234
@@ -0,0 +1,234 @@
|
||||
package art.arcane.iris.engine.mantle.components;
|
||||
|
||||
import art.arcane.iris.engine.mantle.MantleWriter;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
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 static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class MantleObjectComponentCollisionDeterminismTest {
|
||||
private static final int RADIUS = 16;
|
||||
private static final int TARGET_Y = 10;
|
||||
private static final String BLOCKER = "blocker@1";
|
||||
private static final String TARGET = "target@2";
|
||||
private static final Position LEFT = new Position(15, TARGET_Y, 0);
|
||||
private static final Position RIGHT = new Position(16, TARGET_Y, 0);
|
||||
|
||||
@Test
|
||||
public void shiftedDestinationWindowsWouldSplitOneCollisionDecision() {
|
||||
Scenario scenario = new Scenario(false, false, false);
|
||||
Map<Position, String> blocks = new HashMap<>();
|
||||
|
||||
blocks.putAll(generateDirectDestination(0, scenario));
|
||||
blocks.putAll(generateDirectDestination(1, scenario));
|
||||
|
||||
assertEquals(BLOCKER, blocks.get(LEFT));
|
||||
assertEquals(TARGET, blocks.get(RIGHT));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sourceAnchoredPlansMatchForForwardReverseAndParallelDestinations() throws Exception {
|
||||
Scenario scenario = new Scenario(false, false, false);
|
||||
Map<Position, String> forward = generateAnchoredWorld(List.of(0, 1), scenario);
|
||||
Map<Position, String> reverse = generateAnchoredWorld(List.of(1, 0), scenario);
|
||||
Map<Position, String> parallel = generateAnchoredWorldParallel(scenario);
|
||||
|
||||
assertEquals(forward, reverse);
|
||||
assertEquals(forward, parallel);
|
||||
assertFalse(TARGET.equals(forward.get(LEFT)));
|
||||
assertFalse(TARGET.equals(forward.get(RIGHT)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void allowedCollisionOverrideAcceptsEveryDestinationFragment() {
|
||||
Map<Position, String> blocks = generateAnchoredWorld(
|
||||
List.of(0, 1),
|
||||
new Scenario(true, false, false)
|
||||
);
|
||||
|
||||
assertEquals(TARGET, blocks.get(LEFT));
|
||||
assertEquals(TARGET, blocks.get(RIGHT));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sameSourcePredecessorStillVetoesEveryDestinationFragment() {
|
||||
Map<Position, String> blocks = generateAnchoredWorld(
|
||||
List.of(0, 1),
|
||||
new Scenario(false, true, false)
|
||||
);
|
||||
|
||||
assertEquals(BLOCKER, blocks.get(LEFT));
|
||||
assertFalse(TARGET.equals(blocks.get(RIGHT)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void blockerOutsideTheAnchoredHaloIsUniformlyIgnored() {
|
||||
Map<Position, String> blocks = generateAnchoredWorld(
|
||||
List.of(0, 1),
|
||||
new Scenario(false, false, true)
|
||||
);
|
||||
|
||||
assertEquals(TARGET, blocks.get(LEFT));
|
||||
assertEquals(TARGET, blocks.get(RIGHT));
|
||||
}
|
||||
|
||||
private static Map<Position, String> generateAnchoredWorld(List<Integer> destinations, Scenario scenario) {
|
||||
ObjectSourcePlanCache cache = new ObjectSourcePlanCache(256L);
|
||||
Map<Position, String> blocks = new HashMap<>();
|
||||
for (Integer destination : destinations) {
|
||||
blocks.putAll(generateAnchoredDestination(destination, scenario, cache));
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
private static Map<Position, String> generateAnchoredWorldParallel(Scenario scenario) throws Exception {
|
||||
ObjectSourcePlanCache cache = new ObjectSourcePlanCache(256L);
|
||||
CountDownLatch ready = new CountDownLatch(2);
|
||||
CountDownLatch start = new CountDownLatch(1);
|
||||
ExecutorService executor = Executors.newFixedThreadPool(2);
|
||||
try {
|
||||
Future<Map<Position, String>> left = executor.submit(
|
||||
() -> generateAnchoredDestinationAfterBarrier(0, scenario, cache, ready, start)
|
||||
);
|
||||
Future<Map<Position, String>> right = executor.submit(
|
||||
() -> generateAnchoredDestinationAfterBarrier(1, scenario, cache, ready, start)
|
||||
);
|
||||
assertTrue(ready.await(5L, TimeUnit.SECONDS));
|
||||
start.countDown();
|
||||
Map<Position, String> blocks = new HashMap<>(left.get(5L, TimeUnit.SECONDS));
|
||||
blocks.putAll(right.get(5L, TimeUnit.SECONDS));
|
||||
return blocks;
|
||||
} finally {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<Position, String> generateAnchoredDestinationAfterBarrier(
|
||||
int destination,
|
||||
Scenario scenario,
|
||||
ObjectSourcePlanCache cache,
|
||||
CountDownLatch ready,
|
||||
CountDownLatch start
|
||||
) throws Exception {
|
||||
ready.countDown();
|
||||
assertTrue(start.await(5L, TimeUnit.SECONDS));
|
||||
return generateAnchoredDestination(destination, scenario, cache);
|
||||
}
|
||||
|
||||
private static Map<Position, String> generateAnchoredDestination(
|
||||
int destination,
|
||||
Scenario scenario,
|
||||
ObjectSourcePlanCache cache
|
||||
) {
|
||||
Map<Position, String> blocks = new HashMap<>();
|
||||
MantleWriter writer = writer(blocks);
|
||||
ObjectDestinationTransaction transaction = new ObjectDestinationTransaction(writer, destination, 0);
|
||||
MantleObjectComponent.replaySourceChunks(destination, 0, RADIUS, (sourceX, sourceZ) ->
|
||||
transaction.apply(cache.get(sourceX, sourceZ,
|
||||
() -> buildSourcePlan(writer, sourceX, sourceZ, scenario))));
|
||||
transaction.commit();
|
||||
return blocks;
|
||||
}
|
||||
|
||||
private static Map<Position, String> generateDirectDestination(int destination, Scenario scenario) {
|
||||
Map<Position, String> blocks = new HashMap<>();
|
||||
MantleWriter writer = writer(blocks);
|
||||
ObjectDestinationTransaction transaction = new ObjectDestinationTransaction(writer, destination, 0);
|
||||
MantleObjectComponent.replaySourceChunks(destination, 0, RADIUS,
|
||||
(sourceX, sourceZ) -> generateOrigin(transaction, sourceX, sourceZ, scenario));
|
||||
transaction.commit();
|
||||
return blocks;
|
||||
}
|
||||
|
||||
private static ObjectSourcePlan buildSourcePlan(
|
||||
MantleWriter writer,
|
||||
int sourceX,
|
||||
int sourceZ,
|
||||
Scenario scenario
|
||||
) {
|
||||
ObjectDestinationTransaction scratch = new ObjectDestinationTransaction(writer, sourceX, sourceZ);
|
||||
MantleObjectComponent.replaySourcePredecessors(sourceX, sourceZ, RADIUS,
|
||||
(predecessorX, predecessorZ) -> generateOrigin(scratch, predecessorX, predecessorZ, scenario));
|
||||
int checkpoint = scratch.mutationCheckpoint();
|
||||
generateOrigin(scratch, sourceX, sourceZ, scenario);
|
||||
return scratch.sourcePlanSince(checkpoint);
|
||||
}
|
||||
|
||||
private static void generateOrigin(
|
||||
ObjectDestinationTransaction transaction,
|
||||
int sourceX,
|
||||
int sourceZ,
|
||||
Scenario scenario
|
||||
) {
|
||||
if (isBlockerSource(sourceX, sourceZ, scenario)) {
|
||||
transaction.setData(LEFT.x(), LEFT.y(), LEFT.z(), BLOCKER);
|
||||
}
|
||||
if (sourceX != 0 || sourceZ != 0) {
|
||||
return;
|
||||
}
|
||||
String collision = transaction.getDataIfPresent(LEFT.x(), LEFT.y(), LEFT.z(), String.class);
|
||||
if (BLOCKER.equals(collision) && !scenario.allowed()) {
|
||||
return;
|
||||
}
|
||||
transaction.setData(LEFT.x(), LEFT.y(), LEFT.z(), TARGET);
|
||||
transaction.setData(RIGHT.x(), RIGHT.y(), RIGHT.z(), TARGET);
|
||||
}
|
||||
|
||||
private static boolean isBlockerSource(int sourceX, int sourceZ, Scenario scenario) {
|
||||
if (sourceZ != 0) {
|
||||
return false;
|
||||
}
|
||||
if (scenario.sameSource()) {
|
||||
return sourceX == 0;
|
||||
}
|
||||
return sourceX == (scenario.outsideHalo() ? -2 : -1);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static MantleWriter writer(Map<Position, String> blocks) {
|
||||
MantleWriter writer = mock(MantleWriter.class);
|
||||
Mantle<Matter> mantle = mock(Mantle.class);
|
||||
when(mantle.getWorldHeight()).thenReturn(64);
|
||||
when(writer.getMantle()).thenReturn(mantle);
|
||||
when(writer.getPrerequisiteDataIfPresent(anyInt(), anyInt(), anyInt(), any())).thenReturn(null);
|
||||
doAnswer(invocation -> {
|
||||
int x = invocation.getArgument(0);
|
||||
int y = invocation.getArgument(1);
|
||||
int z = invocation.getArgument(2);
|
||||
Object value = invocation.getArgument(3);
|
||||
if (value instanceof String marker) {
|
||||
blocks.put(new Position(x, y, z), marker);
|
||||
}
|
||||
return null;
|
||||
}).when(writer).setData(anyInt(), anyInt(), anyInt(), any());
|
||||
doAnswer(invocation -> {
|
||||
Runnable task = invocation.getArgument(2);
|
||||
task.run();
|
||||
return null;
|
||||
}).when(writer).withChunkFence(anyInt(), anyInt(), any(Runnable.class));
|
||||
return writer;
|
||||
}
|
||||
|
||||
private record Scenario(boolean allowed, boolean sameSource, boolean outsideHalo) {
|
||||
}
|
||||
|
||||
private record Position(int x, int y, int z) {
|
||||
}
|
||||
}
|
||||
-182
@@ -1,182 +0,0 @@
|
||||
package art.arcane.iris.engine.mantle.components;
|
||||
|
||||
import art.arcane.iris.engine.river.cave.CavePosition;
|
||||
import art.arcane.iris.engine.river.cave.CaveVoxel;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveAction;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveFluidKind;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.mantle.runtime.TectonicPlate;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.matter.MatterSlice;
|
||||
import art.arcane.volmlib.util.matter.MatterCavern;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class MantleRiverCaveVoxelViewTest {
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void absentCellsAboveLocalTerrainAreOpenAir() {
|
||||
Mantle<Matter> mantle = mock(Mantle.class);
|
||||
when(mantle.getLoadedRegions()).thenReturn(new KMap<Long, TectonicPlate<Matter>>());
|
||||
MantleRiverCaveVoxelView view = new MantleRiverCaveVoxelView(
|
||||
mantle,
|
||||
128,
|
||||
(x, z) -> x < 0 ? 20 : 60,
|
||||
(x, z) -> null,
|
||||
RiverCaveFluidKind.RIVER,
|
||||
(chunkX, chunkZ) -> {
|
||||
}
|
||||
);
|
||||
CavePosition cliffAir = new CavePosition(-1, 21, 0);
|
||||
CavePosition terrain = new CavePosition(-1, 20, 0);
|
||||
|
||||
assertEquals(CaveVoxel.CAVE_AIR, view.voxelAt(cliffAir));
|
||||
assertTrue(view.isOpenToSurface(cliffAir));
|
||||
assertEquals(CaveVoxel.SOLID, view.voxelAt(terrain));
|
||||
assertFalse(view.isOpenToSurface(terrain));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void carvingInputLoadsOnlyOncePerReadChunk() {
|
||||
Mantle<Matter> mantle = mock(Mantle.class);
|
||||
when(mantle.getLoadedRegions()).thenReturn(new KMap<Long, TectonicPlate<Matter>>());
|
||||
List<String> loaded = new ArrayList<>();
|
||||
MantleRiverCaveVoxelView view = new MantleRiverCaveVoxelView(
|
||||
mantle,
|
||||
128,
|
||||
(x, z) -> 60,
|
||||
(x, z) -> null,
|
||||
RiverCaveFluidKind.RIVER,
|
||||
(chunkX, chunkZ) -> loaded.add(chunkX + "," + chunkZ)
|
||||
);
|
||||
|
||||
view.voxelAt(new CavePosition(0, 20, 0));
|
||||
view.voxelAt(new CavePosition(15, 21, 15));
|
||||
view.riverHydrologyAt(new CavePosition(1, 22, 1));
|
||||
view.voxelAt(new CavePosition(16, 20, 0));
|
||||
|
||||
assertEquals(List.of("0,0", "1,0"), loaded);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void publishedRiverActionsDoNotReplaceTheUnderlyingPlanningBaseline() {
|
||||
Mantle<Matter> mantle = mock(Mantle.class);
|
||||
TectonicPlate<Matter> plate = mock(TectonicPlate.class);
|
||||
MantleChunk<Matter> chunk = mock(MantleChunk.class);
|
||||
Matter matter = mock(Matter.class);
|
||||
MatterSlice<RiverCaveHydrology> hydrologySlice = mock(MatterSlice.class);
|
||||
KMap<Long, TectonicPlate<Matter>> regions = new KMap<>();
|
||||
regions.put(Mantle.key(0, 0), plate);
|
||||
CavePosition position = new CavePosition(0, 20, 0);
|
||||
when(mantle.getLoadedRegions()).thenReturn(regions);
|
||||
when(plate.get(0, 0)).thenReturn(chunk);
|
||||
when(chunk.exists(1)).thenReturn(true);
|
||||
when(chunk.get(1)).thenReturn(matter);
|
||||
when(matter.hasSlice(RiverCaveHydrology.class)).thenReturn(true);
|
||||
doReturn(hydrologySlice).when(matter).getSlice(RiverCaveHydrology.class);
|
||||
when(hydrologySlice.get(0, 4, 0)).thenReturn(RiverCaveHydrology.of(RiverCaveAction.WET_SOURCE));
|
||||
MantleRiverCaveVoxelView view = new MantleRiverCaveVoxelView(
|
||||
mantle,
|
||||
128,
|
||||
(x, z) -> 60,
|
||||
(x, z) -> null,
|
||||
RiverCaveFluidKind.RIVER,
|
||||
(chunkX, chunkZ) -> {
|
||||
}
|
||||
);
|
||||
|
||||
assertEquals(CaveVoxel.SOLID, view.voxelAt(position));
|
||||
assertEquals(RiverCaveAction.WET_SOURCE, view.riverHydrologyAt(position).action());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void configuredLavaRiverTreatsPublishedLavaAsCompatibleFluid() {
|
||||
Mantle<Matter> mantle = mock(Mantle.class);
|
||||
TectonicPlate<Matter> plate = mock(TectonicPlate.class);
|
||||
MantleChunk<Matter> chunk = mock(MantleChunk.class);
|
||||
Matter matter = mock(Matter.class);
|
||||
MatterSlice<MatterCavern> cavernSlice = mock(MatterSlice.class);
|
||||
PlatformBlockState lava = mock(PlatformBlockState.class);
|
||||
KMap<Long, TectonicPlate<Matter>> regions = new KMap<>();
|
||||
regions.put(Mantle.key(0, 0), plate);
|
||||
when(mantle.getLoadedRegions()).thenReturn(regions);
|
||||
when(plate.get(0, 0)).thenReturn(chunk);
|
||||
when(chunk.exists(1)).thenReturn(true);
|
||||
when(chunk.get(1)).thenReturn(matter);
|
||||
when(matter.hasSlice(MatterCavern.class)).thenReturn(true);
|
||||
doReturn(cavernSlice).when(matter).getSlice(MatterCavern.class);
|
||||
when(cavernSlice.get(0, 4, 0)).thenReturn(new MatterCavern(true, "", (byte) 2));
|
||||
when(lava.materialKey()).thenReturn("minecraft:lava");
|
||||
MantleRiverCaveVoxelView view = new MantleRiverCaveVoxelView(
|
||||
mantle,
|
||||
128,
|
||||
(x, z) -> 60,
|
||||
(x, z) -> lava,
|
||||
RiverCaveFluidKind.RIVER,
|
||||
(chunkX, chunkZ) -> {
|
||||
}
|
||||
);
|
||||
|
||||
assertEquals(CaveVoxel.COMPATIBLE_FLUID, view.voxelAt(new CavePosition(0, 20, 0)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void planningRejectsHydrologyOwnedByTheOtherFluidKind() {
|
||||
Mantle<Matter> mantle = mock(Mantle.class);
|
||||
TectonicPlate<Matter> plate = mock(TectonicPlate.class);
|
||||
MantleChunk<Matter> chunk = mock(MantleChunk.class);
|
||||
Matter matter = mock(Matter.class);
|
||||
MatterSlice<RiverCaveHydrology> hydrologySlice = mock(MatterSlice.class);
|
||||
KMap<Long, TectonicPlate<Matter>> regions = new KMap<>();
|
||||
regions.put(Mantle.key(0, 0), plate);
|
||||
CavePosition position = new CavePosition(0, 20, 0);
|
||||
when(mantle.getLoadedRegions()).thenReturn(regions);
|
||||
when(plate.get(0, 0)).thenReturn(chunk);
|
||||
when(chunk.exists(1)).thenReturn(true);
|
||||
when(chunk.get(1)).thenReturn(matter);
|
||||
when(matter.hasSlice(RiverCaveHydrology.class)).thenReturn(true);
|
||||
doReturn(hydrologySlice).when(matter).getSlice(RiverCaveHydrology.class);
|
||||
when(hydrologySlice.get(0, 4, 0)).thenReturn(RiverCaveHydrology.of(
|
||||
RiverCaveAction.WET_SOURCE,
|
||||
RiverCaveFluidKind.DEEP_POOL
|
||||
));
|
||||
MantleRiverCaveVoxelView riverView = new MantleRiverCaveVoxelView(
|
||||
mantle,
|
||||
128,
|
||||
(x, z) -> 60,
|
||||
(x, z) -> null,
|
||||
RiverCaveFluidKind.RIVER,
|
||||
(chunkX, chunkZ) -> {
|
||||
}
|
||||
);
|
||||
MantleRiverCaveVoxelView deepPoolView = new MantleRiverCaveVoxelView(
|
||||
mantle,
|
||||
128,
|
||||
(x, z) -> 60,
|
||||
(x, z) -> null,
|
||||
RiverCaveFluidKind.DEEP_POOL,
|
||||
(chunkX, chunkZ) -> {
|
||||
}
|
||||
);
|
||||
|
||||
assertEquals(CaveVoxel.INCOMPATIBLE_FLUID, riverView.voxelAt(position));
|
||||
assertEquals(CaveVoxel.SOLID, deepPoolView.voxelAt(position));
|
||||
}
|
||||
}
|
||||
-1025
File diff suppressed because it is too large.
Load diff
+244
@@ -0,0 +1,244 @@
|
||||
package art.arcane.iris.engine.mantle.components;
|
||||
|
||||
import art.arcane.iris.core.link.Identifier;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveAction;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveCell;
|
||||
import art.arcane.iris.engine.mantle.MantleWriter;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.matter.MatterCavern;
|
||||
import org.junit.Test;
|
||||
import org.mockito.InOrder;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
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.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
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 ObjectDestinationTransactionTest {
|
||||
@Test
|
||||
public void overlayReadsEarlierWritesAndCommitsOnlyDestinationChunk() {
|
||||
MantleWriter writer = writer();
|
||||
Marker prerequisite = new Marker("prerequisite");
|
||||
Marker outside = new Marker("outside");
|
||||
Marker first = new Marker("first");
|
||||
Marker second = new Marker("second");
|
||||
when(writer.getPrerequisiteDataIfPresent(0, 4, 0, Marker.class)).thenReturn(prerequisite);
|
||||
ObjectDestinationTransaction transaction = new ObjectDestinationTransaction(writer, 0, 0);
|
||||
|
||||
assertSame(prerequisite, transaction.getDataIfPresent(0, 4, 0, Marker.class));
|
||||
transaction.setData(-1, 4, 0, outside);
|
||||
transaction.setData(0, 4, 0, first);
|
||||
transaction.setData(0, 4, 0, second);
|
||||
assertSame(outside, transaction.getDataIfPresent(-1, 4, 0, Marker.class));
|
||||
assertSame(second, transaction.getDataIfPresent(0, 4, 0, Marker.class));
|
||||
|
||||
transaction.commit();
|
||||
|
||||
verify(writer, never()).setData(-1, 4, 0, outside);
|
||||
InOrder order = inOrder(writer);
|
||||
order.verify(writer).setData(0, 4, 0, first);
|
||||
order.verify(writer).setData(0, 4, 0, second);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sourcePlanCapturesOnlyTheSourceMutationTail() {
|
||||
MantleWriter writer = writer();
|
||||
Marker predecessor = new Marker("predecessor");
|
||||
Marker source = new Marker("source");
|
||||
ObjectDestinationTransaction scratch = new ObjectDestinationTransaction(writer, 0, 0);
|
||||
scratch.setData(0, 4, 0, predecessor);
|
||||
int checkpoint = scratch.mutationCheckpoint();
|
||||
scratch.setData(1, 4, 0, source);
|
||||
ObjectSourcePlan plan = scratch.sourcePlanSince(checkpoint);
|
||||
ObjectDestinationTransaction destination = new ObjectDestinationTransaction(writer, 0, 0);
|
||||
|
||||
destination.apply(plan);
|
||||
|
||||
assertNull(destination.getDataIfPresent(0, 4, 0, Marker.class));
|
||||
assertSame(source, destination.getDataIfPresent(1, 4, 0, Marker.class));
|
||||
destination.commit();
|
||||
verify(writer, never()).setData(0, 4, 0, predecessor);
|
||||
verify(writer).setData(1, 4, 0, source);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sourcePlanRejectsAnInvalidCheckpoint() {
|
||||
ObjectDestinationTransaction transaction = new ObjectDestinationTransaction(writer(), 0, 0);
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> transaction.sourcePlanSince(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void carvedColumnComposesPrerequisiteHydrologyAndEarlierOriginWrites() {
|
||||
MantleWriter writer = writer();
|
||||
byte[] prerequisite = new byte[8];
|
||||
prerequisite[4] = 1;
|
||||
when(writer.getPrerequisiteCarvedColumn(0, 0, 8)).thenReturn(prerequisite);
|
||||
when(writer.isPrerequisiteCarved(0, 4, 0)).thenReturn(true);
|
||||
ObjectDestinationTransaction transaction = new ObjectDestinationTransaction(writer, 0, 0);
|
||||
HydrologyCaveCell sealed = HydrologyCaveCell.of(HydrologyCaveAction.SEAL_GUARD);
|
||||
MatterCavern cavern = new MatterCavern(true, "", (byte) 0);
|
||||
|
||||
transaction.setData(0, 4, 0, sealed);
|
||||
transaction.setData(0, 5, 0, cavern);
|
||||
|
||||
byte[] expected = new byte[8];
|
||||
expected[5] = 1;
|
||||
assertArrayEquals(expected, transaction.getCarvedColumn(0, 0, 8));
|
||||
assertFalse(transaction.isCarved(0, 4, 0));
|
||||
assertTrue(transaction.isCarved(0, 5, 0));
|
||||
assertArrayEquals(new byte[]{0, 0}, transaction.getCarvedColumn(0, 0, 2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failedCommitRestoresEveryTouchedDestinationCell() {
|
||||
MantleWriter writer = writer();
|
||||
Marker firstOriginal = new Marker("first-original");
|
||||
Marker secondOriginal = new Marker("second-original");
|
||||
Marker first = new Marker("first");
|
||||
Marker second = new Marker("second");
|
||||
when(writer.getPrerequisiteDataIfPresent(0, 4, 0, Marker.class)).thenReturn(firstOriginal);
|
||||
when(writer.getPrerequisiteDataIfPresent(1, 4, 0, Marker.class)).thenReturn(secondOriginal);
|
||||
doThrow(new IllegalStateException("publication failed"))
|
||||
.when(writer).setData(1, 4, 0, second);
|
||||
ObjectDestinationTransaction transaction = new ObjectDestinationTransaction(writer, 0, 0);
|
||||
transaction.setData(0, 4, 0, first);
|
||||
transaction.setData(1, 4, 0, second);
|
||||
|
||||
assertThrows(IllegalStateException.class, transaction::commit);
|
||||
|
||||
verify(writer).clearData(0, 4, 0, Marker.class);
|
||||
verify(writer).clearData(1, 4, 0, Marker.class);
|
||||
verify(writer).setData(0, 4, 0, firstOriginal);
|
||||
verify(writer).setData(1, 4, 0, secondOriginal);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectedWriterMutationsNeverEnterTheOverlay() {
|
||||
MantleWriter writer = writer();
|
||||
PlatformBlockState prerequisite = mock(PlatformBlockState.class);
|
||||
PlatformBlockState rejected = mock(PlatformBlockState.class);
|
||||
MatterCavern cavern = new MatterCavern(true, "", (byte) 0);
|
||||
when(writer.getPrerequisiteBlock(0, 0, 0)).thenReturn(prerequisite);
|
||||
when(writer.getEngine().getDimension().isBedrock()).thenReturn(true);
|
||||
when(writer.getPrerequisiteDataIfPresent(1, 4, 0, HydrologyCaveCell.class))
|
||||
.thenReturn(HydrologyCaveCell.of(HydrologyCaveAction.SEAL_GUARD));
|
||||
ObjectDestinationTransaction transaction = new ObjectDestinationTransaction(writer, 0, 0);
|
||||
|
||||
transaction.setData(0, 0, 0, rejected);
|
||||
transaction.setData(1, 4, 0, cavern);
|
||||
|
||||
assertSame(prerequisite, transaction.get(0, 0, 0));
|
||||
assertFalse(transaction.isCarved(1, 4, 0));
|
||||
transaction.commit();
|
||||
verify(writer, never()).setData(0, 0, 0, rejected);
|
||||
verify(writer, never()).setData(1, 4, 0, cavern);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptedCustomPlacementPublishesBlockAndIdentifierAsOneMutation() {
|
||||
MantleWriter writer = writer();
|
||||
PlatformBlockState custom = mock(PlatformBlockState.class);
|
||||
PlatformBlockState base = mock(PlatformBlockState.class);
|
||||
Identifier identifier = Identifier.fromString("iris:custom_block");
|
||||
when(custom.isCustom()).thenReturn(true);
|
||||
when(custom.deferredPlacementKey()).thenReturn(identifier.toString());
|
||||
when(custom.placementBaseState()).thenReturn(base);
|
||||
ObjectDestinationTransaction transaction = new ObjectDestinationTransaction(writer, 0, 0);
|
||||
|
||||
transaction.set(1, 4, 2, custom);
|
||||
|
||||
assertSame(base, transaction.get(1, 4, 2));
|
||||
assertEquals(identifier, transaction.getDataIfPresent(1, 4, 2, Identifier.class));
|
||||
transaction.commit();
|
||||
verify(writer).set(1, 4, 2, custom);
|
||||
verify(writer, never()).setData(1, 4, 2, base);
|
||||
verify(writer, never()).setData(1, 4, 2, identifier);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectedCustomPlacementStagesNeitherBlockNorIdentifier() {
|
||||
MantleWriter writer = writer();
|
||||
PlatformBlockState custom = mock(PlatformBlockState.class);
|
||||
PlatformBlockState base = mock(PlatformBlockState.class);
|
||||
when(custom.isCustom()).thenReturn(true);
|
||||
when(custom.deferredPlacementKey()).thenReturn("iris:custom_block");
|
||||
when(custom.placementBaseState()).thenReturn(base);
|
||||
when(writer.getEngine().getDimension().isBedrock()).thenReturn(true);
|
||||
when(writer.getPrerequisiteDataIfPresent(1, 4, 0, HydrologyCaveCell.class))
|
||||
.thenReturn(HydrologyCaveCell.of(HydrologyCaveAction.SEAL_GUARD));
|
||||
ObjectDestinationTransaction transaction = new ObjectDestinationTransaction(writer, 0, 0);
|
||||
|
||||
transaction.set(0, 0, 0, custom);
|
||||
transaction.set(1, 4, 0, custom);
|
||||
transaction.set(2, 64, 0, custom);
|
||||
|
||||
assertNull(transaction.getDataIfPresent(0, 0, 0, Identifier.class));
|
||||
assertNull(transaction.getDataIfPresent(1, 4, 0, Identifier.class));
|
||||
assertNull(transaction.getDataIfPresent(2, 64, 0, Identifier.class));
|
||||
transaction.commit();
|
||||
verify(writer, never()).set(anyInt(), anyInt(), anyInt(), any(PlatformBlockState.class));
|
||||
verify(writer, never()).setData(anyInt(), anyInt(), anyInt(), any(Identifier.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void blockReplacementClearsOverlayIdentifierAndRestoresItAfterFailure() {
|
||||
MantleWriter writer = writer();
|
||||
PlatformBlockState block = mock(PlatformBlockState.class);
|
||||
Marker failure = new Marker("failure");
|
||||
Identifier originalIdentifier = Identifier.fromString("iris:deferred");
|
||||
when(writer.getPrerequisiteDataIfPresent(0, 4, 0, Identifier.class)).thenReturn(originalIdentifier);
|
||||
doThrow(new IllegalStateException("publication failed"))
|
||||
.when(writer).setData(1, 4, 0, failure);
|
||||
ObjectDestinationTransaction transaction = new ObjectDestinationTransaction(writer, 0, 0);
|
||||
|
||||
transaction.setData(0, 4, 0, block);
|
||||
assertNull(transaction.getDataIfPresent(0, 4, 0, Identifier.class));
|
||||
transaction.setData(1, 4, 0, failure);
|
||||
|
||||
assertThrows(IllegalStateException.class, transaction::commit);
|
||||
verify(writer).clearData(0, 4, 0, Identifier.class);
|
||||
verify(writer).setData(0, 4, 0, originalIdentifier);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static MantleWriter writer() {
|
||||
MantleWriter writer = mock(MantleWriter.class);
|
||||
Mantle<Matter> mantle = mock(Mantle.class);
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisDimension dimension = mock(IrisDimension.class);
|
||||
when(mantle.getWorldHeight()).thenReturn(64);
|
||||
when(writer.getMantle()).thenReturn(mantle);
|
||||
when(writer.getEngine()).thenReturn(engine);
|
||||
when(engine.getDimension()).thenReturn(dimension);
|
||||
when(writer.getPrerequisiteCarvedColumn(anyInt(), anyInt(), anyInt()))
|
||||
.thenAnswer(invocation -> new byte[invocation.getArgument(2)]);
|
||||
when(writer.getPrerequisiteDataIfPresent(anyInt(), anyInt(), anyInt(), any()))
|
||||
.thenReturn(null);
|
||||
doAnswer(invocation -> {
|
||||
Runnable task = invocation.getArgument(2);
|
||||
task.run();
|
||||
return null;
|
||||
}).when(writer).withChunkFence(anyInt(), anyInt(), any(Runnable.class));
|
||||
return writer;
|
||||
}
|
||||
|
||||
private record Marker(String value) {
|
||||
}
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
package art.arcane.iris.engine.mantle.components;
|
||||
|
||||
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.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class ObjectSourcePlanCacheTest {
|
||||
@Test
|
||||
public void concurrentRequestsBuildOneSourcePlan() throws Exception {
|
||||
ObjectSourcePlanCache cache = new ObjectSourcePlanCache(64L);
|
||||
ObjectSourcePlan expected = new ObjectSourcePlan(List.of());
|
||||
AtomicInteger builds = new AtomicInteger();
|
||||
CountDownLatch ready = new CountDownLatch(8);
|
||||
CountDownLatch start = new CountDownLatch(1);
|
||||
ExecutorService executor = Executors.newFixedThreadPool(8);
|
||||
List<Future<ObjectSourcePlan>> futures = new ArrayList<>();
|
||||
try {
|
||||
for (int index = 0; index < 8; index++) {
|
||||
futures.add(executor.submit(() -> {
|
||||
ready.countDown();
|
||||
assertTrue(start.await(5L, TimeUnit.SECONDS));
|
||||
return cache.get(4, -7, () -> {
|
||||
builds.incrementAndGet();
|
||||
return expected;
|
||||
});
|
||||
}));
|
||||
}
|
||||
assertTrue(ready.await(5L, TimeUnit.SECONDS));
|
||||
start.countDown();
|
||||
for (Future<ObjectSourcePlan> future : futures) {
|
||||
assertSame(expected, future.get(5L, TimeUnit.SECONDS));
|
||||
}
|
||||
} finally {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
|
||||
assertEquals(1, builds.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cacheEvictsByMutationWeightAndCanBeCleared() {
|
||||
ObjectSourcePlanCache cache = new ObjectSourcePlanCache(3L);
|
||||
ObjectSourcePlan first = planAt(0);
|
||||
ObjectSourcePlan second = planAt(1);
|
||||
|
||||
cache.get(0, 0, () -> first);
|
||||
cache.get(1, 0, () -> second);
|
||||
|
||||
assertEquals(1L, cache.estimatedSize());
|
||||
cache.clear();
|
||||
assertEquals(0L, cache.estimatedSize());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cacheRejectsNonPositiveMutationCapacity() {
|
||||
assertThrows(IllegalArgumentException.class, () -> new ObjectSourcePlanCache(0L));
|
||||
}
|
||||
|
||||
private static ObjectSourcePlan planAt(int x) {
|
||||
ObjectDestinationTransaction.DataKey key = new ObjectDestinationTransaction.DataKey(
|
||||
x,
|
||||
4,
|
||||
0,
|
||||
String.class
|
||||
);
|
||||
ObjectDestinationTransaction.Mutation mutation = new ObjectDestinationTransaction.SetMutation(
|
||||
key,
|
||||
"marker"
|
||||
);
|
||||
return new ObjectSourcePlan(List.of(mutation));
|
||||
}
|
||||
}
|
||||
+24
-28
@@ -5,8 +5,8 @@ import art.arcane.iris.core.loader.ResourceLoader;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisDimensionCarvingResolver;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveAction;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
|
||||
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.volmlib.util.mantle.runtime.MantleChunk;
|
||||
@@ -110,40 +110,36 @@ public class IrisCarveModifierBoundarySupportTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void riverGuardOnlyAcceptsStableSolidBoundaryLayers() {
|
||||
RiverCaveHydrology guard = RiverCaveHydrology.of(RiverCaveAction.SEAL_GUARD);
|
||||
public void hydrologyGuardOnlyAcceptsStableSolidBoundaryLayers() {
|
||||
HydrologyCaveCell guard = HydrologyCaveCell.of(HydrologyCaveAction.SEAL_GUARD);
|
||||
PlatformBlockState stone = state("minecraft:stone", true);
|
||||
PlatformBlockState sand = state("minecraft:sand", true);
|
||||
PlatformBlockState water = state("minecraft:water", false, true);
|
||||
|
||||
assertTrue(IrisCarveModifier.canReplaceRiverGuard(guard, stone, false));
|
||||
assertTrue(IrisCarveModifier.canReplaceRiverGuard(guard, stone, true));
|
||||
assertTrue(IrisCarveModifier.canReplaceRiverGuard(guard, sand, false));
|
||||
assertFalse(IrisCarveModifier.canReplaceRiverGuard(guard, sand, true));
|
||||
assertFalse(IrisCarveModifier.canReplaceRiverGuard(guard, water, false));
|
||||
assertTrue(IrisCarveModifier.canReplaceHydrologyGuard(guard, stone, false));
|
||||
assertTrue(IrisCarveModifier.canReplaceHydrologyGuard(guard, stone, true));
|
||||
assertTrue(IrisCarveModifier.canReplaceHydrologyGuard(guard, sand, false));
|
||||
assertFalse(IrisCarveModifier.canReplaceHydrologyGuard(guard, sand, true));
|
||||
assertFalse(IrisCarveModifier.canReplaceHydrologyGuard(guard, water, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void riverBiomeInheritanceIsColumnCoherentAndHonorsLimits() {
|
||||
long seed = 7845123L;
|
||||
boolean cell = IrisCarveModifier.selectsParentRiverBiome(seed, 8, 12, 0.5D);
|
||||
for (int x = 8; x < 12; x++) {
|
||||
for (int z = 12; z < 16; z++) {
|
||||
assertTrue(cell == IrisCarveModifier.selectsParentRiverBiome(seed, x, z, 0.5D));
|
||||
}
|
||||
}
|
||||
assertFalse(IrisCarveModifier.selectsParentRiverBiome(seed, 8, 12, 0D));
|
||||
assertTrue(IrisCarveModifier.selectsParentRiverBiome(seed, 8, 12, 1D));
|
||||
@SuppressWarnings("unchecked")
|
||||
public void submergedCaveFloorUsesBuriedSubstrateInsteadOfVegetatedSurface() {
|
||||
Hunk<PlatformBlockState> output = mock(Hunk.class);
|
||||
PlatformBlockState grass = state("minecraft:grass_block", true);
|
||||
PlatformBlockState dirt = state("minecraft:dirt", true);
|
||||
PlatformBlockState moss = state("minecraft:moss_block", true);
|
||||
HydrologyCaveCell wet = HydrologyCaveCell.of(HydrologyCaveAction.WET_SOURCE);
|
||||
|
||||
boolean inherited = false;
|
||||
boolean overridden = false;
|
||||
for (int x = -128; x <= 128; x += 4) {
|
||||
boolean selected = IrisCarveModifier.selectsParentRiverBiome(seed, x, 0, 0.5D);
|
||||
inherited |= selected;
|
||||
overridden |= !selected;
|
||||
}
|
||||
assertTrue(inherited);
|
||||
assertTrue(overridden);
|
||||
doReturn(dirt).when(output).getRaw(0, 4, 0);
|
||||
|
||||
assertSame(dirt, IrisCarveModifier.resolveSubmergedCaveFloorLayer(
|
||||
output, 0, 5, 0, grass, wet));
|
||||
assertSame(dirt, IrisCarveModifier.resolveSubmergedCaveFloorLayer(
|
||||
output, 0, 5, 0, moss, wet));
|
||||
assertSame(grass, IrisCarveModifier.resolveSubmergedCaveFloorLayer(
|
||||
output, 0, 5, 0, grass, HydrologyCaveCell.of(HydrologyCaveAction.DRY_AIR)));
|
||||
}
|
||||
|
||||
private PlatformBlockState state(String key, boolean solid) {
|
||||
|
||||
+28
-16
@@ -1,37 +1,49 @@
|
||||
package art.arcane.iris.engine.modifier;
|
||||
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveAction;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveFluidKind;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
|
||||
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.volmlib.util.matter.MatterCavern;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
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 IrisCarveModifierRiverHydrologyTest {
|
||||
public class IrisCarveModifierHydrologyTest {
|
||||
@Test
|
||||
public void explicitHydrologyCanMaterializeIntoAnUninitializedCell() {
|
||||
PlatformBlockState air = mock(PlatformBlockState.class);
|
||||
when(air.isAir()).thenReturn(true);
|
||||
|
||||
assertFalse(IrisCarveModifier.shouldSkipEmptyCarve(null, true));
|
||||
assertFalse(IrisCarveModifier.shouldSkipEmptyCarve(air, true));
|
||||
assertTrue(IrisCarveModifier.shouldSkipEmptyCarve(null, false));
|
||||
assertTrue(IrisCarveModifier.shouldSkipEmptyCarve(air, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void overlayCompositionDoesNotMutateBaselineCavern() {
|
||||
MatterCavern baseline = new MatterCavern(true, "baseline", (byte) 2);
|
||||
|
||||
assertSame(baseline, IrisCarveModifier.composeCavern(baseline, null));
|
||||
assertNull(IrisCarveModifier.composeCavern(
|
||||
baseline, RiverCaveHydrology.of(RiverCaveAction.SEAL_GUARD)));
|
||||
baseline, HydrologyCaveCell.of(HydrologyCaveAction.SEAL_GUARD)));
|
||||
|
||||
MatterCavern wet = IrisCarveModifier.composeCavern(
|
||||
baseline, new RiverCaveHydrology(
|
||||
RiverCaveAction.WET_SOURCE,
|
||||
"iris:flooded",
|
||||
RiverCaveFluidKind.DEEP_POOL
|
||||
baseline, new HydrologyCaveCell(
|
||||
HydrologyCaveAction.WET_SOURCE,
|
||||
"deep_lava",
|
||||
"iris:flooded"
|
||||
));
|
||||
MatterCavern dry = IrisCarveModifier.composeCavern(
|
||||
baseline, RiverCaveHydrology.of(RiverCaveAction.DRY_AIR));
|
||||
baseline, HydrologyCaveCell.of(HydrologyCaveAction.DRY_AIR));
|
||||
|
||||
assertEquals(1, wet.getLiquid());
|
||||
assertEquals("iris:flooded", wet.getCustomBiome());
|
||||
@@ -50,11 +62,11 @@ public class IrisCarveModifierRiverHydrologyTest {
|
||||
when(source.withProperty("level", "8")).thenReturn(falling);
|
||||
|
||||
assertSame(source, IrisCarveModifier.resolveHydrologyState(
|
||||
RiverCaveHydrology.of(RiverCaveAction.WET_SOURCE), current, source, air));
|
||||
HydrologyCaveCell.of(HydrologyCaveAction.WET_SOURCE), current, source, air));
|
||||
assertSame(falling, IrisCarveModifier.resolveHydrologyState(
|
||||
RiverCaveHydrology.of(RiverCaveAction.FALLING_FLUID), current, source, air));
|
||||
HydrologyCaveCell.of(HydrologyCaveAction.FALLING_FLUID), current, source, air));
|
||||
assertSame(air, IrisCarveModifier.resolveHydrologyState(
|
||||
RiverCaveHydrology.of(RiverCaveAction.DRY_AIR), current, source, air));
|
||||
HydrologyCaveCell.of(HydrologyCaveAction.DRY_AIR), current, source, air));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -71,7 +83,7 @@ public class IrisCarveModifierRiverHydrologyTest {
|
||||
assertSame(dry, IrisCarveModifier.normalizeWaterlogging(waterlogged, null));
|
||||
assertSame(dry, IrisCarveModifier.normalizeWaterlogging(waterlogged, lava));
|
||||
assertSame(dry, IrisCarveModifier.resolveHydrologyState(
|
||||
RiverCaveHydrology.of(RiverCaveAction.SEAL_GUARD), waterlogged, water, null));
|
||||
HydrologyCaveCell.of(HydrologyCaveAction.SEAL_GUARD), waterlogged, water, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -102,13 +114,13 @@ public class IrisCarveModifierRiverHydrologyTest {
|
||||
assertSame(waterlogged, IrisCarveModifier.normalizeHydrologyWaterlogging(
|
||||
waterlogged,
|
||||
flooded,
|
||||
RiverCaveHydrology.of(RiverCaveAction.WET_SOURCE),
|
||||
HydrologyCaveCell.of(HydrologyCaveAction.WET_SOURCE),
|
||||
water
|
||||
));
|
||||
assertSame(dry, IrisCarveModifier.normalizeHydrologyWaterlogging(
|
||||
waterlogged,
|
||||
flooded,
|
||||
RiverCaveHydrology.of(RiverCaveAction.DRY_AIR),
|
||||
HydrologyCaveCell.of(HydrologyCaveAction.DRY_AIR),
|
||||
water
|
||||
));
|
||||
}
|
||||
+1
-1
@@ -70,7 +70,7 @@ public class IrisCarveModifierInferenceIsolationTest {
|
||||
Map<String, IrisBiome> customBiomes = new HashMap<>();
|
||||
customBiomes.put("shared", biome);
|
||||
CarveWallBuffer walls = new CarveWallBuffer(1);
|
||||
walls.put(0, 1, 0, new MatterCavern(true, "shared", (byte) 0), false);
|
||||
walls.put(0, 1, 0, new MatterCavern(true, "shared", (byte) 0));
|
||||
Method paintBoundaryZone = IrisCarveModifier.class.getDeclaredMethod(
|
||||
"paintBoundaryZone",
|
||||
Hunk.class,
|
||||
|
||||
@@ -22,21 +22,19 @@ public class IrisCarveScratchTest {
|
||||
int y = 20 + index;
|
||||
int z = (index * 3) & 15;
|
||||
MatterCavern cavern = new MatterCavern(true, "cave-" + index, (byte) 0);
|
||||
buffer.put(x, y, z, cavern, (index & 1) == 0);
|
||||
buffer.put(x, y, z, cavern);
|
||||
expected.put(key(x, y, z), cavern);
|
||||
}
|
||||
|
||||
MatterCavern replacement = new MatterCavern(true, "replacement", (byte) 0);
|
||||
buffer.put(5, 25, 15, replacement, true);
|
||||
buffer.put(5, 25, 15, replacement);
|
||||
expected.put(key(5, 25, 15), replacement);
|
||||
|
||||
assertSame(replacement, buffer.get(5, 25, 15));
|
||||
assertTrue(buffer.isRiverBoundary(5, 25, 15));
|
||||
assertNull(buffer.get(5, 26, 15));
|
||||
assertFalse(buffer.isRiverBoundary(5, 26, 15));
|
||||
|
||||
Map<String, MatterCavern> actual = new HashMap<>();
|
||||
buffer.forEach((x, y, z, cavern, riverBoundary) -> actual.put(key(x, y, z), cavern));
|
||||
buffer.forEach((x, y, z, cavern) -> actual.put(key(x, y, z), cavern));
|
||||
assertEquals(expected.keySet(), actual.keySet());
|
||||
for (Map.Entry<String, MatterCavern> entry : expected.entrySet()) {
|
||||
assertSame(entry.getValue(), actual.get(entry.getKey()));
|
||||
@@ -49,7 +47,7 @@ public class IrisCarveScratchTest {
|
||||
MatterCavern cavern = new MatterCavern(true, "cave", (byte) 0);
|
||||
scratch.columnMasks[0].add(12);
|
||||
scratch.boundaryMasks[0].add(13);
|
||||
scratch.walls.put(1, 12, 2, cavern, true);
|
||||
scratch.walls.put(1, 12, 2, cavern);
|
||||
scratch.customBiomeCache.put("cave", null);
|
||||
scratch.customCaveBiomePresent = true;
|
||||
|
||||
@@ -60,7 +58,7 @@ public class IrisCarveScratchTest {
|
||||
assertTrue(scratch.customBiomeCache.isEmpty());
|
||||
assertFalse(scratch.customCaveBiomePresent);
|
||||
int[] wallCount = new int[1];
|
||||
scratch.walls.forEach((x, y, z, value, riverBoundary) -> wallCount[0]++);
|
||||
scratch.walls.forEach((x, y, z, value) -> wallCount[0]++);
|
||||
assertEquals(0, wallCount[0]);
|
||||
}
|
||||
|
||||
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.util.project.noise.CNG;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Map;
|
||||
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 static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.same;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public final class IrisBiomePaletteLayerGeneratorCacheTest {
|
||||
private static final int SIGNATURE = 7;
|
||||
private static final long LAYER_SIGNATURE = 2L;
|
||||
|
||||
@Test
|
||||
public void callerSeedsDoNotShareAFirstWinnerGenerator() {
|
||||
GeneratorFixture fixture = new GeneratorFixture();
|
||||
|
||||
assertSame(fixture.carveGenerator,
|
||||
fixture.layer.getLayerGenerator(new RNG(11L), SIGNATURE, fixture.data));
|
||||
assertSame(fixture.postGenerator,
|
||||
fixture.layer.getLayerGenerator(new RNG(29L), SIGNATURE, fixture.data));
|
||||
assertSame(fixture.carveGenerator,
|
||||
fixture.layer.getLayerGenerator(new RNG(11L), SIGNATURE, fixture.data));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reverseInitializationOrderKeepsSeedAssignments() {
|
||||
GeneratorFixture fixture = new GeneratorFixture();
|
||||
|
||||
assertSame(fixture.postGenerator,
|
||||
fixture.layer.getLayerGenerator(new RNG(29L), SIGNATURE, fixture.data));
|
||||
assertSame(fixture.carveGenerator,
|
||||
fixture.layer.getLayerGenerator(new RNG(11L), SIGNATURE, fixture.data));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void concurrentInitializationKeepsSeedAssignments() throws Exception {
|
||||
GeneratorFixture fixture = new GeneratorFixture();
|
||||
CountDownLatch start = new CountDownLatch(1);
|
||||
ExecutorService executor = Executors.newFixedThreadPool(2);
|
||||
try {
|
||||
Future<CNG> carve = executor.submit(() -> {
|
||||
assertTrue(start.await(5L, TimeUnit.SECONDS));
|
||||
return fixture.layer.getLayerGenerator(new RNG(11L), SIGNATURE, fixture.data);
|
||||
});
|
||||
Future<CNG> post = executor.submit(() -> {
|
||||
assertTrue(start.await(5L, TimeUnit.SECONDS));
|
||||
return fixture.layer.getLayerGenerator(new RNG(29L), SIGNATURE, fixture.data);
|
||||
});
|
||||
start.countDown();
|
||||
|
||||
assertSame(fixture.carveGenerator, carve.get(5L, TimeUnit.SECONDS));
|
||||
assertSame(fixture.postGenerator, post.get(5L, TimeUnit.SECONDS));
|
||||
} finally {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class GeneratorFixture {
|
||||
private final IrisData data = mock(IrisData.class);
|
||||
private final Engine engine = mock(Engine.class);
|
||||
private final IrisGeneratorStyle style = mock(IrisGeneratorStyle.class);
|
||||
private final CNG carveGenerator = mock(CNG.class);
|
||||
private final CNG postGenerator = mock(CNG.class);
|
||||
private final IrisBiomePaletteLayer layer = new IrisBiomePaletteLayer().zero().setStyle(style);
|
||||
|
||||
private GeneratorFixture() {
|
||||
when(data.getEngine()).thenReturn(engine);
|
||||
Map<Long, CNG> generators = Map.of(
|
||||
11L + SIGNATURE + LAYER_SIGNATURE, carveGenerator,
|
||||
29L + SIGNATURE + LAYER_SIGNATURE, postGenerator
|
||||
);
|
||||
when(style.create(any(RNG.class), same(data), same(engine))).thenAnswer(invocation -> {
|
||||
RNG rng = invocation.getArgument(0);
|
||||
return generators.get(rng.getSeed());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,6 +69,28 @@ public class IrisDepositTuningTest {
|
||||
assertEquals(firstWorld, new IrisDepositGenerator.ClumpCacheKey(41L, 4, 8));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void clumpSaltIsStableAcrossEquivalentConfigInstances() {
|
||||
IrisData data = mock(IrisData.class);
|
||||
IrisDepositGenerator first = generatorWithState(data, false, "minecraft:granite");
|
||||
IrisDepositGenerator second = generatorWithState(data, false, "minecraft:granite");
|
||||
|
||||
assertEquals(first.stableClumpSalt(data), second.stableClumpSalt(data));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void clumpSaltIncludesAuthoredConfigAndPalette() {
|
||||
IrisData data = mock(IrisData.class);
|
||||
IrisDepositGenerator granite = generatorWithState(data, false, "minecraft:granite");
|
||||
IrisDepositGenerator andesite = generatorWithState(data, false, "minecraft:andesite");
|
||||
IrisDepositGenerator alteredShape = generatorWithState(data, false, "minecraft:granite");
|
||||
alteredShape.setShape(IrisDepositShape.VANILLA_SCATTERED);
|
||||
|
||||
assertEquals(3112546198474861350L, granite.stableClumpSalt(data));
|
||||
assertNotEquals(granite.stableClumpSalt(data), andesite.stableClumpSalt(data));
|
||||
assertNotEquals(granite.stableClumpSalt(data), alteredShape.stableClumpSalt(data));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onlyOreDepositPalettesReceiveBiomeTuning() {
|
||||
IrisData data = mock(IrisData.class);
|
||||
@@ -143,10 +165,15 @@ public class IrisDepositTuningTest {
|
||||
}
|
||||
|
||||
private IrisDepositGenerator generatorWithState(IrisData data, boolean ore) {
|
||||
return generatorWithState(data, ore, ore ? "minecraft:iron_ore" : "minecraft:stone");
|
||||
}
|
||||
|
||||
private IrisDepositGenerator generatorWithState(IrisData data, boolean ore, String key) {
|
||||
IrisBlockData block = mock(IrisBlockData.class);
|
||||
PlatformBlockState state = mock(PlatformBlockState.class);
|
||||
when(block.getBlockData(data)).thenReturn(state);
|
||||
when(state.isOre()).thenReturn(ore);
|
||||
when(state.key()).thenReturn(key);
|
||||
IrisDepositGenerator generator = new IrisDepositGenerator();
|
||||
generator.getPalette().add(block);
|
||||
return generator;
|
||||
|
||||
+114
@@ -130,6 +130,120 @@ public class IrisDimensionReachableBiomesTest {
|
||||
verify(biomeLoader, times(1)).load("d");
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void includesEveryBiomeReferencedByEnabledPolicyHierarchy() {
|
||||
IrisDimension dimension = new IrisDimension()
|
||||
.setRegions(new KList<>("reachable"))
|
||||
.setRiverPolicy(new IrisRiverPolicy()
|
||||
.setSurfaceBiomes(new KList<>("dimension-channel")));
|
||||
dimension.getHydrology().getRivers().setEnabled(true);
|
||||
IrisRegion reachable = new IrisRegion()
|
||||
.setLandBiomes(new KList<>("root"))
|
||||
.setRiverPolicy(new IrisRiverPolicy()
|
||||
.setMouthBiomes(new KList<>("region-mouth")));
|
||||
IrisBiome root = biome("root").setRiverPolicy(new IrisRiverPolicy()
|
||||
.setShoreBiomes(new KList<>("biome-shore")));
|
||||
IrisBiome dimensionChannel = biome("dimension-channel")
|
||||
.setChildren(new KList<>("dimension-child"));
|
||||
IrisBiome dimensionChild = biome("dimension-child");
|
||||
IrisBiome regionMouth = biome("region-mouth").setRiverPolicy(new IrisRiverPolicy()
|
||||
.setDryBiomes(new KList<>("nested-dry")));
|
||||
IrisBiome biomeShore = biome("biome-shore");
|
||||
IrisBiome nestedDry = biome("nested-dry");
|
||||
|
||||
IrisData data = mock(IrisData.class);
|
||||
ResourceLoader<IrisRegion> regionLoader = mock(ResourceLoader.class);
|
||||
ResourceLoader<IrisBiome> biomeLoader = mock(ResourceLoader.class);
|
||||
when(data.getRegionLoader()).thenReturn(regionLoader);
|
||||
when(data.getBiomeLoader()).thenReturn(biomeLoader);
|
||||
when(regionLoader.load("reachable")).thenReturn(reachable);
|
||||
when(biomeLoader.load("root")).thenReturn(root);
|
||||
when(biomeLoader.load("dimension-channel")).thenReturn(dimensionChannel);
|
||||
when(biomeLoader.load("dimension-child")).thenReturn(dimensionChild);
|
||||
when(biomeLoader.load("region-mouth")).thenReturn(regionMouth);
|
||||
when(biomeLoader.load("biome-shore")).thenReturn(biomeShore);
|
||||
when(biomeLoader.load("nested-dry")).thenReturn(nestedDry);
|
||||
|
||||
Set<String> keys = dimension.getReachableBiomes(() -> data).stream()
|
||||
.map(IrisBiome::getLoadKey)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
assertEquals(Set.of(
|
||||
"root",
|
||||
"dimension-channel",
|
||||
"dimension-child",
|
||||
"region-mouth",
|
||||
"biome-shore",
|
||||
"nested-dry"
|
||||
), keys);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void excludesPolicyOnlyBiomesWhenRiverHydrologyIsDisabled() {
|
||||
IrisDimension dimension = new IrisDimension()
|
||||
.setRegions(new KList<>("reachable"))
|
||||
.setRiverPolicy(new IrisRiverPolicy()
|
||||
.setSurfaceBiomes(new KList<>("dimension-channel")));
|
||||
IrisRegion reachable = new IrisRegion()
|
||||
.setLandBiomes(new KList<>("root"))
|
||||
.setRiverPolicy(new IrisRiverPolicy()
|
||||
.setMouthBiomes(new KList<>("region-mouth")));
|
||||
IrisBiome root = biome("root").setRiverPolicy(new IrisRiverPolicy()
|
||||
.setShoreBiomes(new KList<>("biome-shore")));
|
||||
|
||||
IrisData data = mock(IrisData.class);
|
||||
ResourceLoader<IrisRegion> regionLoader = mock(ResourceLoader.class);
|
||||
ResourceLoader<IrisBiome> biomeLoader = mock(ResourceLoader.class);
|
||||
when(data.getRegionLoader()).thenReturn(regionLoader);
|
||||
when(data.getBiomeLoader()).thenReturn(biomeLoader);
|
||||
when(regionLoader.load("reachable")).thenReturn(reachable);
|
||||
when(biomeLoader.load("root")).thenReturn(root);
|
||||
|
||||
Set<String> keys = dimension.getReachableBiomes(() -> data).stream()
|
||||
.map(IrisBiome::getLoadKey)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
assertEquals(Set.of("root"), keys);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void includesPolicyBiomesForActiveDeepOnlyHydrology() {
|
||||
IrisDimension dimension = new IrisDimension()
|
||||
.setRegions(new KList<>("reachable"))
|
||||
.setRiverPolicy(new IrisRiverPolicy()
|
||||
.setFloodedCaveBiomes(new KList<>("dimension-flooded")));
|
||||
dimension.getHydrology().getRivers().setEnabled(false);
|
||||
dimension.getHydrology().getDeepFluids().add(new IrisDeepFluidConfig()
|
||||
.setDensity(1D)
|
||||
.setContainedPools(true));
|
||||
IrisRegion reachable = new IrisRegion()
|
||||
.setLandBiomes(new KList<>("root"))
|
||||
.setRiverPolicy(new IrisRiverPolicy()
|
||||
.setDryBiomes(new KList<>("region-dry")));
|
||||
IrisBiome root = biome("root").setRiverPolicy(new IrisRiverPolicy()
|
||||
.setFloodedCaveBiomes(new KList<>("biome-flooded")));
|
||||
|
||||
IrisData data = mock(IrisData.class);
|
||||
ResourceLoader<IrisRegion> regionLoader = mock(ResourceLoader.class);
|
||||
ResourceLoader<IrisBiome> biomeLoader = mock(ResourceLoader.class);
|
||||
when(data.getRegionLoader()).thenReturn(regionLoader);
|
||||
when(data.getBiomeLoader()).thenReturn(biomeLoader);
|
||||
when(regionLoader.load("reachable")).thenReturn(reachable);
|
||||
when(biomeLoader.load("root")).thenReturn(root);
|
||||
when(biomeLoader.load("dimension-flooded")).thenReturn(biome("dimension-flooded"));
|
||||
when(biomeLoader.load("region-dry")).thenReturn(biome("region-dry"));
|
||||
when(biomeLoader.load("biome-flooded")).thenReturn(biome("biome-flooded"));
|
||||
|
||||
Set<String> keys = dimension.getReachableBiomes(() -> data).stream()
|
||||
.map(IrisBiome::getLoadKey)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
assertEquals(Set.of("root", "dimension-flooded", "region-dry", "biome-flooded"), keys);
|
||||
}
|
||||
|
||||
private IrisBiome biome(String loadKey) {
|
||||
IrisBiome biome = new IrisBiome();
|
||||
biome.setLoadKey(loadKey);
|
||||
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.util.project.noise.CNG;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Map;
|
||||
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 static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.same;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public final class IrisMaterialPaletteGeneratorCacheTest {
|
||||
private static final long GENERATOR_SALT = -23_498_896L;
|
||||
|
||||
@Test
|
||||
public void callerSeedsDoNotShareAFirstWinnerGenerator() {
|
||||
GeneratorFixture fixture = new GeneratorFixture();
|
||||
|
||||
assertSame(fixture.firstGenerator, fixture.palette.getLayerGenerator(new RNG(11L), fixture.data));
|
||||
assertSame(fixture.secondGenerator, fixture.palette.getLayerGenerator(new RNG(29L), fixture.data));
|
||||
assertSame(fixture.firstGenerator, fixture.palette.getLayerGenerator(new RNG(11L), fixture.data));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reverseInitializationOrderKeepsSeedAssignments() {
|
||||
GeneratorFixture fixture = new GeneratorFixture();
|
||||
|
||||
assertSame(fixture.secondGenerator, fixture.palette.getLayerGenerator(new RNG(29L), fixture.data));
|
||||
assertSame(fixture.firstGenerator, fixture.palette.getLayerGenerator(new RNG(11L), fixture.data));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void concurrentInitializationKeepsSeedAssignments() throws Exception {
|
||||
GeneratorFixture fixture = new GeneratorFixture();
|
||||
CountDownLatch start = new CountDownLatch(1);
|
||||
ExecutorService executor = Executors.newFixedThreadPool(2);
|
||||
try {
|
||||
Future<CNG> first = executor.submit(() -> {
|
||||
assertTrue(start.await(5L, TimeUnit.SECONDS));
|
||||
return fixture.palette.getLayerGenerator(new RNG(11L), fixture.data);
|
||||
});
|
||||
Future<CNG> second = executor.submit(() -> {
|
||||
assertTrue(start.await(5L, TimeUnit.SECONDS));
|
||||
return fixture.palette.getLayerGenerator(new RNG(29L), fixture.data);
|
||||
});
|
||||
start.countDown();
|
||||
|
||||
assertSame(fixture.firstGenerator, first.get(5L, TimeUnit.SECONDS));
|
||||
assertSame(fixture.secondGenerator, second.get(5L, TimeUnit.SECONDS));
|
||||
} finally {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class GeneratorFixture {
|
||||
private final IrisData data = mock(IrisData.class);
|
||||
private final Engine engine = mock(Engine.class);
|
||||
private final IrisGeneratorStyle style = mock(IrisGeneratorStyle.class);
|
||||
private final CNG firstGenerator = mock(CNG.class);
|
||||
private final CNG secondGenerator = mock(CNG.class);
|
||||
private final IrisMaterialPalette palette = new IrisMaterialPalette().qclear().setStyle(style);
|
||||
|
||||
private GeneratorFixture() {
|
||||
when(data.getEngine()).thenReturn(engine);
|
||||
Map<Long, CNG> generators = Map.of(
|
||||
11L + GENERATOR_SALT, firstGenerator,
|
||||
29L + GENERATOR_SALT, secondGenerator
|
||||
);
|
||||
when(style.create(any(RNG.class), same(data), same(engine))).thenAnswer(invocation -> {
|
||||
RNG rng = invocation.getArgument(0);
|
||||
return generators.get(rng.getSeed());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -43,7 +43,7 @@ public class IrisMathNoiseHotPathParityTest {
|
||||
.setOpacity(0.91D)
|
||||
.setComposite(new KList<IrisNoiseGenerator>().qadd(noiseGenerator));
|
||||
|
||||
assertEquals(0.451949817597527D, generator.getHeight(63.0D, -27.0D, 445566L), 0D);
|
||||
assertEquals(0.5671980255964687D, generator.getHeight(63.0D, -27.0D, 445566L), 0D);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.util.project.noise.CNG;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Map;
|
||||
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 static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.same;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public final class IrisNoiseGeneratorCacheTest {
|
||||
private static final long GENERATOR_SALT = 33_955_677L;
|
||||
private static final long CONFIGURED_SEED = 7L;
|
||||
private static final int OCTAVES = 3;
|
||||
|
||||
@Test
|
||||
public void callerSeedsDoNotShareAFirstWinnerGenerator() {
|
||||
GeneratorFixture fixture = new GeneratorFixture();
|
||||
|
||||
assertSame(fixture.firstGenerator, fixture.generator.getGenerator(11L, fixture.data));
|
||||
assertSame(fixture.secondGenerator, fixture.generator.getGenerator(29L, fixture.data));
|
||||
assertSame(fixture.firstGenerator, fixture.generator.getGenerator(11L, fixture.data));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reverseInitializationOrderKeepsSeedAssignments() {
|
||||
GeneratorFixture fixture = new GeneratorFixture();
|
||||
|
||||
assertSame(fixture.secondGenerator, fixture.generator.getGenerator(29L, fixture.data));
|
||||
assertSame(fixture.firstGenerator, fixture.generator.getGenerator(11L, fixture.data));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void concurrentInitializationKeepsSeedAssignments() throws Exception {
|
||||
GeneratorFixture fixture = new GeneratorFixture();
|
||||
CountDownLatch start = new CountDownLatch(1);
|
||||
ExecutorService executor = Executors.newFixedThreadPool(2);
|
||||
try {
|
||||
Future<CNG> first = executor.submit(() -> {
|
||||
assertTrue(start.await(5L, TimeUnit.SECONDS));
|
||||
return fixture.generator.getGenerator(11L, fixture.data);
|
||||
});
|
||||
Future<CNG> second = executor.submit(() -> {
|
||||
assertTrue(start.await(5L, TimeUnit.SECONDS));
|
||||
return fixture.generator.getGenerator(29L, fixture.data);
|
||||
});
|
||||
start.countDown();
|
||||
|
||||
assertSame(fixture.firstGenerator, first.get(5L, TimeUnit.SECONDS));
|
||||
assertSame(fixture.secondGenerator, second.get(5L, TimeUnit.SECONDS));
|
||||
} finally {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class GeneratorFixture {
|
||||
private final IrisData data = mock(IrisData.class);
|
||||
private final Engine engine = mock(Engine.class);
|
||||
private final IrisGeneratorStyle style = mock(IrisGeneratorStyle.class);
|
||||
private final CNG firstGenerator = mock(CNG.class);
|
||||
private final CNG secondGenerator = mock(CNG.class);
|
||||
private final IrisNoiseGenerator generator = new IrisNoiseGenerator()
|
||||
.setStyle(style)
|
||||
.setSeed(CONFIGURED_SEED)
|
||||
.setOctaves(OCTAVES);
|
||||
|
||||
private GeneratorFixture() {
|
||||
when(data.getEngine()).thenReturn(engine);
|
||||
when(firstGenerator.oct(OCTAVES)).thenReturn(firstGenerator);
|
||||
when(secondGenerator.oct(OCTAVES)).thenReturn(secondGenerator);
|
||||
Map<Long, CNG> generators = Map.of(
|
||||
11L + GENERATOR_SALT - CONFIGURED_SEED, firstGenerator,
|
||||
29L + GENERATOR_SALT - CONFIGURED_SEED, secondGenerator
|
||||
);
|
||||
when(style.createNoCache(any(RNG.class), same(data))).thenAnswer(invocation -> {
|
||||
RNG rng = invocation.getArgument(0);
|
||||
return generators.get(rng.getSeed());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
+26
-7
@@ -25,6 +25,8 @@ import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
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 IrisObjectPlacementRunnerRegressionTest {
|
||||
@@ -94,13 +96,9 @@ public class IrisObjectPlacementRunnerRegressionTest {
|
||||
@Test
|
||||
public void collisionCheckUsesTheTransformedBounds() {
|
||||
PlacedObject forbidden = new PlacedObject(null, object("forbidden"), 1, 0, 0);
|
||||
when(engine.getObjectPlacement(anyInt(), anyInt(), anyInt())).thenAnswer(invocation -> {
|
||||
int x = invocation.getArgument(0);
|
||||
int y = invocation.getArgument(1);
|
||||
int z = invocation.getArgument(2);
|
||||
return x == 0 && y == ANCHOR_Y && z == -4 ? forbidden : null;
|
||||
});
|
||||
RecordingPlacer placer = new RecordingPlacer(engine);
|
||||
placer.setData(0, ANCHOR_Y, -4, "forbidden@1");
|
||||
when(engine.resolveObjectPlacementMarker(0, -4, "forbidden@1")).thenReturn(forbidden);
|
||||
IrisObjectPlacement placement = placement();
|
||||
placement.setRotation(IrisObjectRotation.of(0, 90, 0));
|
||||
placement.setTranslate(new IrisObjectTranslate().setX(4));
|
||||
@@ -110,6 +108,25 @@ public class IrisObjectPlacementRunnerRegressionTest {
|
||||
|
||||
assertEquals(-1, result);
|
||||
assertTrue(placer.writes().isEmpty());
|
||||
verify(engine).resolveObjectPlacementMarker(0, -4, "forbidden@1");
|
||||
verify(engine, never()).getObjectPlacement(anyInt(), anyInt(), anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void allowedCollisionOverridesTheForbiddenKey() {
|
||||
PlacedObject allowed = new PlacedObject(null, object("allowed"), 1, 0, 0);
|
||||
RecordingPlacer placer = new RecordingPlacer(engine);
|
||||
placer.setData(0, ANCHOR_Y, 0, "allowed@1");
|
||||
when(engine.resolveObjectPlacementMarker(0, 0, "allowed@1")).thenReturn(allowed);
|
||||
IrisObjectPlacement placement = placement();
|
||||
placement.getForbiddenCollisions().add("allowed");
|
||||
placement.getAllowedCollisions().add("allowed");
|
||||
|
||||
int result = lineObject(1).place(0, ANCHOR_Y, 0, placer, placement, new RNG(2L), data);
|
||||
|
||||
assertEquals(ANCHOR_Y, result);
|
||||
assertFalse(placer.writes().isEmpty());
|
||||
verify(engine).resolveObjectPlacementMarker(0, 0, "allowed@1");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -216,6 +233,7 @@ public class IrisObjectPlacementRunnerRegressionTest {
|
||||
private static final class RecordingPlacer implements IObjectPlacer {
|
||||
private final List<BlockWrite> writes = new ArrayList<>();
|
||||
private final Map<String, PlatformBlockState> world = new HashMap<>();
|
||||
private final Map<String, Object> data = new HashMap<>();
|
||||
private final List<String> sampledColumns = new ArrayList<>();
|
||||
private final Engine engine;
|
||||
private int failAfterWrites = Integer.MAX_VALUE;
|
||||
@@ -308,11 +326,12 @@ public class IrisObjectPlacementRunnerRegressionTest {
|
||||
|
||||
@Override
|
||||
public <T> void setData(int x, int y, int z, T data) {
|
||||
this.data.put(x + ":" + y + ":" + z + ":" + data.getClass().getName(), data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T getData(int x, int y, int z, Class<T> type) {
|
||||
return null;
|
||||
return type.cast(data.get(x + ":" + y + ":" + z + ":" + type.getName()));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+32
-1
@@ -34,6 +34,7 @@ public class IrisObjectSurfaceSupportPlacementTest {
|
||||
|
||||
private IrisData data;
|
||||
private Engine engine;
|
||||
private IrisComplex complex;
|
||||
|
||||
@Before
|
||||
public void bindPlatform() {
|
||||
@@ -48,11 +49,12 @@ public class IrisObjectSurfaceSupportPlacementTest {
|
||||
@SuppressWarnings("unchecked")
|
||||
ProceduralStream<Double> heightStream = mock(ProceduralStream.class);
|
||||
when(heightStream.get(anyDouble(), anyDouble())).thenReturn((double) SURFACE_Y);
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
complex = mock(IrisComplex.class);
|
||||
when(complex.getHeightStream()).thenReturn(heightStream);
|
||||
engine = mock(Engine.class);
|
||||
when(engine.getHeight()).thenReturn(256);
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
when(engine.getDimension()).thenReturn(new IrisDimension());
|
||||
data = mock(IrisData.class);
|
||||
when(data.getEngine()).thenReturn(engine);
|
||||
}
|
||||
@@ -102,6 +104,35 @@ public class IrisObjectSurfaceSupportPlacementTest {
|
||||
assertTrue(place(placer, placement(ObjectPlaceMode.CENTER_HEIGHT), -1) >= 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void automaticSurfaceObjectCannotOverlapRiverWater() {
|
||||
when(complex.hasHydrologySurfaceFluid(0, 0)).thenReturn(true);
|
||||
SurfacePlacer placer = new SurfacePlacer(engine);
|
||||
|
||||
assertEquals(-1, place(placer, placement(ObjectPlaceMode.CENTER_HEIGHT), -1));
|
||||
assertTrue(placer.written().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void automaticForcePlacementCannotOverlapRiverWater() {
|
||||
when(complex.hasHydrologySurfaceFluid(0, 0)).thenReturn(true);
|
||||
SurfacePlacer placer = new SurfacePlacer(engine);
|
||||
IrisObjectPlacement placement = placement(ObjectPlaceMode.CENTER_HEIGHT);
|
||||
placement.setForcePlace(true);
|
||||
|
||||
assertEquals(-1, place(placer, placement, -1));
|
||||
assertTrue(placer.written().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void explicitObjectPlacementRemainsAllowedInsideRiverWater() {
|
||||
when(complex.hasHydrologySurfaceFluid(0, 0)).thenReturn(true);
|
||||
SurfacePlacer placer = new SurfacePlacer(engine);
|
||||
|
||||
assertTrue(place(placer, placement(ObjectPlaceMode.CENTER_HEIGHT), SURFACE_Y) >= 0);
|
||||
assertFalse(placer.written().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void explicitSurfaceAnchorIsGuardedByTheBufferRing() {
|
||||
SurfacePlacer placer = new SurfacePlacer();
|
||||
|
||||
+243
-278
@@ -1,317 +1,282 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.loader.ResourceLoader;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import com.google.gson.Gson;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class IrisRiverConfigurationTest {
|
||||
@Test
|
||||
public void defaultsKeepRiverGenerationDisabledAndContained() {
|
||||
public void defaultsExposeCanonicalHydrologyHierarchy() {
|
||||
IrisDimension dimension = new IrisDimension();
|
||||
|
||||
assertNotNull(dimension.getRivers());
|
||||
assertFalse(dimension.getRivers().isEnabled());
|
||||
assertEquals(IrisRiverWaterMode.FIXED, dimension.getRivers().getWater().getMode());
|
||||
assertEquals(63, dimension.getRivers().getWater().getFluidHeight());
|
||||
assertEquals("water", dimension.getRivers().getWater().getFluidPalette().getPalette().getFirst().getBlock());
|
||||
assertFalse(dimension.getRivers().getTopology().isRequireOcean());
|
||||
assertEquals(512, dimension.getRivers().getTopology().getCellSize());
|
||||
assertEquals(16, dimension.getRivers().getTopology().getMaxRouteReaches());
|
||||
assertEquals(0, dimension.getRivers().getTopology().getMinimumSourcesPerTile());
|
||||
assertEquals(64, dimension.getRivers().getTopology().getRoutingBasinCells());
|
||||
assertEquals(8D, dimension.getRivers().getTopology().getRoutingPlateauHeight(), 0D);
|
||||
assertEquals(0.05D, dimension.getRivers().getTopology().getSource().getChance(), 0D);
|
||||
assertEquals(0.035D, dimension.getRivers().getTopology().getSource().getInfluence(), 0D);
|
||||
assertEquals(10D, dimension.getRivers().getTerrain().getMaxChannelWidth(), 0D);
|
||||
assertEquals(4D, dimension.getRivers().getTerrain().getMaxBankWidth(), 0D);
|
||||
assertEquals(10D, dimension.getRivers().getTerrain().getMaxDepth(), 0D);
|
||||
assertTrue(dimension.getRivers().getTerrain().getWorms().isEmpty());
|
||||
assertEquals(IrisRiverCaveMode.SEALED, dimension.getRivers().getCaves().getMode());
|
||||
assertEquals(IrisRiverCaveFallback.SEALED, dimension.getRivers().getCaves().getFallback());
|
||||
assertEquals(IrisRiverExistingFluidPolicy.REJECT,
|
||||
dimension.getRivers().getCaves().getExistingFluidPolicy());
|
||||
assertNotNull(dimension.getRivers().getCaves().getDeepPools());
|
||||
assertFalse(dimension.getRivers().getCaves().getDeepPools().isEnabled());
|
||||
assertEquals(-224, dimension.getRivers().getCaves().getDeepPools().getMinimumFluidY());
|
||||
assertEquals(-104, dimension.getRivers().getCaves().getDeepPools().getMaximumFluidY());
|
||||
assertEquals("lava", dimension.getRivers().getCaves().getDeepPools()
|
||||
assertNotNull(dimension.getHydrology());
|
||||
assertNotNull(dimension.getHydrology().getRivers());
|
||||
assertFalse(dimension.getHydrology().getRivers().isEnabled());
|
||||
assertNotNull(dimension.getHydrology().getRivers().getRouting());
|
||||
assertNotNull(dimension.getHydrology().getRivers().getSurface());
|
||||
assertNotNull(dimension.getHydrology().getRivers().getUnderground());
|
||||
assertNotNull(dimension.getHydrology().getRivers().getGrottos());
|
||||
assertEquals(1, dimension.getHydrology().getRivers().getProfiles().size());
|
||||
assertEquals("default", dimension.getHydrology().getRivers().getProfiles().getFirst().getId());
|
||||
assertEquals("water", dimension.getHydrology().getRivers().getProfiles().getFirst()
|
||||
.getFluidPalette().getPalette().getFirst().getBlock());
|
||||
assertTrue(dimension.getRivers().getBiomes().getAllBiomeIds().isEmpty());
|
||||
assertNull(new IrisRegion().getRiverOverride());
|
||||
assertNull(new IrisBiome().getRiverOverride());
|
||||
assertTrue(dimension.getHydrology().getDeepFluids().isEmpty());
|
||||
|
||||
IrisRiverRoutingConfig routing = dimension.getHydrology().getRivers().getRouting();
|
||||
assertEquals(2048, routing.getTileSize());
|
||||
assertEquals(16384, routing.getMaximumRouteLength());
|
||||
assertEquals(384, routing.getBranching().getMinimumSurfaceCourseLength());
|
||||
assertEquals(192, routing.getBranching().getMinimumUndergroundCourseLength());
|
||||
|
||||
IrisSurfaceRiverConfig surface = dimension.getHydrology().getRivers().getSurface();
|
||||
assertTrue(surface.isEnabled());
|
||||
assertEquals(0.5D, surface.getSources().getDensity(), 0D);
|
||||
assertEquals(0, surface.getSources().getMinimumPerTile());
|
||||
assertEquals(384, surface.getSources().getMinimumSpacing());
|
||||
assertEquals(4D, surface.getChannel().getWidth().getMin(), 0D);
|
||||
assertEquals(8D, surface.getChannel().getWidth().getMax(), 0D);
|
||||
assertEquals(1D, surface.getChannel().getDepth().getMin(), 0D);
|
||||
assertEquals(4D, surface.getChannel().getDepth().getMax(), 0D);
|
||||
assertEquals(2D, surface.getChannel().getDepth().getStyle().getExponent(), 0D);
|
||||
assertEquals(3D, surface.getChannel().getSurfaceInset().getMin(), 0D);
|
||||
assertEquals(7D, surface.getChannel().getSurfaceInset().getMax(), 0D);
|
||||
assertEquals(2D, surface.getChannel().getSurfaceInset().getStyle().getExponent(), 0D);
|
||||
assertEquals(6, surface.getChannel().getMaximumIncision());
|
||||
assertEquals(2D, surface.getChannel().getShoreWidth(), 0D);
|
||||
assertEquals(64, surface.getMouths().getLevelingDistance());
|
||||
assertEquals(8, surface.getMouths().getMaximumOceanApron());
|
||||
assertEquals(80D, surface.getHydraulics().getTargetPoolLength().getMin(), 0D);
|
||||
assertEquals(180D, surface.getHydraulics().getTargetPoolLength().getMax(), 0D);
|
||||
assertEquals(8, surface.getHydraulics().getWaterfallMinimumDrop());
|
||||
IrisRiverDropShapeConfig drops = dimension.getHydrology().getRivers().getGeometry().getDrops();
|
||||
assertEquals(2, drops.getCascadeRunPerBlock());
|
||||
assertEquals(1.4D, drops.getCascadeExponent(), 0D);
|
||||
assertEquals(2, drops.getMaximumCascadeStep());
|
||||
assertEquals(0.45D, drops.getFlowWidthRatio(), 0D);
|
||||
assertEquals(2, drops.getMaximumFlowDepth());
|
||||
assertEquals(1.8D, drops.getBasinWidthRatio(), 0D);
|
||||
assertEquals(8, drops.getMaximumBasinDepth());
|
||||
|
||||
IrisUndergroundRiverConfig underground = dimension.getHydrology().getRivers().getUnderground();
|
||||
assertTrue(underground.isEnabled());
|
||||
assertEquals(0.25D, underground.getSources().getDensity(), 0D);
|
||||
assertEquals(0, underground.getSources().getMinimumPerTile());
|
||||
assertEquals(512, underground.getSources().getMinimumSpacing());
|
||||
assertEquals(-48D, underground.getFluidLevel().getMin(), 0D);
|
||||
assertEquals(50D, underground.getFluidLevel().getMax(), 0D);
|
||||
assertEquals(3D, underground.getChannelWidth().getMin(), 0D);
|
||||
assertEquals(8D, underground.getChannelWidth().getMax(), 0D);
|
||||
assertTrue(underground.isConnectToExistingCaves());
|
||||
|
||||
IrisCoastalRiverGrottoConfig coastal = dimension.getHydrology().getRivers().getGrottos().getCoastal();
|
||||
IrisInlandRiverGrottoConfig inland = dimension.getHydrology().getRivers().getGrottos().getInland();
|
||||
assertEquals(12, coastal.getHorizontalRadius());
|
||||
assertEquals(7, coastal.getVerticalRadius());
|
||||
assertEquals(10, coastal.getHeadroom());
|
||||
assertEquals(8192, coastal.getMaximumVolume());
|
||||
assertEquals(10, inland.getHorizontalRadius());
|
||||
assertEquals(6, inland.getVerticalRadius());
|
||||
assertEquals(10, inland.getHeadroom());
|
||||
assertEquals(8192, inland.getMaximumVolume());
|
||||
assertFalse(inland.isConnectSurfaceRivers());
|
||||
|
||||
IrisDeepFluidConfig deepFluid = new IrisDeepFluidConfig();
|
||||
assertEquals(0.125D, deepFluid.getDensity(), 0D);
|
||||
assertEquals(768, deepFluid.getSpacing());
|
||||
assertEquals(14, deepFluid.getHorizontalRadius());
|
||||
assertEquals(6, deepFluid.getVerticalRadius());
|
||||
assertEquals(3, deepFluid.getChannelWidth());
|
||||
assertEquals(1, deepFluid.getDepth());
|
||||
assertEquals(6, deepFluid.getHeadroom());
|
||||
assertFalse(deepFluid.isShortChannels());
|
||||
assertNotNull(dimension.getRiverPolicy());
|
||||
assertNull(dimension.getRiverPolicy().getPlacement());
|
||||
assertNull(dimension.getRiverPolicy().getRouting());
|
||||
assertNull(dimension.getRiverPolicy().getOutletAdmission());
|
||||
assertNull(new IrisRegion().getRiverPolicy());
|
||||
assertNull(new IrisBiome().getRiverPolicy());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deserializesTypedDimensionRegionAndBiomeSettings() {
|
||||
public void deserializesCanonicalDimensionRegionAndBiomeConfiguration() {
|
||||
Gson gson = new Gson();
|
||||
IrisDimension dimension = gson.fromJson("""
|
||||
{
|
||||
"rivers": {
|
||||
"enabled": true,
|
||||
"topology": {
|
||||
"cellSize": 512,
|
||||
"minimumSourcesPerTile": 2,
|
||||
"routingBasinCells": 96,
|
||||
"routingPlateauHeight": 12,
|
||||
"requireOcean": false,
|
||||
"source": {"chance": 0.27, "influence": 0.4}
|
||||
},
|
||||
"terrain": {
|
||||
"channelRadiusBonus": 3,
|
||||
"maxChannelWidth": 9,
|
||||
"maxBankWidth": 2.5,
|
||||
"maxDepth": 8,
|
||||
"maxIncision": 36,
|
||||
"worms": [
|
||||
{
|
||||
"seed": 73,
|
||||
"weight": 2.5,
|
||||
"wavelength": 1536,
|
||||
"detailWavelength": 192,
|
||||
"tortuosity": 0.65,
|
||||
"detailTortuosity": 0.2,
|
||||
"maxOffset": 420,
|
||||
"segments": 56,
|
||||
"widthMultiplier": 1.4,
|
||||
"bankMultiplier": 1.2,
|
||||
"depthMultiplier": 0.8,
|
||||
"bodyWavelength": 704,
|
||||
"bodyDetailWavelength": 18,
|
||||
"bodyDetailInfluence": 0.82,
|
||||
"widthVariation": 0.75,
|
||||
"bankVariation": 0.65,
|
||||
"depthVariation": 0.55,
|
||||
"roofVariation": 0.45
|
||||
}
|
||||
],
|
||||
"terminalMode": "SUPPRESS"
|
||||
},
|
||||
"water": {
|
||||
"mode": "TERRACED",
|
||||
"fluidHeight": -48,
|
||||
"fluidPalette": {
|
||||
"palette": [{"block": "minecraft:lava"}]
|
||||
},
|
||||
"poolLength": 80
|
||||
},
|
||||
"biomes": {
|
||||
"channel": ["river/channel"],
|
||||
"floodedCave": ["river/grotto"]
|
||||
},
|
||||
"caves": {
|
||||
"mode": "FLOOD_CLOSED_COMPONENT",
|
||||
"maxFloodVolume": 2048,
|
||||
"existingFluidPolicy": "ALLOW_SAME",
|
||||
"deepPools": {
|
||||
"enabled": true,
|
||||
"reach": {
|
||||
"chance": 0.4,
|
||||
"influence": 0.1
|
||||
"hydrology": {
|
||||
"rivers": {
|
||||
"enabled": true,
|
||||
"routing": {
|
||||
"tileSize": 4096,
|
||||
"sampleSpacing": 96,
|
||||
"refinementSpacing": 6,
|
||||
"branching": {
|
||||
"minimumSurfaceCourseLength": 640,
|
||||
"minimumUndergroundCourseLength": 480
|
||||
},
|
||||
"minimumSpacing": 896,
|
||||
"maximumPerReach": 2,
|
||||
"minimumFluidY": -220,
|
||||
"maximumFluidY": -112,
|
||||
"searchRadius": 24,
|
||||
"searchAttempts": 18,
|
||||
"horizontalRadius": 28,
|
||||
"verticalRadius": 11,
|
||||
"dryHeadroom": 5,
|
||||
"shapeVariation": 0.7,
|
||||
"warpStrength": 9,
|
||||
"maximumVolume": 65536,
|
||||
"fluidPalette": {
|
||||
"palette": [{"block": "minecraft:lava"}]
|
||||
"maximumRouteLength": 32768,
|
||||
"maximumOutletsPerTile": 3,
|
||||
"oceanOutlets": true,
|
||||
"inlandOutlets": ["SINKHOLE_GROTTO"]
|
||||
},
|
||||
"geometry": {
|
||||
"drops": {
|
||||
"cascadeRunPerBlock": 5,
|
||||
"cascadeExponent": 2.4,
|
||||
"maximumCascadeStep": 3,
|
||||
"flowWidthRatio": 0.6,
|
||||
"maximumFlowDepth": 4,
|
||||
"basinWidthRatio": 2.2,
|
||||
"maximumBasinDepth": 11
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"surface": {
|
||||
"sources": {"density": 0.75, "minimumElevation": 104, "minimumPerTile": 2,
|
||||
"minimumSpacing": 768},
|
||||
"channel": {
|
||||
"width": {"min": 5, "max": 24},
|
||||
"depth": {"min": 2, "max": 6},
|
||||
"surfaceInset": {"min": 5, "max": 9},
|
||||
"maximumIncision": 14,
|
||||
"shoreWidth": 1.5,
|
||||
"terrainBlendWidth": {"min": 8, "max": 10}
|
||||
},
|
||||
"hydraulics": {
|
||||
"targetPoolLength": {"min": 64, "max": 192},
|
||||
"riffleDrop": 1,
|
||||
"maximumGradualDrop": 4,
|
||||
"maximumGradualLength": 12,
|
||||
"waterfallMinimumDrop": 5
|
||||
},
|
||||
"ridgeTunnels": {"enabled": true, "maximumLength": 224, "headroom": 12},
|
||||
"mouths": {"levelingDistance": 80, "maximumOceanApron": 1}
|
||||
},
|
||||
"underground": {
|
||||
"sources": {"density": 0.4, "minimumPerTile": 3, "minimumSpacing": 896},
|
||||
"fluidLevel": {"min": -96, "max": 12},
|
||||
"channelWidth": {"min": 4, "max": 16},
|
||||
"depth": {"min": 2, "max": 4},
|
||||
"headroom": {"min": 7, "max": 15},
|
||||
"connectToExistingCaves": false
|
||||
},
|
||||
"grottos": {
|
||||
"coastal": {
|
||||
"enabled": true,
|
||||
"poolLevel": "SEA_LEVEL",
|
||||
"horizontalRadius": 40,
|
||||
"verticalRadius": 18,
|
||||
"headroom": 9,
|
||||
"maximumVolume": 70000
|
||||
},
|
||||
"inland": {
|
||||
"enabled": true,
|
||||
"connectSurfaceRivers": true,
|
||||
"horizontalRadius": 30,
|
||||
"verticalRadius": 15,
|
||||
"headroom": 7,
|
||||
"maximumVolume": 60000
|
||||
}
|
||||
},
|
||||
"profiles": [{
|
||||
"id": "underworld",
|
||||
"fluidPalette": {"palette": [{"block": "minecraft:lava"}]}
|
||||
}]
|
||||
},
|
||||
"deepFluids": [{
|
||||
"id": "deep_lava",
|
||||
"fluidPalette": {"palette": [{"block": "minecraft:lava"}]},
|
||||
"density": 0.3,
|
||||
"spacing": 896,
|
||||
"height": {"min": -220, "max": -112},
|
||||
"horizontalRadius": 36,
|
||||
"verticalRadius": 12,
|
||||
"channelWidth": 6,
|
||||
"depth": 3,
|
||||
"headroom": 7,
|
||||
"containedPools": true,
|
||||
"shortChannels": false
|
||||
}]
|
||||
},
|
||||
"riverPolicy": {
|
||||
"placement": "PREFERRED_HEADWATER",
|
||||
"routing": "PREFER",
|
||||
"outletAdmission": false,
|
||||
"profiles": ["underworld"],
|
||||
"surfaceBiomes": ["river/surface"],
|
||||
"shoreBiomes": ["river/shore"],
|
||||
"widthMultiplier": 1.5,
|
||||
"incisionMultiplier": 0.75
|
||||
}
|
||||
}
|
||||
""", IrisDimension.class);
|
||||
IrisRegion region = gson.fromJson("""
|
||||
{
|
||||
"riverOverride": {
|
||||
"allowSources": false,
|
||||
"routingPolicy": "AVOID",
|
||||
"widthMultiplier": 0.75,
|
||||
"bankBiomes": ["river/region-bank"]
|
||||
}
|
||||
}
|
||||
{"riverPolicy": {
|
||||
"placement": "TRANSIT_ONLY",
|
||||
"routing": "AVOID",
|
||||
"mouthBiomes": ["river/mouth"],
|
||||
"routingMultiplier": 2.5
|
||||
}}
|
||||
""", IrisRegion.class);
|
||||
IrisBiome biome = gson.fromJson("""
|
||||
{
|
||||
"riverOverride": {
|
||||
"routingPolicy": "BLOCK",
|
||||
"caveEntryMultiplier": 0.2,
|
||||
"floodedCaveBiomes": []
|
||||
}
|
||||
}
|
||||
{"riverPolicy": {
|
||||
"placement": "REQUIRED_HEADWATER",
|
||||
"routing": "BLOCK",
|
||||
"floodedCaveBiomes": [],
|
||||
"depthMultiplier": 0.8
|
||||
}}
|
||||
""", IrisBiome.class);
|
||||
|
||||
assertTrue(dimension.getRivers().isEnabled());
|
||||
assertEquals(512, dimension.getRivers().getTopology().getCellSize());
|
||||
assertEquals(2, dimension.getRivers().getTopology().getMinimumSourcesPerTile());
|
||||
assertEquals(96, dimension.getRivers().getTopology().getRoutingBasinCells());
|
||||
assertEquals(12D, dimension.getRivers().getTopology().getRoutingPlateauHeight(), 0D);
|
||||
assertFalse(dimension.getRivers().getTopology().isRequireOcean());
|
||||
assertEquals(0.27D, dimension.getRivers().getTopology().getSource().getChance(), 0D);
|
||||
assertEquals(9D, dimension.getRivers().getTerrain().getMaxChannelWidth(), 0D);
|
||||
assertEquals(3D, dimension.getRivers().getTerrain().getChannelRadiusBonus(), 0D);
|
||||
assertEquals(2.5D, dimension.getRivers().getTerrain().getMaxBankWidth(), 0D);
|
||||
assertEquals(8D, dimension.getRivers().getTerrain().getMaxDepth(), 0D);
|
||||
assertEquals(36, dimension.getRivers().getTerrain().getMaxIncision());
|
||||
IrisRiverWorm worm = dimension.getRivers().getTerrain().getWorms().get(0);
|
||||
assertEquals(73L, worm.getSeed());
|
||||
assertEquals(2.5D, worm.getWeight(), 0D);
|
||||
assertEquals(1536D, worm.getWavelength(), 0D);
|
||||
assertEquals(192D, worm.getDetailWavelength(), 0D);
|
||||
assertEquals(0.65D, worm.getTortuosity(), 0D);
|
||||
assertEquals(0.2D, worm.getDetailTortuosity(), 0D);
|
||||
assertEquals(420D, worm.getMaxOffset(), 0D);
|
||||
assertEquals(56, worm.getSegments());
|
||||
assertEquals(1.4D, worm.getWidthMultiplier(), 0D);
|
||||
assertEquals(1.2D, worm.getBankMultiplier(), 0D);
|
||||
assertEquals(0.8D, worm.getDepthMultiplier(), 0D);
|
||||
assertEquals(704D, worm.getBodyWavelength(), 0D);
|
||||
assertEquals(18D, worm.getBodyDetailWavelength(), 0D);
|
||||
assertEquals(0.82D, worm.getBodyDetailInfluence(), 0D);
|
||||
assertEquals(0.75D, worm.getWidthVariation(), 0D);
|
||||
assertEquals(0.65D, worm.getBankVariation(), 0D);
|
||||
assertEquals(0.55D, worm.getDepthVariation(), 0D);
|
||||
assertEquals(0.45D, worm.getRoofVariation(), 0D);
|
||||
assertEquals(IrisRiverTerminalMode.SUPPRESS,
|
||||
dimension.getRivers().getTerrain().getTerminalMode());
|
||||
assertEquals(IrisRiverWaterMode.TERRACED, dimension.getRivers().getWater().getMode());
|
||||
assertEquals(-48, dimension.getRivers().getWater().getFluidHeight());
|
||||
assertEquals("minecraft:lava",
|
||||
dimension.getRivers().getWater().getFluidPalette().getPalette().getFirst().getBlock());
|
||||
assertEquals(Set.of("river/channel", "river/grotto"),
|
||||
Set.copyOf(dimension.getRivers().getBiomes().getAllBiomeIds()));
|
||||
assertEquals(IrisRiverCaveMode.FLOOD_CLOSED_COMPONENT,
|
||||
dimension.getRivers().getCaves().getMode());
|
||||
assertEquals(IrisRiverExistingFluidPolicy.ALLOW_SAME,
|
||||
dimension.getRivers().getCaves().getExistingFluidPolicy());
|
||||
IrisRiverDeepPools deepPools = dimension.getRivers().getCaves().getDeepPools();
|
||||
assertTrue(deepPools.isEnabled());
|
||||
assertEquals(0.4D, deepPools.getReach().getChance(), 0D);
|
||||
assertEquals(0.1D, deepPools.getReach().getInfluence(), 0D);
|
||||
assertEquals(896, deepPools.getMinimumSpacing());
|
||||
assertEquals(2, deepPools.getMaximumPerReach());
|
||||
assertEquals(-220, deepPools.getMinimumFluidY());
|
||||
assertEquals(-112, deepPools.getMaximumFluidY());
|
||||
assertEquals(24, deepPools.getSearchRadius());
|
||||
assertEquals(18, deepPools.getSearchAttempts());
|
||||
assertEquals(28, deepPools.getHorizontalRadius());
|
||||
assertEquals(11, deepPools.getVerticalRadius());
|
||||
assertEquals(5, deepPools.getDryHeadroom());
|
||||
assertEquals(0.7D, deepPools.getShapeVariation(), 0D);
|
||||
assertEquals(9D, deepPools.getWarpStrength(), 0D);
|
||||
assertEquals(65536, deepPools.getMaximumVolume());
|
||||
assertEquals("minecraft:lava", deepPools.getFluidPalette().getPalette().getFirst().getBlock());
|
||||
IrisRiverHydrology rivers = dimension.getHydrology().getRivers();
|
||||
assertTrue(rivers.isEnabled());
|
||||
assertEquals(4096, rivers.getRouting().getTileSize());
|
||||
assertEquals(32768, rivers.getRouting().getMaximumRouteLength());
|
||||
assertEquals(3, rivers.getRouting().getMaximumOutletsPerTile());
|
||||
assertEquals(640, rivers.getRouting().getBranching().getMinimumSurfaceCourseLength());
|
||||
assertEquals(480, rivers.getRouting().getBranching().getMinimumUndergroundCourseLength());
|
||||
assertEquals(new KList<>(IrisRiverInlandOutlet.SINKHOLE_GROTTO), rivers.getRouting().getInlandOutlets());
|
||||
assertEquals(24D, rivers.getSurface().getChannel().getWidth().getMax(), 0D);
|
||||
assertEquals(9D, rivers.getSurface().getChannel().getSurfaceInset().getMax(), 0D);
|
||||
assertEquals(5, rivers.getSurface().getHydraulics().getWaterfallMinimumDrop());
|
||||
assertEquals(768, rivers.getSurface().getSources().getMinimumSpacing());
|
||||
assertEquals(5, rivers.getGeometry().getDrops().getCascadeRunPerBlock());
|
||||
assertEquals(2.4D, rivers.getGeometry().getDrops().getCascadeExponent(), 0D);
|
||||
assertEquals(3, rivers.getGeometry().getDrops().getMaximumCascadeStep());
|
||||
assertEquals(0.6D, rivers.getGeometry().getDrops().getFlowWidthRatio(), 0D);
|
||||
assertEquals(4, rivers.getGeometry().getDrops().getMaximumFlowDepth());
|
||||
assertEquals(2.2D, rivers.getGeometry().getDrops().getBasinWidthRatio(), 0D);
|
||||
assertEquals(11, rivers.getGeometry().getDrops().getMaximumBasinDepth());
|
||||
assertEquals(224, rivers.getSurface().getRidgeTunnels().getMaximumLength());
|
||||
assertEquals(-96D, rivers.getUnderground().getFluidLevel().getMin(), 0D);
|
||||
assertEquals(896, rivers.getUnderground().getSources().getMinimumSpacing());
|
||||
assertFalse(rivers.getUnderground().isConnectToExistingCaves());
|
||||
assertEquals(40, rivers.getGrottos().getCoastal().getHorizontalRadius());
|
||||
assertTrue(rivers.getGrottos().getInland().isEnabled());
|
||||
assertTrue(rivers.getGrottos().getInland().isConnectSurfaceRivers());
|
||||
assertEquals("underworld", rivers.getProfiles().getFirst().getId());
|
||||
|
||||
assertEquals(Boolean.FALSE, region.getRiverOverride().getAllowSources());
|
||||
assertEquals(IrisRiverRoutingPolicy.AVOID, region.getRiverOverride().getRoutingPolicy());
|
||||
assertEquals(Double.valueOf(0.75D), region.getRiverOverride().getWidthMultiplier());
|
||||
assertNull(region.getRiverOverride().getChannelBiomes());
|
||||
assertEquals(new KList<>("river/region-bank"), region.getRiverOverride().getBankBiomes());
|
||||
IrisDeepFluidConfig configuredDeepFluid = dimension.getHydrology().getDeepFluids().getFirst();
|
||||
assertEquals("deep_lava", configuredDeepFluid.getId());
|
||||
assertEquals(0.3D, configuredDeepFluid.getDensity(), 0D);
|
||||
assertEquals(896, configuredDeepFluid.getSpacing());
|
||||
assertEquals(-220D, configuredDeepFluid.getHeight().getMin(), 0D);
|
||||
assertEquals(36, configuredDeepFluid.getHorizontalRadius());
|
||||
assertEquals(6, configuredDeepFluid.getChannelWidth());
|
||||
assertFalse(configuredDeepFluid.isShortChannels());
|
||||
|
||||
assertEquals(IrisRiverRoutingPolicy.BLOCK, biome.getRiverOverride().getRoutingPolicy());
|
||||
assertNull(biome.getRiverOverride().getChannelBiomes());
|
||||
assertNotNull(biome.getRiverOverride().getFloodedCaveBiomes());
|
||||
assertTrue(biome.getRiverOverride().getFloodedCaveBiomes().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void resolvesRiverPoolsOnlyWhenRiversAreEnabled() {
|
||||
IrisRiverBiomes dimensionBiomes = new IrisRiverBiomes()
|
||||
.setChannel(new KList<>("dimension-channel"));
|
||||
IrisRiverOverride regionOverride = new IrisRiverOverride()
|
||||
.setBankBiomes(new KList<>("region-bank"));
|
||||
IrisRiverOverride biomeOverride = new IrisRiverOverride()
|
||||
.setMouthBiomes(new KList<>("biome-mouth"))
|
||||
.setFloodedCaveBiomes(new KList<>("biome-grotto"));
|
||||
IrisDimension dimension = new IrisDimension()
|
||||
.setRegions(new KList<>("region"))
|
||||
.setRivers(new IrisRiverNetwork()
|
||||
.setEnabled(true)
|
||||
.setTerrain(new IrisRiverTerrain()
|
||||
.setWorms(new KList<>(new IrisRiverWorm())))
|
||||
.setBiomes(dimensionBiomes));
|
||||
IrisRegion region = new IrisRegion()
|
||||
.setLandBiomes(new KList<>("natural"))
|
||||
.setRiverOverride(regionOverride);
|
||||
IrisBiome natural = biome("natural").setRiverOverride(biomeOverride);
|
||||
|
||||
IrisData data = mock(IrisData.class);
|
||||
ResourceLoader<IrisRegion> regionLoader = mock(ResourceLoader.class);
|
||||
ResourceLoader<IrisBiome> biomeLoader = mock(ResourceLoader.class);
|
||||
when(data.getRegionLoader()).thenReturn(regionLoader);
|
||||
when(data.getBiomeLoader()).thenReturn(biomeLoader);
|
||||
when(regionLoader.load("region")).thenReturn(region);
|
||||
when(biomeLoader.load("natural")).thenReturn(natural);
|
||||
when(biomeLoader.load("dimension-channel")).thenReturn(biome("dimension-channel"));
|
||||
when(biomeLoader.load("region-bank")).thenReturn(biome("region-bank"));
|
||||
when(biomeLoader.load("biome-mouth")).thenReturn(biome("biome-mouth"));
|
||||
when(biomeLoader.load("biome-grotto")).thenReturn(biome("biome-grotto"));
|
||||
|
||||
Set<String> enabledKeys = keys(dimension.getReachableBiomes(() -> data));
|
||||
dimension.getRivers().setEnabled(false);
|
||||
Set<String> disabledKeys = keys(dimension.getReachableBiomes(() -> data));
|
||||
|
||||
assertEquals(Set.of("natural", "dimension-channel", "region-bank", "biome-mouth", "biome-grotto"),
|
||||
enabledKeys);
|
||||
assertEquals(Set.of("natural"), disabledKeys);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void separatesNaturalAndRiverOnlyRegionBiomes() {
|
||||
IrisRegion region = new IrisRegion()
|
||||
.setLandBiomes(new KList<>("natural-parent"))
|
||||
.setRiverOverride(new IrisRiverOverride()
|
||||
.setChannelBiomes(new KList<>("river-parent")));
|
||||
IrisBiome naturalParent = biome("natural-parent").setChildren(new KList<>("natural-child"));
|
||||
IrisBiome naturalChild = biome("natural-child");
|
||||
IrisBiome riverParent = biome("river-parent").setChildren(new KList<>("river-child"));
|
||||
IrisBiome riverChild = biome("river-child");
|
||||
|
||||
IrisData data = mock(IrisData.class);
|
||||
ResourceLoader<IrisBiome> biomeLoader = mock(ResourceLoader.class);
|
||||
when(data.getBiomeLoader()).thenReturn(biomeLoader);
|
||||
when(biomeLoader.load("natural-parent")).thenReturn(naturalParent);
|
||||
when(biomeLoader.load("natural-child")).thenReturn(naturalChild);
|
||||
when(biomeLoader.load("river-parent")).thenReturn(riverParent);
|
||||
when(biomeLoader.load("river-child")).thenReturn(riverChild);
|
||||
|
||||
assertEquals(Set.of("natural-parent"), Set.copyOf(region.getNaturalBiomeIds()));
|
||||
assertEquals(Set.of("natural-parent", "river-parent"), Set.copyOf(region.getAllBiomeIds()));
|
||||
assertEquals(Set.of("natural-parent", "natural-child"),
|
||||
keys(region.getNaturalBiomes(() -> data)));
|
||||
assertEquals(Set.of("natural-parent", "natural-child", "river-parent", "river-child"),
|
||||
keys(region.getAllBiomes(() -> data)));
|
||||
}
|
||||
|
||||
private static IrisBiome biome(String loadKey) {
|
||||
IrisBiome biome = new IrisBiome();
|
||||
biome.setLoadKey(loadKey);
|
||||
return biome;
|
||||
}
|
||||
|
||||
private static Set<String> keys(KList<IrisBiome> biomes) {
|
||||
return biomes.stream().map(IrisBiome::getLoadKey).collect(Collectors.toSet());
|
||||
assertEquals(IrisRiverPlacementMode.PREFERRED_HEADWATER, dimension.getRiverPolicy().getPlacement());
|
||||
assertEquals(IrisRiverRoutingMode.PREFER, dimension.getRiverPolicy().getRouting());
|
||||
assertFalse(dimension.getRiverPolicy().getOutletAdmission());
|
||||
assertEquals(Set.of("river/surface", "river/shore"), dimension.getRiverPolicy().getAllBiomeIds());
|
||||
assertEquals(IrisRiverPlacementMode.TRANSIT_ONLY, region.getRiverPolicy().getPlacement());
|
||||
assertEquals(new KList<>("river/mouth"), region.getRiverPolicy().getMouthBiomes());
|
||||
assertEquals(IrisRiverPlacementMode.REQUIRED_HEADWATER, biome.getRiverPolicy().getPlacement());
|
||||
assertTrue(biome.getRiverPolicy().getFloodedCaveBiomes().isEmpty());
|
||||
}
|
||||
}
|
||||
-1
@@ -56,7 +56,6 @@ public class BukkitChunkGeneratorInitialSpawnTest {
|
||||
chunkFuture.complete(chunk);
|
||||
assertNotNull(regionTask.get());
|
||||
assertFalse(readiness.isDone());
|
||||
|
||||
regionTask.get().run();
|
||||
readiness.get(5L, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
+52
@@ -2,16 +2,26 @@ package art.arcane.iris.engine.platform;
|
||||
|
||||
import art.arcane.iris.core.link.Identifier;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineMetrics;
|
||||
import art.arcane.iris.engine.object.IrisWorld;
|
||||
import art.arcane.iris.util.project.matter.TileWrapper;
|
||||
import art.arcane.volmlib.util.function.Consumer4;
|
||||
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleDataAdapter;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleHooks;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.matter.MatterUpdate;
|
||||
import art.arcane.volmlib.util.matter.slices.UpdateMatter;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.junit.Test;
|
||||
import org.mockito.InOrder;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
@@ -20,13 +30,55 @@ import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
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 EngineBukkitOpsDeferredMaterializationTest {
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void updateMarkerUsesWorldHeightAndDeletesSliceAfterDispatch() {
|
||||
MantleChunk<Matter> mantleChunk = mock(MantleChunk.class);
|
||||
doAnswer(invocation -> {
|
||||
Consumer4<Integer, Integer, Integer, MatterUpdate> iterator = invocation.getArgument(1);
|
||||
iterator.accept(-1, 5, -2, UpdateMatter.ON);
|
||||
return null;
|
||||
}).when(mantleChunk).iterate(eq(MatterUpdate.class), any());
|
||||
IrisWorld irisWorld = mock(IrisWorld.class);
|
||||
when(irisWorld.minHeight()).thenReturn(-64);
|
||||
Engine engine = mock(Engine.class);
|
||||
when(engine.getWorld()).thenReturn(irisWorld);
|
||||
when(engine.getMetrics()).thenReturn(new EngineMetrics(8));
|
||||
Chunk chunk = mock(Chunk.class);
|
||||
ArrayList<String> updates = new ArrayList<>();
|
||||
|
||||
EngineBukkitOps.materializeUpdates(
|
||||
engine,
|
||||
chunk,
|
||||
mantleChunk,
|
||||
(x, y, z) -> updates.add(x + "," + y + "," + z)
|
||||
);
|
||||
|
||||
assertEquals(List.of("-1,-59,-2"), updates);
|
||||
verify(mantleChunk).deleteSlices(MatterUpdate.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fallingFluidUpdateRetainsLevelEightStateAndRequestsPhysics() {
|
||||
Block block = mock(Block.class);
|
||||
BlockData falling = mock(BlockData.class);
|
||||
|
||||
EngineBukkitOps.applyPhysicsUpdate(block, falling);
|
||||
|
||||
InOrder order = inOrder(block);
|
||||
order.verify(block).setType(Material.AIR, false);
|
||||
order.verify(block).setBlockData(falling, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void tileSliceIsDeletedAfterIteration() {
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,306 +0,0 @@
|
||||
package art.arcane.iris.engine.river;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
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.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class RiverTileCacheTest {
|
||||
@Test
|
||||
public void concurrentRequestsBuildOneTileOnce() throws Exception {
|
||||
int requestCount = 16;
|
||||
AtomicInteger builds = new AtomicInteger();
|
||||
CountDownLatch ready = new CountDownLatch(requestCount);
|
||||
CountDownLatch start = new CountDownLatch(1);
|
||||
CountDownLatch builderEntered = new CountDownLatch(1);
|
||||
CountDownLatch releaseBuilder = new CountDownLatch(1);
|
||||
RiverTileCache cache = new RiverTileCache(8, (tileX, tileZ) -> {
|
||||
builds.incrementAndGet();
|
||||
builderEntered.countDown();
|
||||
assertTrue(releaseBuilder.await(5, TimeUnit.SECONDS));
|
||||
return emptyTile(tileX, tileZ);
|
||||
});
|
||||
ExecutorService executor = Executors.newFixedThreadPool(requestCount);
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
Future<RiverTile>[] futures = new Future[requestCount];
|
||||
for (int request = 0; request < requestCount; request++) {
|
||||
futures[request] = executor.submit(() -> {
|
||||
ready.countDown();
|
||||
assertTrue(start.await(5, TimeUnit.SECONDS));
|
||||
return cache.get(-7, -11);
|
||||
});
|
||||
}
|
||||
assertTrue(ready.await(5, TimeUnit.SECONDS));
|
||||
start.countDown();
|
||||
assertTrue(builderEntered.await(5, TimeUnit.SECONDS));
|
||||
releaseBuilder.countDown();
|
||||
|
||||
RiverTile first = futures[0].get(5, TimeUnit.SECONDS);
|
||||
for (Future<RiverTile> future : futures) {
|
||||
assertSame(first, future.get(5, TimeUnit.SECONDS));
|
||||
}
|
||||
assertEquals(-7, first.tileX());
|
||||
assertEquals(-11, first.tileZ());
|
||||
assertEquals(1, builds.get());
|
||||
assertEquals(1, cache.completedSize());
|
||||
} finally {
|
||||
releaseBuilder.countDown();
|
||||
executor.shutdownNow();
|
||||
cache.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void differentKeysBuildConcurrentlyOutsideCacheLock() throws Exception {
|
||||
CountDownLatch buildersEntered = new CountDownLatch(2);
|
||||
CountDownLatch releaseBuilders = new CountDownLatch(1);
|
||||
RiverTileCache cache = new RiverTileCache(4, (tileX, tileZ) -> {
|
||||
buildersEntered.countDown();
|
||||
assertTrue(releaseBuilders.await(5, TimeUnit.SECONDS));
|
||||
return emptyTile(tileX, tileZ);
|
||||
});
|
||||
ExecutorService executor = Executors.newFixedThreadPool(2);
|
||||
try {
|
||||
Future<RiverTile> first = executor.submit(() -> cache.get(1, 1));
|
||||
Future<RiverTile> second = executor.submit(() -> cache.get(2, 2));
|
||||
assertTrue(buildersEntered.await(5, TimeUnit.SECONDS));
|
||||
releaseBuilders.countDown();
|
||||
assertEquals(1, first.get(5, TimeUnit.SECONDS).tileX());
|
||||
assertEquals(2, second.get(5, TimeUnit.SECONDS).tileX());
|
||||
} finally {
|
||||
releaseBuilders.countDown();
|
||||
executor.shutdownNow();
|
||||
cache.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void completedEntriesUseBoundedAccessOrderEviction() {
|
||||
AtomicInteger builds = new AtomicInteger();
|
||||
RiverTileCache cache = new RiverTileCache(2, (tileX, tileZ) -> {
|
||||
builds.incrementAndGet();
|
||||
return emptyTile(tileX, tileZ);
|
||||
});
|
||||
try {
|
||||
RiverTile zero = cache.get(0, 0);
|
||||
RiverTile one = cache.get(1, 0);
|
||||
assertSame(zero, cache.get(0, 0));
|
||||
cache.get(2, 0);
|
||||
assertEquals(2, cache.completedSize());
|
||||
assertSame(zero, cache.get(0, 0));
|
||||
|
||||
RiverTile rebuiltOne = cache.get(1, 0);
|
||||
assertFalse(one == rebuiltOne);
|
||||
assertEquals(4, builds.get());
|
||||
assertEquals(2, cache.completedSize());
|
||||
} finally {
|
||||
cache.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failedBuildsAreRemovedAndRetryable() {
|
||||
AtomicInteger attempts = new AtomicInteger();
|
||||
RiverTileCache cache = new RiverTileCache(2, (tileX, tileZ) -> {
|
||||
if (attempts.incrementAndGet() == 1) {
|
||||
throw new IllegalArgumentException("expected failure");
|
||||
}
|
||||
return emptyTile(tileX, tileZ);
|
||||
});
|
||||
try {
|
||||
IllegalArgumentException failure = assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> cache.get(-3, 4)
|
||||
);
|
||||
assertEquals("expected failure", failure.getMessage());
|
||||
assertEquals(0, cache.completedSize());
|
||||
|
||||
RiverTile retried = cache.get(-3, 4);
|
||||
assertEquals(-3, retried.tileX());
|
||||
assertEquals(2, attempts.get());
|
||||
assertEquals(1, cache.completedSize());
|
||||
} finally {
|
||||
cache.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mismatchedBuilderResultIsRemovedAndRetryable() {
|
||||
AtomicInteger attempts = new AtomicInteger();
|
||||
RiverTileCache cache = new RiverTileCache(2, (tileX, tileZ) -> attempts.incrementAndGet() == 1
|
||||
? emptyTile(tileX + 1, tileZ)
|
||||
: emptyTile(tileX, tileZ));
|
||||
try {
|
||||
assertThrows(IllegalStateException.class, () -> cache.get(5, 6));
|
||||
RiverTile retried = cache.get(5, 6);
|
||||
assertEquals(5, retried.tileX());
|
||||
assertEquals(2, attempts.get());
|
||||
} finally {
|
||||
cache.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void clearAndCloseReleaseEntriesAndEnforceLifecycle() {
|
||||
AtomicInteger builds = new AtomicInteger();
|
||||
RiverTileCache cache = new RiverTileCache(4, (tileX, tileZ) -> {
|
||||
builds.incrementAndGet();
|
||||
return emptyTile(tileX, tileZ);
|
||||
});
|
||||
cache.get(0, 0);
|
||||
cache.get(1, 0);
|
||||
assertEquals(2, cache.completedSize());
|
||||
|
||||
cache.clear();
|
||||
assertEquals(0, cache.completedSize());
|
||||
cache.get(0, 0);
|
||||
assertEquals(3, builds.get());
|
||||
|
||||
cache.close();
|
||||
cache.close();
|
||||
assertTrue(cache.isClosed());
|
||||
assertEquals(0, cache.completedSize());
|
||||
assertThrows(IllegalStateException.class, () -> cache.get(0, 0));
|
||||
assertThrows(IllegalStateException.class, cache::clear);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void closeInvalidatesInflightBuildWithoutRetainingItsResult() throws Exception {
|
||||
CountDownLatch builderEntered = new CountDownLatch(1);
|
||||
CountDownLatch releaseBuilder = new CountDownLatch(1);
|
||||
RiverTileCache cache = new RiverTileCache(2, (tileX, tileZ) -> {
|
||||
builderEntered.countDown();
|
||||
assertTrue(releaseBuilder.await(5, TimeUnit.SECONDS));
|
||||
return emptyTile(tileX, tileZ);
|
||||
});
|
||||
ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
try {
|
||||
Future<RiverTile> result = executor.submit(() -> cache.get(9, -9));
|
||||
assertTrue(builderEntered.await(5, TimeUnit.SECONDS));
|
||||
cache.close();
|
||||
releaseBuilder.countDown();
|
||||
|
||||
ExecutionException failure = assertThrows(
|
||||
ExecutionException.class,
|
||||
() -> result.get(5, TimeUnit.SECONDS)
|
||||
);
|
||||
assertTrue(failure.getCause() instanceof IllegalStateException);
|
||||
assertEquals(0, cache.completedSize());
|
||||
} finally {
|
||||
releaseBuilder.countDown();
|
||||
executor.shutdownNow();
|
||||
cache.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void clearInvalidatesInflightBuildAndFreshRequestPublishesNewEntry() throws Exception {
|
||||
AtomicInteger attempts = new AtomicInteger();
|
||||
CountDownLatch firstBuilderEntered = new CountDownLatch(1);
|
||||
CountDownLatch releaseFirstBuilder = new CountDownLatch(1);
|
||||
RiverTileCache cache = new RiverTileCache(2, (tileX, tileZ) -> {
|
||||
if (attempts.incrementAndGet() == 1) {
|
||||
firstBuilderEntered.countDown();
|
||||
assertTrue(releaseFirstBuilder.await(5, TimeUnit.SECONDS));
|
||||
}
|
||||
return emptyTile(tileX, tileZ);
|
||||
});
|
||||
ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
try {
|
||||
Future<RiverTile> invalidated = executor.submit(() -> cache.get(4, -2));
|
||||
assertTrue(firstBuilderEntered.await(5, TimeUnit.SECONDS));
|
||||
cache.clear();
|
||||
|
||||
RiverTile replacement = cache.get(4, -2);
|
||||
releaseFirstBuilder.countDown();
|
||||
|
||||
ExecutionException failure = assertThrows(
|
||||
ExecutionException.class,
|
||||
() -> invalidated.get(5, TimeUnit.SECONDS)
|
||||
);
|
||||
assertTrue(failure.getCause() instanceof IllegalStateException);
|
||||
assertEquals(4, replacement.tileX());
|
||||
assertEquals(-2, replacement.tileZ());
|
||||
assertEquals(2, attempts.get());
|
||||
assertEquals(1, cache.completedSize());
|
||||
assertSame(replacement, cache.get(4, -2));
|
||||
} finally {
|
||||
releaseFirstBuilder.countDown();
|
||||
executor.shutdownNow();
|
||||
cache.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tileProvidesConstantTimeReachIdentityLookup() {
|
||||
RiverReach reach = reach();
|
||||
RiverTile tile = new RiverTile(0, 0, 0, 0, 64, 64, List.of(reach));
|
||||
|
||||
assertSame(reach, tile.reach(reach.id()));
|
||||
assertNull(tile.reach(RiverEdgeId.of(new RiverNodeId(2L, 0L), new RiverNodeId(3L, 0L))));
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> new RiverTile(0, 0, 0, 0, 64, 64, List.of(reach, reach))
|
||||
);
|
||||
}
|
||||
|
||||
private static RiverTile emptyTile(int tileX, int tileZ) {
|
||||
int minimumX = tileX * 64;
|
||||
int minimumZ = tileZ * 64;
|
||||
return new RiverTile(tileX, tileZ, minimumX, minimumZ, minimumX + 64, minimumZ + 64, List.of());
|
||||
}
|
||||
|
||||
private static RiverReach reach() {
|
||||
RiverNode from = new RiverNode(
|
||||
new RiverNodeId(0L, 0L),
|
||||
8.0,
|
||||
8.0,
|
||||
20.0,
|
||||
20.0,
|
||||
20.0,
|
||||
20.0,
|
||||
false,
|
||||
true
|
||||
);
|
||||
RiverNode to = new RiverNode(
|
||||
new RiverNodeId(1L, 0L),
|
||||
56.0,
|
||||
8.0,
|
||||
10.0,
|
||||
10.0,
|
||||
10.0,
|
||||
10.0,
|
||||
false,
|
||||
true
|
||||
);
|
||||
return new RiverReach(
|
||||
RiverEdgeId.of(from.id(), to.id()),
|
||||
from,
|
||||
to,
|
||||
RiverRouteState.WET,
|
||||
1,
|
||||
1,
|
||||
8.0,
|
||||
4.0,
|
||||
3.0,
|
||||
RiverBodyProfile.constant(8.0, 4.0, 3.0),
|
||||
false,
|
||||
false,
|
||||
new RiverPolyline(new double[]{8.0, 56.0}, new double[]{8.0, 8.0})
|
||||
);
|
||||
}
|
||||
}
|
||||
-757
@@ -1,757 +0,0 @@
|
||||
package art.arcane.iris.engine.river.cave;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Queue;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class RiverCaveContainmentPlannerTest {
|
||||
private static final List<CavePosition> DIRECTIONS = List.of(
|
||||
new CavePosition(1, 0, 0),
|
||||
new CavePosition(-1, 0, 0),
|
||||
new CavePosition(0, 1, 0),
|
||||
new CavePosition(0, -1, 0),
|
||||
new CavePosition(0, 0, 1),
|
||||
new CavePosition(0, 0, -1)
|
||||
);
|
||||
|
||||
private final RiverCaveContainmentPlanner planner = new RiverCaveContainmentPlanner();
|
||||
|
||||
@Test
|
||||
public void closedComponentProducesConnectedThroatPoolAndGuards() {
|
||||
TestVoxelView view = new TestVoxelView();
|
||||
view.set(CaveVoxel.CAVE_AIR, position(0, 7, 0), position(1, 7, 0), position(1, 6, 0));
|
||||
RiverCaveSource source = source(14L, 10, RiverCaveMode.CLOSED_COMPONENT, position(0, 7, 0));
|
||||
|
||||
RiverCavePlan plan = planner.plan(view, source, settings());
|
||||
|
||||
assertTrue(plan.accepted());
|
||||
assertEquals(RiverCaveAction.DRY_AIR, plan.actions().get(position(0, 12, 0)));
|
||||
assertEquals(RiverCaveAction.DRY_AIR, plan.actions().get(position(0, 11, 0)));
|
||||
assertEquals(RiverCaveAction.WET_SOURCE, plan.actions().get(position(0, 10, 0)));
|
||||
assertEquals(RiverCaveAction.WET_SOURCE, plan.actions().get(position(1, 6, 0)));
|
||||
assertTrue(plan.actions().containsValue(RiverCaveAction.SEAL_GUARD));
|
||||
assertMutationConnected(plan, source.entry(), position(1, 6, 0));
|
||||
assertEquals(plan.actions().keySet(), plan.baselinePreconditions().keySet());
|
||||
assertEquals(CaveVoxel.CAVE_AIR, plan.baselinePreconditions().get(position(1, 6, 0)).voxel());
|
||||
assertEquals(CaveVoxel.SOLID, plan.baselinePreconditions().get(position(0, 12, 0)).voxel());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void compatibleFluidFollowsExplicitPolicy() {
|
||||
TestVoxelView view = new TestVoxelView();
|
||||
CavePosition target = position(0, 7, 0);
|
||||
view.set(CaveVoxel.COMPATIBLE_FLUID, target);
|
||||
RiverCaveSource source = source(1L, 10, RiverCaveMode.CLOSED_COMPONENT, target);
|
||||
|
||||
RiverCavePlan allowed = planner.plan(view, source, settings(RiverCaveFluidPolicy.ALLOW_COMPATIBLE));
|
||||
RiverCavePlan rejected = planner.plan(view, source, settings(RiverCaveFluidPolicy.REJECT_EXISTING));
|
||||
|
||||
assertTrue(allowed.accepted());
|
||||
assertEquals(RiverCaveAction.WET_SOURCE, allowed.actions().get(target));
|
||||
assertEquals(RiverCaveRejection.EXISTING_FLUID, rejected.rejection());
|
||||
assertTrue(rejected.actions().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void replacementPolicyIsDistinctAndStillRejectsLava() {
|
||||
CavePosition target = position(0, 7, 0);
|
||||
TestVoxelView compatibleView = new TestVoxelView();
|
||||
compatibleView.set(CaveVoxel.COMPATIBLE_FLUID, target);
|
||||
TestVoxelView incompatibleView = new TestVoxelView();
|
||||
incompatibleView.set(CaveVoxel.INCOMPATIBLE_FLUID, target);
|
||||
TestVoxelView lavaView = new TestVoxelView();
|
||||
lavaView.set(CaveVoxel.LAVA, target);
|
||||
RiverCaveSource source = source(101L, 10, RiverCaveMode.CLOSED_COMPONENT, target);
|
||||
|
||||
RiverCavePlan rejectedCompatible = planner.plan(
|
||||
compatibleView,
|
||||
source,
|
||||
settings(RiverCaveFluidPolicy.REJECT_EXISTING)
|
||||
);
|
||||
RiverCavePlan allowedCompatible = planner.plan(
|
||||
compatibleView,
|
||||
source,
|
||||
settings(RiverCaveFluidPolicy.ALLOW_COMPATIBLE)
|
||||
);
|
||||
RiverCavePlan rejectedIncompatible = planner.plan(
|
||||
incompatibleView,
|
||||
source,
|
||||
settings(RiverCaveFluidPolicy.ALLOW_COMPATIBLE)
|
||||
);
|
||||
RiverCavePlan replacedIncompatible = planner.plan(
|
||||
incompatibleView,
|
||||
source,
|
||||
settings(RiverCaveFluidPolicy.REPLACE_CONTAINED)
|
||||
);
|
||||
RiverCavePlan rejectedLava = planner.plan(
|
||||
lavaView,
|
||||
source,
|
||||
settings(RiverCaveFluidPolicy.REPLACE_CONTAINED)
|
||||
);
|
||||
|
||||
assertEquals(RiverCaveRejection.EXISTING_FLUID, rejectedCompatible.rejection());
|
||||
assertTrue(allowedCompatible.accepted());
|
||||
assertEquals(RiverCaveRejection.INCOMPATIBLE_FLUID, rejectedIncompatible.rejection());
|
||||
assertTrue(replacedIncompatible.accepted());
|
||||
assertEquals(RiverCaveAction.WET_SOURCE, replacedIncompatible.actions().get(target));
|
||||
assertEquals(RiverCaveRejection.LAVA_CONTACT, rejectedLava.rejection());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void throatRadiusExpandsTheConnectedBoreFootprint() {
|
||||
TestVoxelView thinView = new TestVoxelView();
|
||||
TestVoxelView thickView = new TestVoxelView();
|
||||
CavePosition target = position(0, 7, 0);
|
||||
thinView.set(CaveVoxel.CAVE_AIR, target);
|
||||
thickView.set(CaveVoxel.CAVE_AIR, target);
|
||||
RiverCaveSource source = source(102L, 10, RiverCaveMode.CLOSED_COMPONENT, target);
|
||||
RiverCavePlannerSettings thinSettings = detailedSettings(
|
||||
1,
|
||||
0,
|
||||
RiverCaveGrottoShape.ELLIPSOID,
|
||||
RiverCaveFluidPolicy.ALLOW_COMPATIBLE
|
||||
);
|
||||
RiverCavePlannerSettings thickSettings = detailedSettings(
|
||||
3,
|
||||
0,
|
||||
RiverCaveGrottoShape.ELLIPSOID,
|
||||
RiverCaveFluidPolicy.ALLOW_COMPATIBLE
|
||||
);
|
||||
|
||||
RiverCavePlan thin = planner.plan(thinView, source, thinSettings);
|
||||
RiverCavePlan thick = planner.plan(thickView, source, thickSettings);
|
||||
|
||||
assertTrue(thin.accepted());
|
||||
assertTrue(thick.accepted());
|
||||
assertFalse(thin.actions().containsKey(position(2, 10, 0)));
|
||||
assertEquals(RiverCaveAction.WET_SOURCE, thick.actions().get(position(2, 10, 0)));
|
||||
assertTrue(mutationCount(thick) > mutationCount(thin));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generatedGrottoRetainsConfiguredDryHeadroom() {
|
||||
TestVoxelView view = new TestVoxelView();
|
||||
RiverCaveSource source = source(103L, 10, RiverCaveMode.GENERATED_GROTTO, position(0, 8, 0));
|
||||
RiverCavePlannerSettings acceptedSettings = detailedSettings(
|
||||
1,
|
||||
2,
|
||||
RiverCaveGrottoShape.ELLIPSOID,
|
||||
RiverCaveFluidPolicy.ALLOW_COMPATIBLE
|
||||
);
|
||||
RiverCavePlannerSettings rejectedSettings = detailedSettings(
|
||||
1,
|
||||
5,
|
||||
RiverCaveGrottoShape.ELLIPSOID,
|
||||
RiverCaveFluidPolicy.ALLOW_COMPATIBLE
|
||||
);
|
||||
|
||||
RiverCavePlan accepted = planner.plan(view, source, acceptedSettings);
|
||||
RiverCavePlan rejected = planner.plan(view, source, rejectedSettings);
|
||||
|
||||
assertTrue(accepted.accepted());
|
||||
assertEquals(RiverCaveAction.FALLING_FLUID, accepted.actions().get(position(0, 11, 0)));
|
||||
assertEquals(RiverCaveAction.FALLING_FLUID, accepted.actions().get(position(0, 12, 0)));
|
||||
assertEquals(RiverCaveRejection.DRY_HEADROOM_LIMIT, rejected.rejection());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void configuredGrottoPredicateChangesFootprintDeterministically() {
|
||||
TestVoxelView view = new TestVoxelView();
|
||||
RiverCaveSource source = source(104L, 10, RiverCaveMode.GENERATED_GROTTO, position(0, 8, 0));
|
||||
RiverCavePlannerSettings ellipsoid = detailedSettings(
|
||||
1,
|
||||
0,
|
||||
RiverCaveGrottoShape.ELLIPSOID,
|
||||
RiverCaveFluidPolicy.ALLOW_COMPATIBLE
|
||||
);
|
||||
RiverCaveGrottoShape compactShape = (candidate, settings, dx, dy, dz) -> {
|
||||
double horizontal = settings.grottoHorizontalRadius();
|
||||
double vertical = settings.grottoVerticalRadius();
|
||||
double normalized = (dx * dx / (horizontal * horizontal))
|
||||
+ (dy * dy / (vertical * vertical))
|
||||
+ (dz * dz / (horizontal * horizontal));
|
||||
return normalized <= 0.5D;
|
||||
};
|
||||
RiverCavePlannerSettings compact = detailedSettings(
|
||||
1,
|
||||
0,
|
||||
compactShape,
|
||||
RiverCaveFluidPolicy.ALLOW_COMPATIBLE
|
||||
);
|
||||
|
||||
RiverCavePlan full = planner.plan(view, source, ellipsoid);
|
||||
RiverCavePlan firstCompact = planner.plan(view, source, compact);
|
||||
RiverCavePlan secondCompact = planner.plan(view, source, compact);
|
||||
|
||||
assertTrue(full.accepted());
|
||||
assertTrue(firstCompact.accepted());
|
||||
assertEquals(firstCompact, secondCompact);
|
||||
assertTrue(mutationCount(full) > mutationCount(firstCompact));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void radiusLimitRejectsWholeComponentWithoutClipping() {
|
||||
TestVoxelView view = new TestVoxelView();
|
||||
for (int x = 0; x <= 3; x++) {
|
||||
view.set(CaveVoxel.CAVE_AIR, position(x, 7, 0));
|
||||
}
|
||||
RiverCavePlannerSettings settings = new RiverCavePlannerSettings(
|
||||
2,
|
||||
20,
|
||||
64,
|
||||
32,
|
||||
1,
|
||||
1,
|
||||
RiverCaveFluidPolicy.ALLOW_COMPATIBLE
|
||||
);
|
||||
|
||||
RiverCavePlan plan = planner.plan(
|
||||
view,
|
||||
source(2L, 10, RiverCaveMode.CLOSED_COMPONENT, position(0, 7, 0)),
|
||||
settings
|
||||
);
|
||||
|
||||
assertRejectedWithoutPublication(plan, RiverCaveRejection.RADIUS_LIMIT);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void depthLimitRejectsWholeComponentWithoutClipping() {
|
||||
TestVoxelView view = new TestVoxelView();
|
||||
view.set(CaveVoxel.CAVE_AIR, position(0, 8, 0), position(0, 7, 0));
|
||||
RiverCavePlannerSettings settings = new RiverCavePlannerSettings(
|
||||
8,
|
||||
4,
|
||||
64,
|
||||
32,
|
||||
1,
|
||||
1,
|
||||
RiverCaveFluidPolicy.ALLOW_COMPATIBLE
|
||||
);
|
||||
|
||||
RiverCavePlan plan = planner.plan(
|
||||
view,
|
||||
source(3L, 10, RiverCaveMode.CLOSED_COMPONENT, position(0, 8, 0)),
|
||||
settings
|
||||
);
|
||||
|
||||
assertRejectedWithoutPublication(plan, RiverCaveRejection.DEPTH_LIMIT);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void volumeLimitRejectsWholeComponentWithoutClipping() {
|
||||
TestVoxelView view = new TestVoxelView();
|
||||
view.set(CaveVoxel.CAVE_AIR, position(0, 7, 0), position(1, 7, 0), position(2, 7, 0));
|
||||
RiverCavePlannerSettings settings = new RiverCavePlannerSettings(
|
||||
8,
|
||||
20,
|
||||
2,
|
||||
32,
|
||||
1,
|
||||
1,
|
||||
RiverCaveFluidPolicy.ALLOW_COMPATIBLE
|
||||
);
|
||||
|
||||
RiverCavePlan plan = planner.plan(
|
||||
view,
|
||||
source(4L, 10, RiverCaveMode.CLOSED_COMPONENT, position(0, 7, 0)),
|
||||
settings
|
||||
);
|
||||
|
||||
assertRejectedWithoutPublication(plan, RiverCaveRejection.VOLUME_LIMIT);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void worldBoundaryRejectsWholeComponent() {
|
||||
TestVoxelView view = new TestVoxelView(-16, 0, 0, 32, -16, 16);
|
||||
view.set(CaveVoxel.CAVE_AIR, position(0, 7, 0));
|
||||
|
||||
RiverCavePlan plan = planner.plan(
|
||||
view,
|
||||
source(5L, 10, RiverCaveMode.CLOSED_COMPONENT, position(0, 7, 0)),
|
||||
settings()
|
||||
);
|
||||
|
||||
assertRejectedWithoutPublication(plan, RiverCaveRejection.WORLD_BOUNDARY);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void openSurfaceRejectsWholeComponent() {
|
||||
TestVoxelView view = new TestVoxelView();
|
||||
CavePosition target = position(0, 7, 0);
|
||||
view.set(CaveVoxel.CAVE_AIR, target);
|
||||
view.openToSurface(target);
|
||||
|
||||
RiverCavePlan plan = planner.plan(
|
||||
view,
|
||||
source(6L, 10, RiverCaveMode.CLOSED_COMPONENT, target),
|
||||
settings()
|
||||
);
|
||||
|
||||
assertRejectedWithoutPublication(plan, RiverCaveRejection.OPEN_SURFACE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lavaAndIncompatibleFluidContactsRejectWholeComponent() {
|
||||
TestVoxelView lavaView = new TestVoxelView();
|
||||
lavaView.set(CaveVoxel.CAVE_AIR, position(0, 7, 0));
|
||||
lavaView.set(CaveVoxel.LAVA, position(1, 7, 0));
|
||||
TestVoxelView fluidView = new TestVoxelView();
|
||||
fluidView.set(CaveVoxel.CAVE_AIR, position(0, 7, 0));
|
||||
fluidView.set(CaveVoxel.INCOMPATIBLE_FLUID, position(1, 7, 0));
|
||||
RiverCaveSource source = source(7L, 10, RiverCaveMode.CLOSED_COMPONENT, position(0, 7, 0));
|
||||
|
||||
RiverCavePlan lavaPlan = planner.plan(lavaView, source, settings());
|
||||
RiverCavePlan fluidPlan = planner.plan(fluidView, source, settings());
|
||||
|
||||
assertRejectedWithoutPublication(lavaPlan, RiverCaveRejection.LAVA_CONTACT);
|
||||
assertRejectedWithoutPublication(fluidPlan, RiverCaveRejection.INCOMPATIBLE_FLUID);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fallingHazardAbovePoolRejectsWholeComponent() {
|
||||
TestVoxelView view = new TestVoxelView();
|
||||
view.set(CaveVoxel.CAVE_AIR, position(0, 7, 0));
|
||||
view.set(CaveVoxel.LAVA, position(1, 11, 0));
|
||||
|
||||
RiverCavePlan plan = planner.plan(
|
||||
view,
|
||||
source(15L, 10, RiverCaveMode.CLOSED_COMPONENT, position(0, 7, 0)),
|
||||
settings()
|
||||
);
|
||||
|
||||
assertRejectedWithoutPublication(plan, RiverCaveRejection.LAVA_CONTACT);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generatedGrottoHasBoundedWetDryAndGuardActions() {
|
||||
TestVoxelView view = new TestVoxelView();
|
||||
RiverCaveSource source = source(8L, 10, RiverCaveMode.GENERATED_GROTTO, position(0, 8, 0));
|
||||
|
||||
RiverCavePlan plan = planner.plan(view, source, settings());
|
||||
|
||||
assertTrue(plan.accepted());
|
||||
assertEquals(RiverCaveAction.FALLING_FLUID, plan.actions().get(position(0, 12, 0)));
|
||||
assertEquals(RiverCaveAction.WET_SOURCE, plan.actions().get(position(0, 8, 0)));
|
||||
assertTrue(plan.actions().containsValue(RiverCaveAction.SEAL_GUARD));
|
||||
assertEquals(plan.actions().keySet(), plan.baselinePreconditions().keySet());
|
||||
assertFalse(plan.actions().containsKey(position(0, 13, 0)));
|
||||
assertMutationConnected(plan, source.entry(), source.target());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generatedGrottoAcceptsItsCompleteSurfaceMouth() {
|
||||
TestVoxelView view = new TestVoxelView();
|
||||
CavePosition[] wetMouth = {
|
||||
position(-1, 12, 0),
|
||||
position(1, 12, 0),
|
||||
position(0, 12, -1),
|
||||
position(0, 12, 1)
|
||||
};
|
||||
CavePosition[] dryMouth = {
|
||||
position(-1, 13, 0),
|
||||
position(1, 13, 0),
|
||||
position(0, 13, -1),
|
||||
position(0, 13, 1)
|
||||
};
|
||||
view.set(CaveVoxel.COMPATIBLE_FLUID, wetMouth);
|
||||
view.set(CaveVoxel.CAVE_AIR, dryMouth);
|
||||
for (CavePosition position : wetMouth) {
|
||||
view.openToSurface(position);
|
||||
}
|
||||
for (CavePosition position : dryMouth) {
|
||||
view.openToSurface(position);
|
||||
}
|
||||
RiverCaveSource source = source(108L, 10, RiverCaveMode.GENERATED_GROTTO, position(0, 8, 0));
|
||||
|
||||
RiverCavePlan plan = planner.plan(view, source, detailedSettings(
|
||||
2,
|
||||
2,
|
||||
RiverCaveGrottoShape.ELLIPSOID,
|
||||
RiverCaveFluidPolicy.ALLOW_COMPATIBLE
|
||||
));
|
||||
|
||||
assertTrue(plan.rejection().toString(), plan.accepted());
|
||||
for (CavePosition position : wetMouth) {
|
||||
assertEquals(RiverCaveAction.FALLING_FLUID, plan.actions().get(position));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generatedGrottoRejectsAnOpenShell() {
|
||||
TestVoxelView view = new TestVoxelView();
|
||||
view.set(CaveVoxel.CAVE_AIR, position(3, 8, 0));
|
||||
|
||||
RiverCavePlan plan = planner.plan(
|
||||
view,
|
||||
source(9L, 10, RiverCaveMode.GENERATED_GROTTO, position(0, 8, 0)),
|
||||
settings()
|
||||
);
|
||||
|
||||
assertRejectedWithoutPublication(plan, RiverCaveRejection.GROTTO_SHELL_OPEN);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deepPoolOpensOnlyAboveItsContainedFluidHead() {
|
||||
TestVoxelView view = new TestVoxelView();
|
||||
view.set(CaveVoxel.CAVE_AIR, position(0, 11, 0), position(0, 13, 0));
|
||||
RiverCaveSource source = new RiverCaveSource(
|
||||
109L,
|
||||
position(0, 10, 0),
|
||||
position(0, 8, 0),
|
||||
10,
|
||||
RiverCaveMode.DEEP_POOL
|
||||
);
|
||||
|
||||
RiverCavePlan plan = planner.plan(view, source, detailedSettings(
|
||||
1,
|
||||
2,
|
||||
RiverCaveGrottoShape.ELLIPSOID,
|
||||
RiverCaveFluidPolicy.REJECT_EXISTING
|
||||
));
|
||||
|
||||
assertTrue(plan.rejection().toString(), plan.accepted());
|
||||
assertEquals(RiverCaveAction.WET_SOURCE, plan.actions().get(position(0, 10, 0)));
|
||||
assertEquals(RiverCaveAction.DRY_AIR, plan.actions().get(position(0, 11, 0)));
|
||||
assertFalse(plan.actions().containsKey(position(0, 13, 0)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deepPoolRejectsCaveLeakAtOrBelowItsFluidHead() {
|
||||
TestVoxelView view = new TestVoxelView();
|
||||
view.set(CaveVoxel.CAVE_AIR, position(5, 8, 0));
|
||||
RiverCaveSource source = new RiverCaveSource(
|
||||
110L,
|
||||
position(0, 10, 0),
|
||||
position(0, 8, 0),
|
||||
10,
|
||||
RiverCaveMode.DEEP_POOL
|
||||
);
|
||||
|
||||
RiverCavePlan plan = planner.plan(view, source, detailedSettings(
|
||||
1,
|
||||
2,
|
||||
RiverCaveGrottoShape.ELLIPSOID,
|
||||
RiverCaveFluidPolicy.REJECT_EXISTING
|
||||
));
|
||||
|
||||
assertRejectedWithoutPublication(plan, RiverCaveRejection.GROTTO_SHELL_OPEN);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void combinedModeUsesClosedCaveOrGeneratedGrottoFromBaseline() {
|
||||
TestVoxelView caveView = new TestVoxelView();
|
||||
CavePosition target = position(0, 8, 0);
|
||||
caveView.set(CaveVoxel.CAVE_AIR, target);
|
||||
TestVoxelView solidView = new TestVoxelView();
|
||||
RiverCaveSource source = source(10L, 10, RiverCaveMode.GROTTO_OR_CLOSED_COMPONENT, target);
|
||||
|
||||
RiverCavePlan cavePlan = planner.plan(caveView, source, settings());
|
||||
RiverCavePlan grottoPlan = planner.plan(solidView, source, settings());
|
||||
|
||||
assertTrue(cavePlan.accepted());
|
||||
assertTrue(grottoPlan.accepted());
|
||||
assertEquals(CaveVoxel.CAVE_AIR, cavePlan.baselinePreconditions().get(target).voxel());
|
||||
assertEquals(CaveVoxel.SOLID, grottoPlan.baselinePreconditions().get(target).voxel());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void waterfallKeepsColumnDistinctFromSourcePoolAndDryAir() {
|
||||
TestVoxelView view = new TestVoxelView();
|
||||
RiverCaveSource source = source(11L, 10, RiverCaveMode.WATERFALL_POOL, position(0, 8, 0));
|
||||
|
||||
RiverCavePlan plan = planner.plan(view, source, settings());
|
||||
|
||||
assertTrue(plan.accepted());
|
||||
assertEquals(RiverCaveAction.FALLING_FLUID, plan.actions().get(position(0, 12, 0)));
|
||||
assertEquals(RiverCaveAction.FALLING_FLUID, plan.actions().get(position(0, 11, 0)));
|
||||
assertEquals(RiverCaveAction.WET_SOURCE, plan.actions().get(position(0, 10, 0)));
|
||||
assertFalse(plan.actions().containsValue(RiverCaveAction.DRY_AIR));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void waterfallRejectsAnUnsealedFallingColumn() {
|
||||
TestVoxelView view = new TestVoxelView();
|
||||
view.set(CaveVoxel.CAVE_AIR, position(0, 8, 0), position(1, 11, 0));
|
||||
RiverCaveSource source = source(13L, 10, RiverCaveMode.WATERFALL_POOL, position(0, 8, 0));
|
||||
|
||||
RiverCavePlan plan = planner.plan(view, source, settings());
|
||||
|
||||
assertRejectedWithoutPublication(plan, RiverCaveRejection.WATERFALL_SHAFT_OPEN);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void overlapArbitrationIsOrderIndependentAndNamesWinner() {
|
||||
TestVoxelView view = new TestVoxelView();
|
||||
view.set(CaveVoxel.CAVE_AIR, position(0, 7, 0), position(1, 7, 0), position(2, 7, 0));
|
||||
RiverCaveSource lowerHead = new RiverCaveSource(
|
||||
1L,
|
||||
position(2, 12, 0),
|
||||
position(2, 7, 0),
|
||||
10,
|
||||
RiverCaveMode.CLOSED_COMPONENT
|
||||
);
|
||||
RiverCaveSource higherHead = new RiverCaveSource(
|
||||
20L,
|
||||
position(0, 12, 0),
|
||||
position(0, 7, 0),
|
||||
11,
|
||||
RiverCaveMode.CLOSED_COMPONENT
|
||||
);
|
||||
|
||||
RiverCavePlanningResult forward = planner.planAll(view, List.of(lowerHead, higherHead), settings());
|
||||
RiverCavePlanningResult reverse = planner.planAll(view, List.of(higherHead, lowerHead), settings());
|
||||
|
||||
assertEquals(forward, reverse);
|
||||
assertEquals(higherHead, forward.plans().get(0).source());
|
||||
assertTrue(forward.plans().get(0).accepted());
|
||||
assertEquals(RiverCaveRejection.OVERLAPPING_SOURCE, forward.plans().get(1).rejection());
|
||||
assertEquals(20L, forward.plans().get(1).arbitrationWinnerSourceId().getAsLong());
|
||||
assertEquals(forward.actions().keySet(), forward.baselinePreconditions().keySet());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void equalHeadOverlapUsesLowestStableSourceId() {
|
||||
TestVoxelView view = new TestVoxelView();
|
||||
view.set(CaveVoxel.CAVE_AIR, position(0, 7, 0), position(1, 7, 0), position(2, 7, 0));
|
||||
RiverCaveSource largerId = new RiverCaveSource(
|
||||
20L,
|
||||
position(0, 12, 0),
|
||||
position(0, 7, 0),
|
||||
10,
|
||||
RiverCaveMode.CLOSED_COMPONENT
|
||||
);
|
||||
RiverCaveSource smallerId = new RiverCaveSource(
|
||||
1L,
|
||||
position(2, 12, 0),
|
||||
position(2, 7, 0),
|
||||
10,
|
||||
RiverCaveMode.CLOSED_COMPONENT
|
||||
);
|
||||
|
||||
RiverCavePlanningResult result = planner.planAll(view, List.of(largerId, smallerId), settings());
|
||||
|
||||
assertEquals(smallerId, result.plans().get(0).source());
|
||||
assertTrue(result.plans().get(0).accepted());
|
||||
assertEquals(1L, result.plans().get(1).arbitrationWinnerSourceId().getAsLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void arbitrationUsesDirectHigherPriorityPlansEvenWhenTheyAreRejected() {
|
||||
TestVoxelView view = new TestVoxelView();
|
||||
view.set(
|
||||
CaveVoxel.CAVE_AIR,
|
||||
position(0, 7, 0),
|
||||
position(2, 7, 0),
|
||||
position(4, 7, 0)
|
||||
);
|
||||
RiverCaveSource lowest = new RiverCaveSource(
|
||||
30L,
|
||||
position(0, 12, 0),
|
||||
position(0, 7, 0),
|
||||
10,
|
||||
RiverCaveMode.CLOSED_COMPONENT
|
||||
);
|
||||
RiverCaveSource middle = new RiverCaveSource(
|
||||
20L,
|
||||
position(2, 12, 0),
|
||||
position(2, 7, 0),
|
||||
11,
|
||||
RiverCaveMode.CLOSED_COMPONENT
|
||||
);
|
||||
RiverCaveSource highest = new RiverCaveSource(
|
||||
10L,
|
||||
position(4, 12, 0),
|
||||
position(4, 7, 0),
|
||||
12,
|
||||
RiverCaveMode.CLOSED_COMPONENT
|
||||
);
|
||||
|
||||
RiverCavePlanningResult forward = planner.planAll(
|
||||
view,
|
||||
List.of(lowest, middle, highest),
|
||||
settings()
|
||||
);
|
||||
RiverCavePlanningResult reverse = planner.planAll(
|
||||
view,
|
||||
List.of(highest, middle, lowest),
|
||||
settings()
|
||||
);
|
||||
|
||||
assertEquals(forward, reverse);
|
||||
assertTrue(forward.plans().get(0).accepted());
|
||||
assertEquals(RiverCaveRejection.OVERLAPPING_SOURCE, forward.plans().get(1).rejection());
|
||||
assertEquals(10L, forward.plans().get(1).arbitrationWinnerSourceId().getAsLong());
|
||||
assertEquals(RiverCaveRejection.OVERLAPPING_SOURCE, forward.plans().get(2).rejection());
|
||||
assertEquals(20L, forward.plans().get(2).arbitrationWinnerSourceId().getAsLong());
|
||||
assertFalse(forward.actions().containsKey(position(0, 7, 0)));
|
||||
assertTrue(forward.actions().containsKey(position(4, 7, 0)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void planOutputsAreImmutable() {
|
||||
TestVoxelView view = new TestVoxelView();
|
||||
view.set(CaveVoxel.CAVE_AIR, position(0, 7, 0));
|
||||
RiverCaveSource source = source(12L, 10, RiverCaveMode.CLOSED_COMPONENT, position(0, 7, 0));
|
||||
RiverCavePlanningResult result = planner.planAll(view, List.of(source), settings());
|
||||
RiverCavePlan plan = result.plans().get(0);
|
||||
|
||||
assertThrows(
|
||||
UnsupportedOperationException.class,
|
||||
() -> plan.actions().put(position(9, 9, 9), RiverCaveAction.WET_SOURCE)
|
||||
);
|
||||
assertThrows(
|
||||
UnsupportedOperationException.class,
|
||||
() -> plan.baselinePreconditions().put(
|
||||
position(9, 9, 9),
|
||||
new CaveVoxelPrecondition(CaveVoxel.SOLID, false)
|
||||
)
|
||||
);
|
||||
assertThrows(UnsupportedOperationException.class, () -> result.plans().add(plan));
|
||||
assertThrows(
|
||||
UnsupportedOperationException.class,
|
||||
() -> result.actions().put(position(9, 9, 9), RiverCaveAction.WET_SOURCE)
|
||||
);
|
||||
}
|
||||
|
||||
private void assertMutationConnected(RiverCavePlan plan, CavePosition start, CavePosition expected) {
|
||||
Set<CavePosition> mutations = new HashSet<>();
|
||||
for (Map.Entry<CavePosition, RiverCaveAction> entry : plan.actions().entrySet()) {
|
||||
if (entry.getValue() != RiverCaveAction.SEAL_GUARD) {
|
||||
mutations.add(entry.getKey());
|
||||
}
|
||||
}
|
||||
Queue<CavePosition> queue = new ArrayDeque<>();
|
||||
Set<CavePosition> visited = new HashSet<>();
|
||||
queue.add(start);
|
||||
visited.add(start);
|
||||
while (!queue.isEmpty()) {
|
||||
CavePosition position = queue.remove();
|
||||
for (CavePosition direction : DIRECTIONS) {
|
||||
CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z());
|
||||
if (mutations.contains(neighbor) && visited.add(neighbor)) {
|
||||
queue.add(neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
assertTrue(visited.contains(expected));
|
||||
assertEquals(mutations, visited);
|
||||
}
|
||||
|
||||
private int mutationCount(RiverCavePlan plan) {
|
||||
int count = 0;
|
||||
for (RiverCaveAction action : plan.actions().values()) {
|
||||
if (action != RiverCaveAction.SEAL_GUARD) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private void assertRejectedWithoutPublication(RiverCavePlan plan, RiverCaveRejection rejection) {
|
||||
assertFalse(plan.accepted());
|
||||
assertEquals(rejection, plan.rejection());
|
||||
assertTrue(plan.actions().isEmpty());
|
||||
assertTrue(plan.baselinePreconditions().isEmpty());
|
||||
}
|
||||
|
||||
private RiverCaveSource source(long sourceId, int waterHeadY, RiverCaveMode mode, CavePosition target) {
|
||||
return new RiverCaveSource(sourceId, position(0, 12, 0), target, waterHeadY, mode);
|
||||
}
|
||||
|
||||
private RiverCavePlannerSettings settings() {
|
||||
return settings(RiverCaveFluidPolicy.ALLOW_COMPATIBLE);
|
||||
}
|
||||
|
||||
private RiverCavePlannerSettings settings(RiverCaveFluidPolicy policy) {
|
||||
return new RiverCavePlannerSettings(12, 20, 64, 32, 2, 2, policy);
|
||||
}
|
||||
|
||||
private RiverCavePlannerSettings detailedSettings(
|
||||
int throatRadius,
|
||||
int dryHeadroom,
|
||||
RiverCaveGrottoShape shape,
|
||||
RiverCaveFluidPolicy policy
|
||||
) {
|
||||
return new RiverCavePlannerSettings(
|
||||
12,
|
||||
20,
|
||||
2048,
|
||||
32,
|
||||
throatRadius,
|
||||
4,
|
||||
4,
|
||||
dryHeadroom,
|
||||
policy,
|
||||
shape
|
||||
);
|
||||
}
|
||||
|
||||
private CavePosition position(int x, int y, int z) {
|
||||
return new CavePosition(x, y, z);
|
||||
}
|
||||
|
||||
private static final class TestVoxelView implements CaveVoxelView {
|
||||
private final int minX;
|
||||
private final int maxX;
|
||||
private final int minY;
|
||||
private final int maxY;
|
||||
private final int minZ;
|
||||
private final int maxZ;
|
||||
private final Map<CavePosition, CaveVoxel> voxels = new HashMap<>();
|
||||
private final Set<CavePosition> surfaceOpenings = new HashSet<>();
|
||||
|
||||
private TestVoxelView() {
|
||||
this(-32, 32, 0, 32, -32, 32);
|
||||
}
|
||||
|
||||
private TestVoxelView(int minX, int maxX, int minY, int maxY, int minZ, int maxZ) {
|
||||
this.minX = minX;
|
||||
this.maxX = maxX;
|
||||
this.minY = minY;
|
||||
this.maxY = maxY;
|
||||
this.minZ = minZ;
|
||||
this.maxZ = maxZ;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInWorld(CavePosition position) {
|
||||
return position.x() >= minX
|
||||
&& position.x() <= maxX
|
||||
&& position.y() >= minY
|
||||
&& position.y() <= maxY
|
||||
&& position.z() >= minZ
|
||||
&& position.z() <= maxZ;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CaveVoxel voxelAt(CavePosition position) {
|
||||
return voxels.getOrDefault(position, CaveVoxel.SOLID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpenToSurface(CavePosition position) {
|
||||
return surfaceOpenings.contains(position);
|
||||
}
|
||||
|
||||
private void set(CaveVoxel voxel, CavePosition... positions) {
|
||||
for (CavePosition position : positions) {
|
||||
voxels.put(position, voxel);
|
||||
}
|
||||
}
|
||||
|
||||
private void openToSurface(CavePosition position) {
|
||||
surfaceOpenings.add(position);
|
||||
}
|
||||
}
|
||||
}
|
||||
-62
@@ -1,62 +0,0 @@
|
||||
package art.arcane.iris.engine.river.runtime;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.IrisRiverBiomes;
|
||||
import art.arcane.iris.engine.object.IrisRiverNetwork;
|
||||
import art.arcane.iris.engine.object.IrisRiverOverride;
|
||||
import art.arcane.iris.engine.object.IrisRiverRoutingPolicy;
|
||||
import art.arcane.iris.engine.object.IrisRiverTerminalMode;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class EffectiveRiverSettingsTest {
|
||||
@Test
|
||||
public void appliesRegionThenBiomeOverridesWithoutChangingGraphSettings() {
|
||||
IrisRiverNetwork network = new IrisRiverNetwork()
|
||||
.setBiomes(new IrisRiverBiomes()
|
||||
.setChannel(new KList<>("dimension-channel"))
|
||||
.setBank(new KList<>("dimension-bank")));
|
||||
IrisRegion region = new IrisRegion().setRiverOverride(new IrisRiverOverride()
|
||||
.setAllowSources(false)
|
||||
.setRoutingPolicy(IrisRiverRoutingPolicy.AVOID)
|
||||
.setWidthMultiplier(1.5D)
|
||||
.setChannelBiomes(new KList<>("region-channel")));
|
||||
IrisBiome biome = new IrisBiome().setRiverOverride(new IrisRiverOverride()
|
||||
.setRoutingPolicy(IrisRiverRoutingPolicy.BLOCK)
|
||||
.setWidthMultiplier(0.5D)
|
||||
.setChannelBiomes(new KList<>())
|
||||
.setTerminalMode(IrisRiverTerminalMode.SUPPRESS));
|
||||
|
||||
EffectiveRiverSettings settings = EffectiveRiverSettings.resolve(network, region, biome);
|
||||
|
||||
assertFalse(settings.allowSources());
|
||||
assertEquals(IrisRiverRoutingPolicy.BLOCK, settings.routingPolicy());
|
||||
assertEquals(0.5D, settings.widthMultiplier(), 0D);
|
||||
assertTrue(settings.channelBiomes().isEmpty());
|
||||
assertEquals(List.of("dimension-bank"), settings.bankBiomes());
|
||||
assertEquals(IrisRiverTerminalMode.SUPPRESS, settings.terminalMode());
|
||||
assertTrue(settings.terminalModeOverridden());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inheritsDimensionDefaultsWhenOverridesAreAbsent() {
|
||||
IrisRiverNetwork network = new IrisRiverNetwork()
|
||||
.setBiomes(new IrisRiverBiomes().setDry(new KList<>("dry")));
|
||||
|
||||
EffectiveRiverSettings settings = EffectiveRiverSettings.resolve(network, null, null);
|
||||
|
||||
assertTrue(settings.allowSources());
|
||||
assertEquals(IrisRiverRoutingPolicy.ALLOW, settings.routingPolicy());
|
||||
assertEquals(1D, settings.routingCostMultiplier(), 0D);
|
||||
assertEquals(List.of("dry"), settings.dryBiomes());
|
||||
assertEquals(IrisRiverTerminalMode.DRY_CHANNEL, settings.terminalMode());
|
||||
assertFalse(settings.terminalModeOverridden());
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
+46
@@ -0,0 +1,46 @@
|
||||
package art.arcane.iris.util.common.director.specialhandlers;
|
||||
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.director.exceptions.DirectorParsingException;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class ReachableBiomeHandlerTest {
|
||||
@Test
|
||||
public void exposesAndParsesOnlyReachableBiomes() throws Exception {
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisBiome natural = new IrisBiome();
|
||||
natural.setLoadKey("natural");
|
||||
IrisBiome river = new IrisBiome();
|
||||
river.setLoadKey("river");
|
||||
KList<IrisBiome> reachable = new KList<>();
|
||||
reachable.add(natural);
|
||||
reachable.add(river);
|
||||
when(engine.getAllBiomes()).thenReturn(reachable);
|
||||
|
||||
TestReachableBiomeHandler handler = new TestReachableBiomeHandler(engine);
|
||||
|
||||
assertEquals(reachable, handler.getPossibilities());
|
||||
assertEquals(river, handler.parse("river", false));
|
||||
assertThrows(DirectorParsingException.class, () -> handler.parse("unused", false));
|
||||
}
|
||||
|
||||
private static final class TestReachableBiomeHandler extends ReachableBiomeHandler {
|
||||
private final Engine engine;
|
||||
|
||||
private TestReachableBiomeHandler(Engine engine) {
|
||||
this.engine = engine;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Engine engine() {
|
||||
return engine;
|
||||
}
|
||||
}
|
||||
}
|
||||
+99
@@ -1,20 +1,29 @@
|
||||
package art.arcane.iris.util.project.context;
|
||||
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.hydrology.runtime.IrisHydrologyRuntime;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.util.project.stream.ProceduralStream;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.junit.Test;
|
||||
|
||||
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;
|
||||
import static org.mockito.ArgumentMatchers.anyDouble;
|
||||
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;
|
||||
|
||||
public class ChunkContextPrefillPlanTest {
|
||||
@Test
|
||||
@@ -83,6 +92,73 @@ public class ChunkContextPrefillPlanTest {
|
||||
assertFalse(ChunkContext.shouldPrefillAsync(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hydrologyPreparationCompletesBeforeDependentPrefills() throws Exception {
|
||||
AtomicInteger caveCalls = new AtomicInteger();
|
||||
AtomicInteger heightCalls = new AtomicInteger();
|
||||
AtomicInteger biomeCalls = new AtomicInteger();
|
||||
AtomicInteger rockCalls = new AtomicInteger();
|
||||
AtomicInteger fluidCalls = new AtomicInteger();
|
||||
AtomicInteger regionCalls = new AtomicInteger();
|
||||
CountDownLatch preparationStarted = new CountDownLatch(1);
|
||||
CountDownLatch preparationRelease = new CountDownLatch(1);
|
||||
IrisHydrologyRuntime runtime = mock(IrisHydrologyRuntime.class);
|
||||
doAnswer(invocation -> {
|
||||
preparationStarted.countDown();
|
||||
assertTrue(preparationRelease.await(10L, TimeUnit.SECONDS));
|
||||
return null;
|
||||
}).when(runtime).prepareChunkColumns(32, 48);
|
||||
ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
try {
|
||||
Future<ChunkContext> future = executor.submit(() -> createContext(
|
||||
ChunkContext.PrefillPlan.NO_CAVE,
|
||||
caveCalls,
|
||||
heightCalls,
|
||||
biomeCalls,
|
||||
rockCalls,
|
||||
fluidCalls,
|
||||
regionCalls,
|
||||
runtime
|
||||
));
|
||||
assertTrue(preparationStarted.await(10L, TimeUnit.SECONDS));
|
||||
assertEquals(0, heightCalls.get());
|
||||
assertEquals(0, biomeCalls.get());
|
||||
preparationRelease.countDown();
|
||||
future.get(10L, TimeUnit.SECONDS);
|
||||
} finally {
|
||||
preparationRelease.countDown();
|
||||
executor.shutdownNow();
|
||||
assertTrue(executor.awaitTermination(10L, TimeUnit.SECONDS));
|
||||
}
|
||||
assertEquals(256, heightCalls.get());
|
||||
assertEquals(256, biomeCalls.get());
|
||||
verify(runtime).prepareChunkColumns(32, 48);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hydrologyPreparationIsSkippedWithoutDependentPrefills() {
|
||||
AtomicInteger caveCalls = new AtomicInteger();
|
||||
AtomicInteger heightCalls = new AtomicInteger();
|
||||
AtomicInteger biomeCalls = new AtomicInteger();
|
||||
AtomicInteger rockCalls = new AtomicInteger();
|
||||
AtomicInteger fluidCalls = new AtomicInteger();
|
||||
AtomicInteger regionCalls = new AtomicInteger();
|
||||
IrisHydrologyRuntime runtime = mock(IrisHydrologyRuntime.class);
|
||||
|
||||
createContext(
|
||||
ChunkContext.PrefillPlan.NONE,
|
||||
caveCalls,
|
||||
heightCalls,
|
||||
biomeCalls,
|
||||
rockCalls,
|
||||
fluidCalls,
|
||||
regionCalls,
|
||||
runtime
|
||||
);
|
||||
|
||||
verify(runtime, never()).prepareChunkColumns(32, 48);
|
||||
}
|
||||
|
||||
private ChunkContext createContext(
|
||||
ChunkContext.PrefillPlan prefillPlan,
|
||||
AtomicInteger caveCalls,
|
||||
@@ -91,6 +167,28 @@ public class ChunkContextPrefillPlanTest {
|
||||
AtomicInteger rockCalls,
|
||||
AtomicInteger fluidCalls,
|
||||
AtomicInteger regionCalls
|
||||
) {
|
||||
return createContext(
|
||||
prefillPlan,
|
||||
caveCalls,
|
||||
heightCalls,
|
||||
biomeCalls,
|
||||
rockCalls,
|
||||
fluidCalls,
|
||||
regionCalls,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
private ChunkContext createContext(
|
||||
ChunkContext.PrefillPlan prefillPlan,
|
||||
AtomicInteger caveCalls,
|
||||
AtomicInteger heightCalls,
|
||||
AtomicInteger biomeCalls,
|
||||
AtomicInteger rockCalls,
|
||||
AtomicInteger fluidCalls,
|
||||
AtomicInteger regionCalls,
|
||||
IrisHydrologyRuntime runtime
|
||||
) {
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
|
||||
@@ -151,6 +249,7 @@ public class ChunkContextPrefillPlanTest {
|
||||
doReturn(rockStream).when(complex).getRockStream();
|
||||
doReturn(fluidStream).when(complex).getFluidStream();
|
||||
doReturn(regionStream).when(complex).getRegionStream();
|
||||
doReturn(runtime).when(complex).getHydrologyRuntime();
|
||||
|
||||
return new ChunkContext(32, 48, complex, true, prefillPlan, null);
|
||||
}
|
||||
|
||||
+26
-15
@@ -1,8 +1,7 @@
|
||||
package art.arcane.iris.util.project.matter.slices;
|
||||
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveAction;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveFluidKind;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveAction;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveCell;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
@@ -10,25 +9,27 @@ import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class RiverCaveHydrologyMatterTest {
|
||||
public class HydrologyCaveMatterTest {
|
||||
@Test
|
||||
public void everyActionAndBiomeRoundTrips() throws IOException {
|
||||
RiverCaveHydrologyMatter matter = new RiverCaveHydrologyMatter();
|
||||
for (RiverCaveAction action : RiverCaveAction.values()) {
|
||||
for (RiverCaveFluidKind fluidKind : RiverCaveFluidKind.values()) {
|
||||
RiverCaveHydrology expected = new RiverCaveHydrology(
|
||||
HydrologyCaveMatter matter = new HydrologyCaveMatter();
|
||||
for (HydrologyCaveAction action : HydrologyCaveAction.values()) {
|
||||
for (String profileKey : List.of("default", "underworld", "deep_lava")) {
|
||||
HydrologyCaveCell expected = new HydrologyCaveCell(
|
||||
action,
|
||||
"iris:flooded_grotto",
|
||||
fluidKind
|
||||
profileKey,
|
||||
"iris:flooded_grotto"
|
||||
);
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
matter.writeNode(expected, new DataOutputStream(bytes));
|
||||
|
||||
RiverCaveHydrology actual = matter.readNode(
|
||||
HydrologyCaveCell actual = matter.readNode(
|
||||
new DataInputStream(new ByteArrayInputStream(bytes.toByteArray())));
|
||||
|
||||
assertEquals(expected, actual);
|
||||
@@ -38,8 +39,8 @@ public class RiverCaveHydrologyMatterTest {
|
||||
|
||||
@Test
|
||||
public void emptyBiomeAndInvalidActionRemainUnambiguous() throws IOException {
|
||||
RiverCaveHydrologyMatter matter = new RiverCaveHydrologyMatter();
|
||||
RiverCaveHydrology expected = RiverCaveHydrology.of(RiverCaveAction.DRY_AIR);
|
||||
HydrologyCaveMatter matter = new HydrologyCaveMatter();
|
||||
HydrologyCaveCell expected = HydrologyCaveCell.of(HydrologyCaveAction.DRY_AIR);
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
matter.writeNode(expected, new DataOutputStream(bytes));
|
||||
|
||||
@@ -47,7 +48,17 @@ public class RiverCaveHydrologyMatterTest {
|
||||
new DataInputStream(new ByteArrayInputStream(bytes.toByteArray()))));
|
||||
assertThrows(IOException.class, () -> matter.readNode(
|
||||
new DataInputStream(new ByteArrayInputStream(new byte[]{99}))));
|
||||
assertThrows(IOException.class, () -> matter.readNode(
|
||||
new DataInputStream(new ByteArrayInputStream(new byte[]{1, 99}))));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyCaveCell(
|
||||
HydrologyCaveAction.WET_SOURCE,
|
||||
" ",
|
||||
"iris:flooded_grotto"
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void everyPlannedCellProtectsItsAcceptedVolume() {
|
||||
for (HydrologyCaveAction action : HydrologyCaveAction.values()) {
|
||||
assertTrue(HydrologyCaveCell.of(action).protectsPlacement());
|
||||
}
|
||||
}
|
||||
}
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
package art.arcane.iris.util.project.matter.slices;
|
||||
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.project.matter.IrisMatterSupport;
|
||||
import art.arcane.iris.util.project.matter.PreObjectMatterCell;
|
||||
import art.arcane.volmlib.util.matter.IrisMatter;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.matter.MatterCavern;
|
||||
import art.arcane.volmlib.util.matter.MatterSlice;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Server;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class PreObjectMatterTest {
|
||||
@BeforeClass
|
||||
public static void setUpBukkit() {
|
||||
if (Bukkit.getServer() != null) {
|
||||
return;
|
||||
}
|
||||
Server server = mock(Server.class);
|
||||
doReturn(Logger.getLogger("IrisTest")).when(server).getLogger();
|
||||
doReturn("IrisTestServer").when(server).getName();
|
||||
doReturn("1.0").when(server).getVersion();
|
||||
doReturn("1.0").when(server).getBukkitVersion();
|
||||
doAnswer(invocation -> blockData(invocation.getArgument(0, Material.class).name()
|
||||
.toLowerCase(Locale.ROOT))).when(server).createBlockData(any(Material.class));
|
||||
try {
|
||||
Bukkit.setServer(server);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void capturedNullsAndValuesRoundTripThroughMatter() throws IOException {
|
||||
IrisMatterSupport.ensureRegistered();
|
||||
MatterCavern cavern = new MatterCavern(true, "iris:flooded", (byte) 1);
|
||||
PreObjectMatterCell expected = PreObjectMatterCell.block(null)
|
||||
.captureString("object-owner")
|
||||
.captureCavern(cavern);
|
||||
Matter matter = new IrisMatter(16, 16, 16);
|
||||
matter.<PreObjectMatterCell>slice(PreObjectMatterCell.class).set(2, 3, 4, expected);
|
||||
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
matter.write(bytes);
|
||||
Matter restored = Matter.read(new ByteArrayInputStream(bytes.toByteArray()));
|
||||
PreObjectMatterCell actual = restored.<PreObjectMatterCell>getSlice(PreObjectMatterCell.class)
|
||||
.get(2, 3, 4);
|
||||
|
||||
assertEquals(expected, actual);
|
||||
assertTrue(actual.blockCaptured());
|
||||
assertNull(actual.block());
|
||||
assertEquals(1, restored.<PreObjectMatterCell>getSlice(PreObjectMatterCell.class).getEntryCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void everySupportedNullTombstoneRoundTripsIndependently() throws IOException {
|
||||
IrisMatterSupport.ensureRegistered();
|
||||
Matter matter = new IrisMatter(16, 16, 16);
|
||||
MatterSlice<PreObjectMatterCell> journal = matter.slice(PreObjectMatterCell.class);
|
||||
journal.set(1, 1, 1, PreObjectMatterCell.block(null));
|
||||
journal.set(2, 2, 2, PreObjectMatterCell.string(null));
|
||||
journal.set(3, 3, 3, PreObjectMatterCell.cavern(null));
|
||||
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
matter.write(bytes);
|
||||
Matter restored = Matter.read(new ByteArrayInputStream(bytes.toByteArray()));
|
||||
MatterSlice<PreObjectMatterCell> restoredJournal = restored.getSlice(PreObjectMatterCell.class);
|
||||
|
||||
PreObjectMatterCell block = restoredJournal.get(1, 1, 1);
|
||||
PreObjectMatterCell string = restoredJournal.get(2, 2, 2);
|
||||
PreObjectMatterCell cavern = restoredJournal.get(3, 3, 3);
|
||||
assertTrue(block.blockCaptured());
|
||||
assertNull(block.block());
|
||||
assertTrue(string.stringCaptured());
|
||||
assertNull(string.string());
|
||||
assertTrue(cavern.cavernCaptured());
|
||||
assertNull(cavern.cavern());
|
||||
assertEquals(3, restoredJournal.getEntryCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void blockKeyHasAnExplicitPresenceFlag() throws IOException {
|
||||
PlatformBlockState block = mock(PlatformBlockState.class);
|
||||
when(block.key()).thenReturn("minecraft:stone");
|
||||
PreObjectMatter matter = new PreObjectMatter();
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
matter.writeNode(PreObjectMatterCell.block(block), new DataOutputStream(bytes));
|
||||
|
||||
DataInputStream input = new DataInputStream(new ByteArrayInputStream(bytes.toByteArray()));
|
||||
assertEquals(3, input.readUnsignedByte());
|
||||
assertEquals("minecraft:stone", input.readUTF());
|
||||
assertEquals(0, input.available());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void malformedFlagsAreRejected() {
|
||||
PreObjectMatter matter = new PreObjectMatter();
|
||||
|
||||
assertThrows(IOException.class, () -> matter.readNode(input(0)));
|
||||
assertThrows(IOException.class, () -> matter.readNode(input(1 << 1)));
|
||||
assertThrows(IOException.class, () -> matter.readNode(input(1 << 6)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void capturesRemainIndependentAndFirstWriteWins() {
|
||||
MatterCavern cavern = new MatterCavern(true, "", (byte) 0);
|
||||
PlatformBlockState block = mock(PlatformBlockState.class);
|
||||
PreObjectMatterCell cell = PreObjectMatterCell.string(null)
|
||||
.captureBlock(block)
|
||||
.captureCavern(cavern)
|
||||
.captureString("ignored");
|
||||
|
||||
assertTrue(cell.blockCaptured());
|
||||
assertTrue(cell.stringCaptured());
|
||||
assertTrue(cell.cavernCaptured());
|
||||
assertEquals(block, cell.original(PlatformBlockState.class));
|
||||
assertNull(cell.original(String.class));
|
||||
assertEquals(cavern, cell.original(MatterCavern.class));
|
||||
assertFalse(cell.captures(Integer.class));
|
||||
assertThrows(IllegalArgumentException.class, () -> cell.original(Integer.class));
|
||||
}
|
||||
|
||||
private DataInputStream input(int flags) {
|
||||
return new DataInputStream(new ByteArrayInputStream(new byte[]{(byte) flags}));
|
||||
}
|
||||
|
||||
private static BlockData blockData(String key) {
|
||||
BlockData data = mock(BlockData.class);
|
||||
doReturn("minecraft:" + key).when(data).getAsString();
|
||||
return data;
|
||||
}
|
||||
}
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
package art.arcane.iris.util.project.stream;
|
||||
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.MeteredCache;
|
||||
import art.arcane.iris.engine.framework.PreservationRegistry;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.util.project.stream.interpolation.Interpolated;
|
||||
import art.arcane.iris.util.project.stream.utility.CachedDoubleStream2D;
|
||||
import art.arcane.iris.util.project.stream.utility.CachedStream2D;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
public class ProceduralStreamCacheBypassTest {
|
||||
private PreservationRegistry previousRegistry;
|
||||
private Engine engine;
|
||||
|
||||
@Before
|
||||
public void registerPreservationService() {
|
||||
previousRegistry = IrisServices.getOrNull(PreservationRegistry.class);
|
||||
IrisServices.register(PreservationRegistry.class, new NoOpPreservationRegistry());
|
||||
engine = mock(Engine.class);
|
||||
}
|
||||
|
||||
@After
|
||||
public void restorePreservationService() {
|
||||
if (previousRegistry == null) {
|
||||
IrisServices.remove(PreservationRegistry.class);
|
||||
} else {
|
||||
IrisServices.register(PreservationRegistry.class, previousRegistry);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void normalAccessCachesIntegerQuantizedValues() {
|
||||
AtomicInteger genericCalls = new AtomicInteger();
|
||||
ProceduralStream<Integer> genericSource = ProceduralStream.of(
|
||||
(x, z) -> genericCalls.incrementAndGet(),
|
||||
Interpolated.INT
|
||||
);
|
||||
CachedStream2D<Integer> genericCache = new CachedStream2D<>("generic", engine, genericSource, 8);
|
||||
|
||||
assertEquals(Integer.valueOf(1), genericCache.get(12.9D, -3.9D));
|
||||
assertEquals(Integer.valueOf(1), genericCache.get(12.1D, -3.1D));
|
||||
assertEquals(1, genericCalls.get());
|
||||
assertEquals(256L, genericCache.getSize());
|
||||
|
||||
AtomicInteger doubleCalls = new AtomicInteger();
|
||||
ProceduralStream<Double> doubleSource = ProceduralStream.ofDouble((x, z) -> {
|
||||
doubleCalls.incrementAndGet();
|
||||
return x * 31D + z;
|
||||
});
|
||||
CachedDoubleStream2D doubleCache = new CachedDoubleStream2D("double", engine, doubleSource, 8);
|
||||
|
||||
assertEquals(-1_026D, doubleCache.getDouble(-33.9D, -3.9D), 0D);
|
||||
assertEquals(-1_026D, doubleCache.getDouble(-33.1D, -3.1D), 0D);
|
||||
assertEquals(1, doubleCalls.get());
|
||||
assertEquals(256L, doubleCache.getSize());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bypassMatchesNestedTwoDimensionalSemanticsWithoutGrowingCaches() {
|
||||
DoubleChain baseline = new DoubleChain(engine);
|
||||
DoubleChain bypassed = new DoubleChain(engine);
|
||||
for (int index = 0; index < 32; index++) {
|
||||
double x = index * 64D + 4.875D;
|
||||
double z = index * -96D + 5.625D;
|
||||
double expected = baseline.output.get(x, z);
|
||||
double actual = ProceduralStream.bypass2DCaches(() -> bypassed.output.get(x, z));
|
||||
assertEquals(Double.doubleToLongBits(expected), Double.doubleToLongBits(actual));
|
||||
}
|
||||
|
||||
assertEquals(2_048L, baseline.inner.getSize());
|
||||
assertEquals(2_048L, baseline.converted.getSize());
|
||||
assertEquals(2_048L, baseline.output.getSize());
|
||||
assertEquals(0L, bypassed.inner.getSize());
|
||||
assertEquals(0L, bypassed.converted.getSize());
|
||||
assertEquals(0L, bypassed.output.getSize());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nestedScopesRestoreAfterExceptions() {
|
||||
DoubleChain chain = new DoubleChain(engine);
|
||||
|
||||
ProceduralStream.bypass2DCaches(() -> {
|
||||
assertThrows(IllegalStateException.class, () -> ProceduralStream.bypass2DCaches(() -> {
|
||||
chain.output.getDouble(3_000.75D, -4_000.25D);
|
||||
throw new IllegalStateException("expected");
|
||||
}));
|
||||
chain.output.getDouble(5_000.75D, -6_000.25D);
|
||||
assertEquals(0L, chain.inner.getSize());
|
||||
assertEquals(0L, chain.converted.getSize());
|
||||
assertEquals(0L, chain.output.getSize());
|
||||
return null;
|
||||
});
|
||||
|
||||
chain.output.getDouble(7_000.75D, -8_000.25D);
|
||||
assertEquals(256L, chain.inner.getSize());
|
||||
assertEquals(256L, chain.converted.getSize());
|
||||
assertEquals(256L, chain.output.getSize());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bypassIsThreadLocal() {
|
||||
CachedDoubleStream2D cache = new CachedDoubleStream2D(
|
||||
"thread-local",
|
||||
engine,
|
||||
ProceduralStream.ofDouble((x, z) -> x * 17D + z),
|
||||
8
|
||||
);
|
||||
ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
try {
|
||||
ProceduralStream.bypass2DCaches(() -> {
|
||||
CompletableFuture<Double> otherThread = CompletableFuture.supplyAsync(
|
||||
() -> cache.getDouble(1_024.75D, 2_048.25D),
|
||||
executor
|
||||
);
|
||||
assertEquals(19_456D, otherThread.join(), 0D);
|
||||
assertEquals(256L, cache.getSize());
|
||||
cache.getDouble(4_096.75D, 8_192.25D);
|
||||
assertEquals(256L, cache.getSize());
|
||||
return null;
|
||||
});
|
||||
cache.getDouble(12_288.75D, 16_384.25D);
|
||||
assertEquals(512L, cache.getSize());
|
||||
} finally {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class DoubleChain {
|
||||
private final CachedDoubleStream2D inner;
|
||||
private final CachedStream2D<Double> converted;
|
||||
private final CachedDoubleStream2D output;
|
||||
|
||||
private DoubleChain(Engine engine) {
|
||||
ProceduralStream<Double> source = ProceduralStream.ofDouble(
|
||||
(x, z) -> x * x * 0.125D + z * z * 0.25D + x * z * 0.0625D
|
||||
);
|
||||
inner = new CachedDoubleStream2D("inner", engine, source, 8);
|
||||
ProceduralStream<Double> conversion = inner.convert(value -> value * 1.25D - 7D);
|
||||
converted = new CachedStream2D<>("converted", engine, conversion, 8);
|
||||
output = new CachedDoubleStream2D("output", engine, converted.slope(3), 8);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class NoOpPreservationRegistry implements PreservationRegistry {
|
||||
@Override
|
||||
public void register(Thread thread) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(ExecutorService service) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerCache(MeteredCache cache) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dereference() {
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user