mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-25 21:40:05 +00:00
River bed padding and feature search rings
- surface.bed: falling blocks under and beside river water are replaced by a padding palette unless allowed - Ring order helper for bounded feature searches
This commit is contained in:
1 parent
1ad3a25cb6
commit
639786537b
9 files changed
+238
-11
No files matched your search
@@ -324,6 +324,7 @@ final class PackHydrologyValidator {
|
||||
JSONObject banks = nestedObject(surface, "banks", path, errors);
|
||||
JSONObject flow = nestedObject(surface, "flow", path, errors);
|
||||
JSONObject mouths = nestedObject(surface, "mouths", path, errors);
|
||||
JSONObject bed = nestedObject(surface, "bed", path, errors);
|
||||
|
||||
SourceBudget budget = sources == null
|
||||
? SourceBudget.defaults()
|
||||
@@ -340,6 +341,9 @@ final class PackHydrologyValidator {
|
||||
if (mouths != null) {
|
||||
validateMouths(path + ".mouths", mouths, errors);
|
||||
}
|
||||
if (bed != null) {
|
||||
validateSurfaceBed(path + ".bed", bed, errors);
|
||||
}
|
||||
|
||||
boolean surfaceEnabled = booleanValue(surface, "enabled", true);
|
||||
if (surfaceEnabled && budget.maximumExpectedSources() == 0) {
|
||||
@@ -382,6 +386,39 @@ final class PackHydrologyValidator {
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, flow, "waterfallMinimumDrop", 2, 32, errors);
|
||||
}
|
||||
|
||||
private static void validateSurfaceBed(String path, JSONObject bed, List<String> errors) {
|
||||
PackJsonFieldChecks.validateOptionalBoolean(path, bed, "allowGravityBlocks", errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, bed, "padding", 0, 8, errors);
|
||||
if (!bed.has("paddingPalette")) {
|
||||
return;
|
||||
}
|
||||
Object rawPalette = bed.opt("paddingPalette");
|
||||
if (!(rawPalette instanceof JSONObject palette)) {
|
||||
errors.add(path + ".paddingPalette must be an object.");
|
||||
return;
|
||||
}
|
||||
Object rawEntries = palette.opt("palette");
|
||||
if (!(rawEntries instanceof JSONArray entries) || entries.length() == 0) {
|
||||
errors.add(path + ".paddingPalette.palette must contain at least one solid block.");
|
||||
return;
|
||||
}
|
||||
for (int index = 0; index < entries.length(); index++) {
|
||||
String entryPath = path + ".paddingPalette.palette[" + index + "]";
|
||||
JSONObject entry = entries.optJSONObject(index);
|
||||
if (entry == null) {
|
||||
errors.add(entryPath + " must be an object.");
|
||||
continue;
|
||||
}
|
||||
Object rawBlock = entry.opt("block");
|
||||
if (!(rawBlock instanceof String block) || block.isBlank()) {
|
||||
errors.add(entryPath + ".block must name a solid block.");
|
||||
} else if (!definitelyNotFluid(block)) {
|
||||
errors.add(entryPath + ".block must not be a fluid.");
|
||||
}
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(entryPath, entry, "weight", 1, 64, errors);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateMouths(String path, JSONObject mouths, List<String> errors) {
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, mouths, "flareRatio", 1D, 4D, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, mouths, "maximumOceanApron", 0, 32, errors);
|
||||
|
||||
@@ -26,6 +26,8 @@ import art.arcane.iris.engine.UpperDimensionContext;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisProceduralBlocks;
|
||||
import art.arcane.iris.engine.object.IrisSurfaceRiverBedConfig;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisOreGenerator;
|
||||
import art.arcane.iris.engine.object.IrisOreGeneratorBounds;
|
||||
@@ -105,6 +107,10 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
|
||||
IrisOreGeneratorBounds dimensionUndergroundOreBounds = hideOres ? IrisOreGeneratorBounds.EMPTY : dimension.getUndergroundOreGeneratorBounds();
|
||||
boolean exposeCutStrata = dimension.getHydrology() != null
|
||||
&& dimension.getHydrology().getRivers().getSurface().getBanks().isExposeCutStrata();
|
||||
IrisSurfaceRiverBedConfig riverBed = dimension.getHydrology() == null
|
||||
? null
|
||||
: dimension.getHydrology().getRivers().getSurface().getBed();
|
||||
boolean padRiverBed = riverBed != null && !riverBed.isAllowGravityBlocks();
|
||||
|
||||
for (int zf = 0; zf < chunkDepth; zf++) {
|
||||
int realZ = zf + z;
|
||||
@@ -136,6 +142,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
|
||||
&& !hydrologyTerrain.channel()
|
||||
? Math.max(0, hydrology.naturalHeight() - he)
|
||||
: 0;
|
||||
boolean riverOwned = padRiverBed && hydrologyTerrain != null && hydrologyTerrain.terrainOwned();
|
||||
PlatformBlockState fluid = hydrologyFluid == null
|
||||
? complex.resolveSurfaceFluid(realX, realZ)
|
||||
: complex.resolveHydrologyFluid(hydrologyFluid.profileKey(), realX, realZ);
|
||||
@@ -203,7 +210,11 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
|
||||
}
|
||||
|
||||
if (blocks.hasIndex(depth + cut)) {
|
||||
h.setRaw(xf, i, zf, blocks.get(depth + cut));
|
||||
PlatformBlockState layerBlock = blocks.get(depth + cut);
|
||||
if (riverOwned && depth <= riverBed.getPadding() && IrisProceduralBlocks.isGravityAffected(layerBlock)) {
|
||||
layerBlock = riverBed.getPaddingPalette().get(localRng, realX, i, realZ, data);
|
||||
}
|
||||
h.setRaw(xf, i, zf, layerBlock);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
package art.arcane.iris.engine.hydrology.runtime;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.HydrologyTileKey;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Ring order for feature searches: tiles are visited outward from the origin tile so a search can
|
||||
* stop as soon as no unvisited tile could hold a nearer feature, instead of planning the whole box.
|
||||
*/
|
||||
final class HydrologyFeatureSearch {
|
||||
private HydrologyFeatureSearch() {
|
||||
}
|
||||
|
||||
/** Tiles at exactly Chebyshev distance {@code radius} from the origin tile, in row-major order. */
|
||||
static List<HydrologyTileKey> ring(int originTileX, int originTileZ, int radius) {
|
||||
if (radius < 0) {
|
||||
throw new IllegalArgumentException("radius must not be negative.");
|
||||
}
|
||||
if (radius == 0) {
|
||||
return List.of(new HydrologyTileKey(originTileX, originTileZ));
|
||||
}
|
||||
ArrayList<HydrologyTileKey> keys = new ArrayList<>(radius * 8);
|
||||
for (int tileZ = originTileZ - radius; tileZ <= originTileZ + radius; tileZ++) {
|
||||
boolean edgeRow = tileZ == originTileZ - radius || tileZ == originTileZ + radius;
|
||||
for (int tileX = originTileX - radius; tileX <= originTileX + radius; tileX++) {
|
||||
boolean edgeColumn = tileX == originTileX - radius || tileX == originTileX + radius;
|
||||
if (edgeRow || edgeColumn) {
|
||||
keys.add(new HydrologyTileKey(tileX, tileZ));
|
||||
}
|
||||
}
|
||||
}
|
||||
return List.copyOf(keys);
|
||||
}
|
||||
|
||||
/** Number of rings needed so every tile that can publish within {@code maximumDistance} is visited. */
|
||||
static int ringLimit(int maximumDistance, int tileSize, int publicationRadius) {
|
||||
if (maximumDistance < 0 || tileSize <= 0 || publicationRadius < 0) {
|
||||
throw new IllegalArgumentException("Hydrology feature search bounds are invalid.");
|
||||
}
|
||||
long reach = (long) maximumDistance + publicationRadius;
|
||||
return Math.toIntExact((reach + tileSize - 1) / tileSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Smallest block distance a feature published by a ring-{@code radius} tile can have from the
|
||||
* origin: the ring starts {@code radius - 1} tiles away and a tile's footprint reaches
|
||||
* {@code publicationRadius} beyond its bounds.
|
||||
*/
|
||||
static long lowerBound(int radius, int tileSize, int publicationRadius) {
|
||||
if (radius <= 0) {
|
||||
return 0L;
|
||||
}
|
||||
return Math.max(0L, (long) (radius - 1) * tileSize - publicationRadius);
|
||||
}
|
||||
}
|
||||
@@ -988,16 +988,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
}
|
||||
|
||||
static boolean isGravityAffected(PlatformBlockState state) {
|
||||
if (state == null) {
|
||||
return false;
|
||||
}
|
||||
String key = IrisProceduralBlocks.materialKey(state);
|
||||
return key.equals("minecraft:sand")
|
||||
|| key.equals("minecraft:red_sand")
|
||||
|| key.equals("minecraft:gravel")
|
||||
|| key.equals("minecraft:suspicious_sand")
|
||||
|| key.equals("minecraft:suspicious_gravel")
|
||||
|| key.endsWith("_concrete_powder");
|
||||
return IrisProceduralBlocks.isGravityAffected(state);
|
||||
}
|
||||
|
||||
private IrisBiome resolveCaveBiome(Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache, int x, int y, int z, IrisDimensionCarvingResolver.State resolverState) {
|
||||
|
||||
@@ -74,6 +74,20 @@ public final class IrisProceduralBlocks {
|
||||
return key.indexOf(',', valueStart) >= 0 || key.indexOf(']', valueStart) >= 0;
|
||||
}
|
||||
|
||||
/** Blocks that fall when unsupported: sand, gravel and concrete powder. */
|
||||
public static boolean isGravityAffected(PlatformBlockState state) {
|
||||
if (state == null) {
|
||||
return false;
|
||||
}
|
||||
String key = materialKey(state);
|
||||
return key.equals("minecraft:sand")
|
||||
|| key.equals("minecraft:red_sand")
|
||||
|| key.equals("minecraft:gravel")
|
||||
|| key.equals("minecraft:suspicious_sand")
|
||||
|| key.equals("minecraft:suspicious_gravel")
|
||||
|| key.endsWith("_concrete_powder");
|
||||
}
|
||||
|
||||
public static String materialKey(PlatformBlockState state) {
|
||||
String memoized = state.materialKey();
|
||||
if (memoized != null) {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import art.arcane.iris.engine.object.annotations.MaxNumber;
|
||||
import art.arcane.iris.engine.object.annotations.MinNumber;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@Desc("Controls the material under and beside surface river water.")
|
||||
@Data
|
||||
public class IrisSurfaceRiverBedConfig {
|
||||
@Desc("Allow sand, gravel and other falling blocks in the bed, shore and eroded banks. When false they are replaced by the padding palette.")
|
||||
private boolean allowGravityBlocks = false;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(8)
|
||||
@Desc("Blocks below the bed that are also kept free of falling blocks, so nothing under the water can collapse.")
|
||||
private int padding = 2;
|
||||
|
||||
@Desc("Blocks used in place of falling blocks in the bed, padding, shore and banks.")
|
||||
private IrisMaterialPalette paddingPalette = new IrisMaterialPalette().qclear().qadd("clay").qadd("dirt");
|
||||
}
|
||||
@@ -21,6 +21,8 @@ public class IrisSurfaceRiverConfig {
|
||||
|
||||
@Desc("Lip, shore band and eroded valley around the channel.")
|
||||
private IrisSurfaceRiverBankConfig banks = new IrisSurfaceRiverBankConfig();
|
||||
@Desc("Material under and beside the water: falling-block replacement and padding.")
|
||||
private IrisSurfaceRiverBedConfig bed = new IrisSurfaceRiverBedConfig();
|
||||
|
||||
@Desc("Cascade and waterfall thresholds.")
|
||||
private IrisSurfaceRiverFlowConfig flow = new IrisSurfaceRiverFlowConfig();
|
||||
|
||||
@@ -113,6 +113,49 @@ public class PackHydrologyValidatorTest {
|
||||
assertTrue(result.errors().toString(), result.errors().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bedRulesRejectBadPaddingAndFluidPaddingBlocks() throws Exception {
|
||||
File pack = pack(riversDimension("""
|
||||
{
|
||||
"enabled": true,
|
||||
"routing": {"tileSize": 1024, "sampleSpacing": 64, "oceanOutlets": true},
|
||||
"surface": {
|
||||
"bed": {
|
||||
"allowGravityBlocks": false,
|
||||
"padding": 9,
|
||||
"paddingPalette": {"palette": [{"block": "minecraft:water"}]}
|
||||
}
|
||||
}
|
||||
}
|
||||
""", ""));
|
||||
|
||||
PackHydrologyValidator.Validation result = validate(pack);
|
||||
|
||||
assertContains(result.errors(), "padding must be at most 8");
|
||||
assertContains(result.errors(), "paddingPalette.palette[0].block must not be a fluid");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bedRulesAcceptSolidPaddingBlocks() throws Exception {
|
||||
File pack = pack(riversDimension("""
|
||||
{
|
||||
"enabled": true,
|
||||
"routing": {"tileSize": 1024, "sampleSpacing": 64, "oceanOutlets": true},
|
||||
"surface": {
|
||||
"bed": {
|
||||
"allowGravityBlocks": true,
|
||||
"padding": 0,
|
||||
"paddingPalette": {"palette": [{"block": "minecraft:clay"}, {"block": "minecraft:dirt", "weight": 2}]}
|
||||
}
|
||||
}
|
||||
}
|
||||
""", ""));
|
||||
|
||||
PackHydrologyValidator.Validation result = validate(pack);
|
||||
|
||||
assertTrue(result.errors().toString(), result.errors().stream().noneMatch((String error) -> error.contains(".bed")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsMouthLevelingDistanceBeyondTheRouteLength() throws Exception {
|
||||
File pack = pack(riversDimension("""
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package art.arcane.iris.engine.hydrology.runtime;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.HydrologyTileKey;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class HydrologyFeatureSearchTest {
|
||||
@Test
|
||||
public void ringZeroIsTheOriginTile() {
|
||||
assertEquals(List.of(new HydrologyTileKey(3, -2)), HydrologyFeatureSearch.ring(3, -2, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ringsCoverEveryTileAtExactlyTheirChebyshevDistance() {
|
||||
HashSet<HydrologyTileKey> seen = new HashSet<>();
|
||||
for (int radius = 0; radius <= 3; radius++) {
|
||||
List<HydrologyTileKey> ring = HydrologyFeatureSearch.ring(1, 1, radius);
|
||||
assertEquals(radius == 0 ? 1 : radius * 8, ring.size());
|
||||
for (HydrologyTileKey key : ring) {
|
||||
int distance = Math.max(Math.abs(key.tileX() - 1), Math.abs(key.tileZ() - 1));
|
||||
assertEquals(radius, distance);
|
||||
assertTrue(seen.add(key));
|
||||
}
|
||||
}
|
||||
assertEquals(49, seen.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ringLimitCoversTheRequestedDistancePlusPublication() {
|
||||
assertEquals(9, HydrologyFeatureSearch.ringLimit(8192, 1024, 358));
|
||||
assertEquals(1, HydrologyFeatureSearch.ringLimit(0, 1024, 358));
|
||||
assertEquals(0, HydrologyFeatureSearch.ringLimit(0, 1024, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lowerBoundGrowsWithTheRingAndNeverGoesNegative() {
|
||||
assertEquals(0L, HydrologyFeatureSearch.lowerBound(0, 1024, 358));
|
||||
assertEquals(0L, HydrologyFeatureSearch.lowerBound(1, 1024, 358));
|
||||
assertEquals(666L, HydrologyFeatureSearch.lowerBound(2, 1024, 358));
|
||||
assertEquals(1690L, HydrologyFeatureSearch.lowerBound(3, 1024, 358));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user