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:
Brian Neumann-Fopiano committed 2026-09-01 18:28:00 -04:00
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);
}
@@ -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
@@ -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();
}
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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());
}
}
@@ -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;
}
}
@@ -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");
@@ -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) {
@@ -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);
}
}
@@ -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);
}
}
@@ -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
);
}
}
@@ -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"
);
}
}
@@ -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);
}
}
@@ -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());
}
}
}
@@ -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());
}
}
@@ -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;
}
}
@@ -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));
}
}
@@ -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
);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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
@@ -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));
@@ -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() {
@@ -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);
}
}
@@ -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 {
@@ -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) {
@@ -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) {
}
}
@@ -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));
}
}
@@ -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) {
}
}
@@ -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));
}
}
@@ -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) {
@@ -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
));
}
@@ -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]);
}
@@ -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;
@@ -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);
@@ -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());
});
}
}
}
@@ -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());
});
}
}
}
@@ -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
@@ -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();
@@ -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());
}
}
@@ -56,7 +56,6 @@ public class BukkitChunkGeneratorInitialSpawnTest {
chunkFuture.complete(chunk);
assertNotNull(regionTask.get());
assertFalse(readiness.isDone());
regionTask.get().run();
readiness.get(5L, TimeUnit.SECONDS);
}
@@ -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})
);
}
}
@@ -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);
}
}
}
@@ -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
@@ -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;
}
}
}
@@ -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);
}
@@ -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());
}
}
}
@@ -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;
}
}
@@ -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() {
}
}
}