diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/config/IrisHydrology.java b/core/src/main/java/art/arcane/iris/generation/hydrology/config/IrisHydrology.java index 153e7e7d5..fac00354c 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/config/IrisHydrology.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/config/IrisHydrology.java @@ -22,6 +22,9 @@ public class IrisHydrology { @Description("Fraction of eligible water features to keep after local placement probabilities. 0.5 keeps half the candidate river sources, ponds, lakes and underground water features without changing their configured sizes.") private double density = 1D; + @Description("Wet and dry zones that decide where streams and ponds gather. Within a wet zone the configured density applies unchanged.") + private IrisHydrologyClimate climate = new IrisHydrologyClimate(); + @Description("How water drains across the terrain before rivers and ponds are placed.") private IrisHydrologyDrainage drainage = new IrisHydrologyDrainage(); diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/config/IrisHydrologyClimate.java b/core/src/main/java/art/arcane/iris/generation/hydrology/config/IrisHydrologyClimate.java new file mode 100644 index 000000000..2ea21e9ae --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/config/IrisHydrologyClimate.java @@ -0,0 +1,37 @@ +package art.arcane.iris.generation.hydrology.config; + +import art.arcane.iris.generation.noise.IrisGeneratorStyle; +import art.arcane.iris.generation.noise.NoiseStyle; +import art.arcane.iris.pack.schema.annotation.MaxNumber; +import art.arcane.iris.pack.schema.annotation.MinNumber; +import art.arcane.volmlib.util.documentation.Description; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Accessors(chain = true) +@NoArgsConstructor +@Description("Wet and dry zones across the dimension. A noise field splits the land into zones: wet zones keep the rainfall and pond density of their biome, dry zones lose them. Streams and ponds then cluster in the wet zones instead of reaching every hill, and a river that rises in a wet zone still flows on through dry land.") +@Data +public class IrisHydrologyClimate { + @Description("Turns wet and dry zones on. When off, rainfall and pond density follow the hydrology policies alone.") + private boolean enabled = false; + + @Description("Noise that lays out the zones. Land where its value is below dryness is dry. A larger zoom makes larger zones.") + private IrisGeneratorStyle style = NoiseStyle.SIMPLEX.style().zoomed(24D); + + @MinNumber(0) + @MaxNumber(1) + @Description("Noise value below which land is dry. 0 keeps all land wet, and higher values dry out more of it.") + private double dryness = 0.5D; + + @MinNumber(0) + @MaxNumber(1) + @Description("Width of the noise band over which rainfall fades from dry to wet. 0 gives sharp zone edges.") + private double transition = 0.1D; + + @MinNumber(0) + @MaxNumber(1) + @Description("Share of the area's rainfall and pond density kept in dry zones. 0 lets dry zones start no streams or ponds of their own.") + private double dryRainfall = 0D; +} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/config/IrisHydrologyDeformation.java b/core/src/main/java/art/arcane/iris/generation/hydrology/config/IrisHydrologyDeformation.java index 6b1b4cc61..fd80ce729 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/config/IrisHydrologyDeformation.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/config/IrisHydrologyDeformation.java @@ -24,12 +24,12 @@ public class IrisHydrologyDeformation { @MinNumber(0) @MaxNumber(16) - @Description("How much steeper banks become on steep terrain.") + @Description("How much steeper a bank stays where the natural ground between it and the water is steep.") private double bankSteepFactor = 3.5D; @MinNumber(0) @MaxNumber(2) - @Description("Strength of the noise that roughens bank edges.") + @Description("Strength of the smooth noise that roughens bank edges.") private double bankNoise = 0.2D; @Description("Keeps caves from opening under water, so rivers and ponds never drain into them.") diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/config/IrisHydrologyPolicy.java b/core/src/main/java/art/arcane/iris/generation/hydrology/config/IrisHydrologyPolicy.java index 325e0d12c..6f3924f9d 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/config/IrisHydrologyPolicy.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/config/IrisHydrologyPolicy.java @@ -31,13 +31,16 @@ public class IrisHydrologyPolicy { @Description("Multiplier on how many ponds form in this area. With no value set anywhere it is 1.") private Double pondDensity = null; + @Description("Whether the dimension's wet and dry zones apply in this area. false keeps this area's full rainfall and pond density even inside a dry zone. With no value set anywhere they apply.") + private Boolean climateZones = null; + @Description("Whether streams and rivers may form in this area. With no value set anywhere they are allowed.") private IrisHydrologyPermission streams = null; @Description("Whether ponds may form in this area. With no value set anywhere they are allowed.") private IrisHydrologyPermission ponds = null; - @Description("Name of a fluid profile that fills ponds and lakes whose floor lies in this area, instead of the ponds' own fluid. Channels always use the fluid of their tier.") + @Description("Name of a fluid profile that fills the water in this area instead of its usual fluid: ponds and lakes whose floor lies here, and every stretch of stream or river that flows through here.") private String fluid = null; @RegistryListResource(IrisBiome.class) diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/engine/HydrologyRuntime.java b/core/src/main/java/art/arcane/iris/generation/hydrology/engine/HydrologyRuntime.java index c2536f5b8..5024fbcfd 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/engine/HydrologyRuntime.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/engine/HydrologyRuntime.java @@ -14,8 +14,10 @@ import art.arcane.iris.generation.hydrology.plan.WindowKey; import art.arcane.iris.generation.hydrology.query.HydrologyFeatureQuery; import art.arcane.iris.generation.hydrology.query.HydrologyFeatureRef; import art.arcane.iris.generation.hydrology.query.HydrologyFeatureType; +import art.arcane.iris.generation.hydrology.raster.ChannelFluids; import art.arcane.iris.generation.hydrology.raster.ChunkRasterizer; import art.arcane.iris.generation.hydrology.raster.ChunkWaterRaster; +import art.arcane.iris.generation.hydrology.raster.NaturalHeights; import art.arcane.iris.generation.hydrology.underground.SeaInlet; import art.arcane.iris.generation.hydrology.underground.SeaInletRasterizer; import art.arcane.iris.generation.hydrology.water.HydrologyTier; @@ -51,7 +53,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; import java.util.function.BooleanSupplier; import java.util.function.Function; -import java.util.function.IntBinaryOperator; import java.util.function.LongSupplier; import java.util.function.Predicate; import java.util.function.Supplier; @@ -68,7 +69,7 @@ public final class HydrologyRuntime { private final HydrologySettings settings; private final HydrologyPlanCache cache; private final ChunkRasterizer rasterizer; - private final IntBinaryOperator naturalHeight; + private final NaturalHeights naturalHeight; private final BooleanSupplier mainThread; private final RasterLru rasters; private final ThreadLocal last; @@ -109,17 +110,17 @@ public final class HydrologyRuntime { this.caveFluidPolicies = false; } - public HydrologyRuntime(HydrologySettings settings, HydrologyPlanCache cache, IntBinaryOperator naturalHeight, + public HydrologyRuntime(HydrologySettings settings, HydrologyPlanCache cache, NaturalHeights naturalHeight, BooleanSupplier mainThread, LongSupplier ticker) { - this(settings, cache, naturalHeight, mainThread, ticker, HydrologyBinding.UNBOUND, identity -> null, false); + this(settings, cache, naturalHeight, ChannelFluids.TIER, mainThread, ticker, HydrologyBinding.UNBOUND, identity -> null, false); } - private HydrologyRuntime(HydrologySettings settings, HydrologyPlanCache cache, IntBinaryOperator naturalHeight, - BooleanSupplier mainThread, LongSupplier ticker, HydrologyBinding binding, + private HydrologyRuntime(HydrologySettings settings, HydrologyPlanCache cache, NaturalHeights naturalHeight, + ChannelFluids channelFluids, BooleanSupplier mainThread, LongSupplier ticker, HydrologyBinding binding, Function stores, boolean caveFluidPolicies) { this.settings = Objects.requireNonNull(settings, "settings"); this.cache = Objects.requireNonNull(cache, "cache"); - this.rasterizer = new ChunkRasterizer(settings); + this.rasterizer = new ChunkRasterizer(settings, channelFluids); this.naturalHeight = Objects.requireNonNull(naturalHeight, "naturalHeight"); this.mainThread = Objects.requireNonNull(mainThread, "mainThread"); this.rasters = new RasterLru(); @@ -160,7 +161,7 @@ public final class HydrologyRuntime { HydrologyPlanCache cache = HydrologyPlanCache.create(settings, planners.regions(), planners.windows(), MultiBurst.hydrology, mainThread); String dimensionKey = dimension.getLoadKey(); IrisWorld world = engine.getWorld(); - return new HydrologyRuntime(settings, cache, source::naturalHeight, mainThread, System::nanoTime, binding, + return new HydrologyRuntime(settings, cache, source::naturalSurface, source, mainThread, System::nanoTime, binding, identity -> store(world, dimensionKey, settings, identity), caveFluidPolicies); } diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/engine/IrisHydrologyTerrainSource.java b/core/src/main/java/art/arcane/iris/generation/hydrology/engine/IrisHydrologyTerrainSource.java index 2cd730752..86a43c70c 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/engine/IrisHydrologyTerrainSource.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/engine/IrisHydrologyTerrainSource.java @@ -7,11 +7,13 @@ import art.arcane.iris.generation.hydrology.config.IrisHydrologyPolicy; import art.arcane.iris.generation.hydrology.grid.HydrologySampleBuffer; import art.arcane.iris.generation.hydrology.grid.HydrologyTerrainSource; import art.arcane.iris.generation.hydrology.plan.HydrologySettings; +import art.arcane.iris.generation.hydrology.raster.ChannelFluids; import art.arcane.iris.generation.runtime.IrisComplex; import art.arcane.iris.generation.terrain.InferredType; import art.arcane.iris.generation.terrain.IrisDimension; import art.arcane.iris.generation.terrain.IrisRegion; import art.arcane.iris.studio.generation.BiomeBuffetLayout; +import art.arcane.volmlib.util.noise.CNG; import art.arcane.volmlib.util.stream.ProceduralStream; import java.util.ArrayList; @@ -25,9 +27,10 @@ import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; -public final class IrisHydrologyTerrainSource implements HydrologyTerrainSource { +public final class IrisHydrologyTerrainSource implements HydrologyTerrainSource, ChannelFluids { private static final int MINIMUM_SLICE = 256; private static final int POLICY_CAPACITY = 4096; + private static final long SALT_CLIMATE = 0x434c494d415445L; private final IrisComplex complex; private final IrisDimension dimension; @@ -40,6 +43,8 @@ public final class IrisHydrologyTerrainSource implements HydrologyTerrainSource private final BiomeBuffetLayout buffet; private final IrisBiome focus; private final HydrologyBinding binding; + private final HydrologySettings.Climate climate; + private final CNG wetness; private final ConcurrentHashMap policies = new ConcurrentHashMap<>(); public IrisHydrologyTerrainSource(IrisComplex complex, IrisDimension dimension, HydrologySettings settings, ExecutorService pool, @@ -55,6 +60,8 @@ public final class IrisHydrologyTerrainSource implements HydrologyTerrainSource this.buffet = complex.getBiomeBuffet(); this.focus = complex.getFocusBiome(); this.binding = Objects.requireNonNull(binding, "binding"); + this.climate = settings.climate(); + this.wetness = climate.enabled() ? climate.style().create(settings.seed() ^ SALT_CLIMATE, complex.getData()) : null; } @Override @@ -73,8 +80,13 @@ public final class IrisHydrologyTerrainSource implements HydrologyTerrainSource } } - public int naturalHeight(int x, int z) { - return complex.naturalTrueHeight(x, z); + public double naturalSurface(int x, int z) { + return complex.naturalHeight(x, z); + } + + @Override + public String channelFluid(int x, int z) { + return policy(biomes.get(x, z), regions.get(x, z)).fluid(); } @Override @@ -150,12 +162,17 @@ public final class IrisHydrologyTerrainSource implements HydrologyTerrainSource Policy policy = policy(biomes.get(x, z), regions.get(x, z)); out.height()[index] = (float) height; out.ocean()[index] = StrictMath.round(height) < settings.seaLevel() && oceanIntent(x, z); - out.rainfall()[index] = policy.rainfall(); - out.pondDensity()[index] = policy.pondDensity(); + float wet = policy.climateZones() ? wetness(x, z) : 1f; + out.rainfall()[index] = policy.rainfall() * wet; + out.pondDensity()[index] = policy.pondDensity() * wet; out.permission()[index] = policy.permission(); out.fluidProfile()[index] = policy.fluid(); } + private float wetness(int x, int z) { + return wetness == null ? 1f : (float) climate.wetness(wetness.noise(x, z)); + } + private boolean oceanIntent(int x, int z) { BiomeBuffetLayout.Cell cell = buffet == null ? null : buffet.terrain(x, z); IrisBiome focused = cell == null ? focus : cell.biome(); @@ -183,13 +200,14 @@ public final class IrisHydrologyTerrainSource implements HydrologyTerrainSource IrisHydrologyPermission streams = inherited(biome, region, IrisHydrologyPolicy::getStreams, IrisHydrologyPermission.ALLOW); IrisHydrologyPermission ponds = inherited(biome, region, IrisHydrologyPolicy::getPonds, IrisHydrologyPermission.ALLOW); String fluid = inherited(biome, region, IrisHydrologyPolicy::getFluid, null); + boolean climateZones = inherited(biome, region, IrisHydrologyPolicy::getClimateZones, Boolean.TRUE); requireMultiplier("rainfall", rainfall, biome, region); requireMultiplier("pondDensity", pondDensity, biome, region); if (fluid != null && !settings.fluids().containsKey(fluid)) { throw new IllegalArgumentException("Hydrology policy for " + where(biome, region) + " names fluid '" + fluid + "', which is not one of the hydrology fluids " + settings.fluids().keySet()); } - return new Policy((float) rainfall, (float) pondDensity, permission(streams, ponds), fluid); + return new Policy((float) rainfall, (float) pondDensity, permission(streams, ponds), fluid, climateZones); } private T inherited(IrisBiome biome, IrisRegion region, Function field, T fallback) { @@ -214,7 +232,7 @@ public final class IrisHydrologyTerrainSource implements HydrologyTerrainSource return "biome " + (biome == null ? "none" : biome.getLoadKey()) + " in region " + (region == null ? "none" : region.getLoadKey()); } - private record Policy(float rainfall, float pondDensity, byte permission, String fluid) { + private record Policy(float rainfall, float pondDensity, byte permission, String fluid, boolean climateZones) { } private record PolicyKey(IrisBiome biome, IrisRegion region) { diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/plan/HydrologySettings.java b/core/src/main/java/art/arcane/iris/generation/hydrology/plan/HydrologySettings.java index 3f5c9b07f..b47cfdc5c 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/plan/HydrologySettings.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/plan/HydrologySettings.java @@ -5,6 +5,7 @@ import art.arcane.iris.generation.decoration.IrisDecorationPart; import art.arcane.iris.generation.decoration.IrisDecorator; import art.arcane.iris.generation.hydrology.config.IrisCavePools; import art.arcane.iris.generation.hydrology.config.IrisFluidProfile; +import art.arcane.iris.generation.hydrology.config.IrisHydrologyClimate; import art.arcane.iris.generation.hydrology.config.IrisHydrology; import art.arcane.iris.generation.hydrology.config.IrisHydrologyDeformation; import art.arcane.iris.generation.hydrology.config.IrisHydrologyDrainage; @@ -20,6 +21,7 @@ import art.arcane.iris.generation.hydrology.config.IrisPondShore; import art.arcane.iris.generation.hydrology.config.IrisSeaInlets; import art.arcane.iris.generation.hydrology.underground.SeaInletRasterizer; import art.arcane.iris.generation.hydrology.water.HydrologyTier; +import art.arcane.iris.generation.noise.IrisGeneratorStyle; import art.arcane.iris.generation.terrain.IrisMaterialPalette; import com.google.gson.Gson; @@ -51,6 +53,7 @@ public final class HydrologySettings { private final int maxY; private final boolean enabled; private final float density; + private final Climate climate; private final Drainage drainage; private final List tiers; private final Ponds ponds; @@ -72,6 +75,7 @@ public final class HydrologySettings { throw new IllegalArgumentException("Hydrology density must be within 0..1, got " + config.getDensity()); } this.density = (float) config.getDensity(); + this.climate = climate(config.getClimate()); this.drainage = drainage(config.getDrainage()); this.tiers = tiers(config.getTiers()); this.ponds = ponds(config.getPonds()); @@ -135,6 +139,10 @@ public final class HydrologySettings { return density; } + public Climate climate() { + return climate; + } + public Drainage drainage() { return drainage; } @@ -167,6 +175,22 @@ public final class HydrologySettings { return persist; } + private static Climate climate(IrisHydrologyClimate config) { + Objects.requireNonNull(config, "Hydrology climate is missing"); + IrisGeneratorStyle style = Objects.requireNonNull(config.getStyle(), "Hydrology climate.style is missing"); + float dryness = unit(config.getDryness(), "climate.dryness"); + float transition = unit(config.getTransition(), "climate.transition"); + float dryRainfall = unit(config.getDryRainfall(), "climate.dryRainfall"); + return new Climate(config.isEnabled(), style, dryness, transition, dryRainfall); + } + + private static float unit(double value, String field) { + if (!Double.isFinite(value) || value < 0D || value > 1D) { + throw new IllegalArgumentException("Hydrology " + field + " must be within 0..1, got " + value); + } + return (float) value; + } + private static Drainage drainage(IrisHydrologyDrainage config) { return new Drainage((float) config.getFillEpsilon(), config.getBreachDepth(), config.isWetnessThinning(), (float) config.getMinimumHeadKeep(), (float) config.getHeadRelief(), (float) config.getSaturationCatchment()); @@ -468,6 +492,7 @@ public final class HydrologySettings { private long computeFingerprint() { Fingerprint hash = new Fingerprint().add(seed).add(seaLevel).add(minY).add(maxY).add(enabled) .add(density).add(prefetchRings).add(persist); + addClimate(hash); addDrainage(hash); addTiers(hash); addPonds(hash); @@ -477,6 +502,12 @@ public final class HydrologySettings { return hash.value(); } + private void addClimate(Fingerprint hash) { + if (climate.enabled()) { + hash.add(true).add(PALETTE_JSON.toJson(climate.style())).add(climate.dryness()).add(climate.transition()).add(climate.dryRainfall()); + } + } + private void addDrainage(Fingerprint hash) { hash.add(drainage.fillEpsilon()).add(drainage.breachDepth()).add(drainage.wetnessThinning()) .add(drainage.minimumHeadKeep()).add(drainage.headRelief()).add(drainage.saturationCatchment()); @@ -535,6 +566,23 @@ public final class HydrologySettings { public record IntRange(int min, int max) { } + public record Climate(boolean enabled, IrisGeneratorStyle style, float dryness, float transition, float dryRainfall) { + public double wetness(double noise) { + if (!enabled || dryness <= 0f) { + return 1D; + } + double half = 0.5D * transition; + double share; + if (half <= 0D) { + share = noise < dryness ? 0D : 1D; + } else { + double t = Math.clamp((noise - dryness + half) / transition, 0D, 1D); + share = t * t * (3D - 2D * t); + } + return dryRainfall + (1D - dryRainfall) * share; + } + } + public record Drainage(float fillEpsilon, int breachDepth, boolean wetnessThinning, float minimumHeadKeep, float headRelief, float saturationCatchment) { } diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/raster/BedProfile.java b/core/src/main/java/art/arcane/iris/generation/hydrology/raster/BedProfile.java index 1cb9e16d6..0034d2736 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/raster/BedProfile.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/raster/BedProfile.java @@ -6,6 +6,7 @@ final class BedProfile { private static final long SALT_BROAD = 0x42454442524f4144L; private static final long SALT_DETAIL = 0x4245444445544149L; private static final long SALT_TEXTURE = 0x4245445445585455L; + private static final long SALT_ROUGHNESS = 0x42414e4b524f5547L; private BedProfile() { } @@ -18,6 +19,10 @@ final class BedProfile { return 0.35D * noise(seed, SALT_TEXTURE, x, z, 2); } + static double roughness(long seed, int x, int z) { + return noise(seed, SALT_ROUGHNESS, x, z, 4) - 0.5D; + } + static double position(double u, double variation) { double bounded = Math.clamp(u, 0D, 1D); return bounded + 0.8D * variation * bounded * (1D - bounded); diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ChannelFluids.java b/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ChannelFluids.java new file mode 100644 index 000000000..d2a887dbb --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ChannelFluids.java @@ -0,0 +1,8 @@ +package art.arcane.iris.generation.hydrology.raster; + +@FunctionalInterface +public interface ChannelFluids { + ChannelFluids TIER = (x, z) -> null; + + String channelFluid(int x, int z); +} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ChunkRasterizer.java b/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ChunkRasterizer.java index 9a27928ad..86701c773 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ChunkRasterizer.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ChunkRasterizer.java @@ -8,17 +8,18 @@ import art.arcane.iris.generation.hydrology.water.HydrologyTier; import java.util.List; import java.util.Objects; -import java.util.function.IntBinaryOperator; public final class ChunkRasterizer { private static final int CHUNK = 16; private static final int CANDIDATE_HALO = 48; private final HydrologySettings settings; + private final ChannelFluids channelFluids; private final int halo; - public ChunkRasterizer(HydrologySettings settings) { + public ChunkRasterizer(HydrologySettings settings, ChannelFluids channelFluids) { this.settings = Objects.requireNonNull(settings, "settings"); + this.channelFluids = Objects.requireNonNull(channelFluids, "channelFluids"); float widest = 0f; for (HydrologyTier tier : settings.tiers()) { widest = Math.max(widest, tier.maxHalfWidth()); @@ -33,13 +34,13 @@ public final class ChunkRasterizer { chunkX * CHUNK + CHUNK - 1 + halo, chunkZ * CHUNK + CHUNK - 1 + halo); } - public ChunkWaterRaster rasterize(int chunkX, int chunkZ, IntBinaryOperator naturalHeight, List windows, + public ChunkWaterRaster rasterize(int chunkX, int chunkZ, NaturalHeights naturalHeight, List windows, List lakeRegionPlans) { Objects.requireNonNull(naturalHeight, "naturalHeight"); ChunkFeatures features = ChunkFeatures.gather(settings, chunkX, chunkZ, halo, windows, lakeRegionPlans, lakeRegions(chunkX, chunkZ)); if (features.inert()) { return ChunkWaterRaster.EMPTY; } - return new RasterComposer(settings, features, chunkX, chunkZ).compose(naturalHeight); + return new RasterComposer(settings, features, channelFluids, chunkX, chunkZ).compose(naturalHeight); } } diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/raster/NaturalHeights.java b/core/src/main/java/art/arcane/iris/generation/hydrology/raster/NaturalHeights.java new file mode 100644 index 000000000..366992f35 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/raster/NaturalHeights.java @@ -0,0 +1,6 @@ +package art.arcane.iris.generation.hydrology.raster; + +@FunctionalInterface +public interface NaturalHeights { + double height(int x, int z); +} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/raster/RasterComposer.java b/core/src/main/java/art/arcane/iris/generation/hydrology/raster/RasterComposer.java index bb377ec85..c08dcb71c 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/raster/RasterComposer.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/raster/RasterComposer.java @@ -2,7 +2,6 @@ package art.arcane.iris.generation.hydrology.raster; import art.arcane.iris.generation.hydrology.config.IrisBankStyle; import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell; -import art.arcane.iris.generation.hydrology.grid.CellHash; import art.arcane.iris.generation.hydrology.plan.HydrologySettings; import art.arcane.iris.generation.hydrology.underground.SeaInlet; import art.arcane.iris.generation.hydrology.water.HydrologyTier; @@ -11,7 +10,6 @@ import art.arcane.iris.generation.hydrology.water.Reach; import art.arcane.iris.generation.hydrology.water.ReachStyle; import java.util.Arrays; -import java.util.function.IntBinaryOperator; final class RasterComposer { private static final int CHUNK = 16; @@ -22,12 +20,15 @@ final class RasterComposer { private static final double MAX_CONE_SLOPE = 2D; private static final double CONE_EXPONENT = 0.8D; private static final double ROUNDING = 1e-6D; - private static final long SALT_BANK = 0x42414e4b4e5aL; + private static final int SLOPE_REACH = 2; + private static final int RAY_STEP = 4; private final HydrologySettings settings; private final ChunkFeatures features; + private final ChannelFluids channelFluids; private final int originX; private final int originZ; + private final double[] exact = new double[CELLS]; private final int[] natural = new int[CELLS]; private final int[] terrain = new int[CELLS]; private final int[] head = new int[CELLS]; @@ -41,11 +42,12 @@ final class RasterComposer { private final HydrologyTier[] nearTier = new HydrologyTier[CELLS]; private final ReachCursor cursor = new ReachCursor(); private final int[] inletColumn = new int[2]; - private IntBinaryOperator naturalHeight; + private NaturalHeights naturalHeight; - RasterComposer(HydrologySettings settings, ChunkFeatures features, int chunkX, int chunkZ) { + RasterComposer(HydrologySettings settings, ChunkFeatures features, ChannelFluids channelFluids, int chunkX, int chunkZ) { this.settings = settings; this.features = features; + this.channelFluids = channelFluids; this.originX = chunkX * CHUNK - 1; this.originZ = chunkZ * CHUNK - 1; Arrays.fill(head, Integer.MIN_VALUE); @@ -54,11 +56,12 @@ final class RasterComposer { Arrays.fill(distance, Double.POSITIVE_INFINITY); } - ChunkWaterRaster compose(IntBinaryOperator naturalHeight) { + ChunkWaterRaster compose(NaturalHeights naturalHeight) { this.naturalHeight = naturalHeight; for (int cell = 0; cell < CELLS; cell++) { if (!corner(cell)) { - natural[cell] = naturalHeight.applyAsInt(originX + cell % GRID, originZ + cell / GRID); + exact[cell] = naturalHeight.height(originX + cell % GRID, originZ + cell / GRID); + natural[cell] = (int) Math.round(exact[cell]); terrain[cell] = natural[cell]; } } @@ -107,7 +110,7 @@ final class RasterComposer { return; } if (cursor.channel.reach != null) { - channel(cell, ground, variation, BedProfile.detail(settings.seed(), x, z)); + channel(cell, x, z, ground, variation); } else if (interior(cell)) { dry(cell, x, z, ground, variation); } @@ -129,16 +132,17 @@ final class RasterComposer { return Math.min(floor, bed(hit, Math.min(level, level(hit.reach.grade()[hit.point()])), ground, variation, detail)); } - private void channel(int cell, int ground, double variation, double detail) { + private void channel(int cell, int x, int z, int ground, double variation) { ReachCursor.Choice hit = cursor.channel; Reach reach = hit.reach; int point = hit.point(); + String area = channelFluids.channelFluid(x, z); head[cell] = level(reach.grade()[point]); - terrain[cell] = bed(hit, head[cell], ground, variation, detail); + terrain[cell] = bed(hit, head[cell], ground, variation, BedProfile.detail(settings.seed(), x, z)); role[cell] = role(reach.style()[point]); feature[cell] = reach.id(); tier[cell] = reach.tier(); - fluid[cell] = reach.tier().fluid(); + fluid[cell] = area != null ? area : reach.tier().fluid(); } private static int bed(ReachCursor.Choice hit, int surface, int ground, double variation, double detail) { @@ -162,7 +166,8 @@ final class RasterComposer { u = BedProfile.position(u, -variation); } if (ground >= surface) { - cut = BankProfile.cut(bankTier.bankStyle(), surface, ground, u, coneSlope(cell) * u * bankTier.bankBand(), noise(x, z)); + double cone = coneSlope(steepestToWater(x, z, bank, bankEdge)) * u * bankTier.bankBand(); + cut = BankProfile.cut(bankTier.bankStyle(), surface, ground, u, cone, noise(x, z)); } else { raise = BankProfile.levee(bankTier.bankStyle(), surface, ground, u, noise(x, z), settings.deformation().maxFill()); } @@ -205,15 +210,47 @@ final class RasterComposer { distance[cell] = best; } - private double coneSlope(int cell) { - double slope = Math.max(Math.abs(natural[cell + 1] - natural[cell - 1]), Math.abs(natural[cell + GRID] - natural[cell - GRID])) / 2D; + private double coneSlope(double slope) { HydrologySettings.Deformation deformation = settings.deformation(); return Math.min(MAX_CONE_SLOPE, deformation.bankSlope() + deformation.bankSteepFactor() * Math.pow(slope, CONE_EXPONENT)); } + private double steepestToWater(int x, int z, ReachCursor.Choice bank, double bankEdge) { + Reach reach = bank.reach; + int next = Math.min(bank.segment + 1, reach.xs().length - 1); + double centreX = reach.xs()[bank.segment] + (reach.xs()[next] - reach.xs()[bank.segment]) * bank.t; + double centreZ = reach.zs()[bank.segment] + (reach.zs()[next] - reach.zs()[bank.segment]) * bank.t; + double steepest = slopeAt(x, z); + double length = Math.hypot(centreX - x, centreZ - z); + if (length <= 0D) { + return steepest; + } + double towardX = (centreX - x) / length; + double towardZ = (centreZ - z) / length; + for (double edge = 0D; edge < bankEdge; edge += RAY_STEP) { + double back = bankEdge - edge; + steepest = Math.max(steepest, slopeAt((int) Math.floor(x + towardX * back + 0.5D), (int) Math.floor(z + towardZ * back + 0.5D))); + } + return steepest; + } + + private double slopeAt(int x, int z) { + return Math.max(Math.abs(exactAt(x + SLOPE_REACH, z) - exactAt(x - SLOPE_REACH, z)), + Math.abs(exactAt(x, z + SLOPE_REACH) - exactAt(x, z - SLOPE_REACH))) / (2D * SLOPE_REACH); + } + + private double exactAt(int x, int z) { + int gx = x - originX; + int gz = z - originZ; + if (gx >= 0 && gz >= 0 && gx < GRID && gz < GRID && !corner(gz * GRID + gx)) { + return exact[gz * GRID + gx]; + } + return naturalHeight.height(x, z); + } + private double noise(int x, int z) { float strength = settings.deformation().bankNoise(); - return strength == 0f ? 0D : (CellHash.unit(CellHash.mix(settings.seed(), SALT_BANK, x, z)) - 0.5D) * strength; + return strength == 0f ? 0D : BedProfile.roughness(settings.seed(), x, z) * strength; } private static int taperCut(int level, int ground, double u) { @@ -349,7 +386,7 @@ final class RasterComposer { if (gx >= 0 && gz >= 0 && gx < GRID && gz < GRID && !corner(gz * GRID + gx)) { return natural[gz * GRID + gx]; } - return naturalHeight.applyAsInt(x, z); + return (int) Math.round(naturalHeight.height(x, z)); } private void nameShores() { diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/IrisComplex.java b/core/src/main/java/art/arcane/iris/generation/runtime/IrisComplex.java index acec89aa9..791461168 100644 --- a/core/src/main/java/art/arcane/iris/generation/runtime/IrisComplex.java +++ b/core/src/main/java/art/arcane/iris/generation/runtime/IrisComplex.java @@ -1680,7 +1680,11 @@ public class IrisComplex implements DataProvider { /** The natural terrain height of a column, from the natural height stream only: never touches hydrology or its caches. */ public int naturalTrueHeight(int x, int z) { - return (int) Math.round(naturalHeightStream.getDouble(x, z)); + return (int) Math.round(naturalHeight(x, z)); + } + + public double naturalHeight(int x, int z) { + return naturalHeightStream.getDouble(x, z); } /** The terrain slope of a column from the natural height stream only: never touches hydrology or its caches. */ diff --git a/core/src/test/java/art/arcane/iris/generation/hydrology/engine/IrisHydrologyTerrainSourceTest.java b/core/src/test/java/art/arcane/iris/generation/hydrology/engine/IrisHydrologyTerrainSourceTest.java index b411a91a2..9ec081021 100644 --- a/core/src/test/java/art/arcane/iris/generation/hydrology/engine/IrisHydrologyTerrainSourceTest.java +++ b/core/src/test/java/art/arcane/iris/generation/hydrology/engine/IrisHydrologyTerrainSourceTest.java @@ -129,6 +129,52 @@ public class IrisHydrologyTerrainSourceTest { } } + @Test + public void climateZonesDryOutRainfallAndPondsOutsideOptedOutBiomes() throws Exception { + Path pack = EngineFixture.pack(temporaryFolder.newFolder().toPath()); + int[] xs = new int[COLUMNS]; + int[] zs = new int[COLUMNS]; + columns(xs, zs); + try (EngineFixture.Environment ignored = EngineFixture.environment(); EngineFixture fixture = new EngineFixture(pack)) { + IrisHydrology config = new IrisHydrology().setEnabled(true).setPersist(false); + config.getClimate().setEnabled(true).setDryness(0.5D).setTransition(0D).getStyle().setZoom(4D); + HydrologySettings settings = HydrologySettings.compile(config, EngineFixture.SEED, fixture.dimension.getFluidHeight(), 0, + EngineFixture.HEIGHT); + fixture.data.getBiomeLoader().load("meadow").getHydrologyPolicy().setClimateZones(false); + IrisHydrologyTerrainSource source = new IrisHydrologyTerrainSource(fixture.complex, fixture.dimension, settings, null, 1, + HydrologyBinding.UNBOUND); + HydrologySampleBuffer samples = new HydrologySampleBuffer(COLUMNS); + source.sample(xs, zs, samples, false); + int dry = 0; + int wet = 0; + int kept = 0; + for (int index = 0; index < COLUMNS; index++) { + int x = xs[index]; + int z = zs[index]; + boolean meadow = fixture.complex.getBaseBiomeStream().get(x, z).getLoadKey().equals("meadow"); + boolean alpha = fixture.complex.getRegionStream().get(x, z).getLoadKey().equals("alpha"); + String at = x + "," + z; + assertEquals(at, alpha ? "water" : null, source.channelFluid(x, z)); + if (meadow) { + assertEquals(at, 2f, samples.rainfall()[index], 0f); + assertEquals(at, alpha ? 3f : 1f, samples.pondDensity()[index], 0f); + kept++; + continue; + } + boolean zoneDry = samples.rainfall()[index] == 0f; + assertEquals(at, zoneDry ? 0f : 0.5f, samples.rainfall()[index], 0f); + assertEquals(at, zoneDry ? 0f : alpha ? 3f : 1f, samples.pondDensity()[index], 0f); + if (zoneDry) { + dry++; + } else { + wet++; + } + } + assertTrue("dry columns " + dry + ", wet columns " + wet + ", opted-out columns " + kept, + dry > COLUMNS / 20 && wet > COLUMNS / 20 && kept > 0); + } + } + private static void assertUnknownFluidRejected(EngineFixture fixture, HydrologySettings settings, int[] xs, int[] zs) { IrisRegion beta = fixture.data.getRegionLoader().load("beta"); beta.setHydrologyPolicy(new IrisHydrologyPolicy().setFluid("slime")); diff --git a/core/src/test/java/art/arcane/iris/generation/hydrology/plan/HydrologySettingsTest.java b/core/src/test/java/art/arcane/iris/generation/hydrology/plan/HydrologySettingsTest.java index fde9171aa..f34ed7f57 100644 --- a/core/src/test/java/art/arcane/iris/generation/hydrology/plan/HydrologySettingsTest.java +++ b/core/src/test/java/art/arcane/iris/generation/hydrology/plan/HydrologySettingsTest.java @@ -48,6 +48,55 @@ public class HydrologySettingsTest { @Rule public final TemporaryFolder temporary = new TemporaryFolder(); + @Test + public void climateIsBoundedAndOnlyEnabledZonesChangeThePlanIdentity() { + HydrologySettings plain = compile(new IrisHydrology()); + IrisHydrology disabled = new IrisHydrology(); + disabled.getClimate().setDryness(0.8D).setTransition(0.3D); + assertEquals(plain.fingerprint(), compile(disabled).fingerprint()); + IrisHydrology zoned = new IrisHydrology(); + zoned.getClimate().setEnabled(true); + IrisHydrology drier = new IrisHydrology(); + drier.getClimate().setEnabled(true).setDryness(0.6D); + IrisHydrology larger = new IrisHydrology(); + larger.getClimate().setEnabled(true).getStyle().setZoom(48D); + Set fingerprints = new HashSet<>(List.of(plain.fingerprint(), compile(zoned).fingerprint(), compile(drier).fingerprint(), + compile(larger).fingerprint())); + assertEquals(4, fingerprints.size()); + for (double invalid : new double[]{-0.01D, 1.01D, Double.NaN}) { + IrisHydrology dryness = new IrisHydrology(); + dryness.getClimate().setDryness(invalid); + assertRejected(dryness, "climate.dryness"); + IrisHydrology transition = new IrisHydrology(); + transition.getClimate().setTransition(invalid); + assertRejected(transition, "climate.transition"); + IrisHydrology dryRainfall = new IrisHydrology(); + dryRainfall.getClimate().setDryRainfall(invalid); + assertRejected(dryRainfall, "climate.dryRainfall"); + } + } + + @Test + public void climateWetnessFadesFromDryToWetZones() { + HydrologySettings.Climate off = compile(new IrisHydrology()).climate(); + assertEquals(1D, off.wetness(0D), 0D); + IrisHydrology sharp = new IrisHydrology(); + sharp.getClimate().setEnabled(true).setDryness(0.5D).setTransition(0D); + HydrologySettings.Climate edge = compile(sharp).climate(); + assertEquals(0D, edge.wetness(0.49D), 0D); + assertEquals(1D, edge.wetness(0.5D), 0D); + IrisHydrology soft = new IrisHydrology(); + soft.getClimate().setEnabled(true).setDryness(0.5D).setTransition(0.2D).setDryRainfall(0.25D); + HydrologySettings.Climate faded = compile(soft).climate(); + assertEquals(0.25D, faded.wetness(0.3D), 1e-9D); + assertEquals(0.625D, faded.wetness(0.5D), 1e-9D); + assertEquals(1D, faded.wetness(0.7D), 1e-9D); + assertTrue(faded.wetness(0.45D) < faded.wetness(0.55D)); + IrisHydrology wet = new IrisHydrology(); + wet.getClimate().setEnabled(true).setDryness(0D); + assertEquals(1D, compile(wet).climate().wetness(0D), 0D); + } + @Test public void tierForPicksHighestTierBelowCatchment() { HydrologySettings settings = compile(new IrisHydrology()); diff --git a/core/src/test/java/art/arcane/iris/generation/hydrology/raster/ChunkWaterRasterTest.java b/core/src/test/java/art/arcane/iris/generation/hydrology/raster/ChunkWaterRasterTest.java index 74f4cab03..4d1f1c065 100644 --- a/core/src/test/java/art/arcane/iris/generation/hydrology/raster/ChunkWaterRasterTest.java +++ b/core/src/test/java/art/arcane/iris/generation/hydrology/raster/ChunkWaterRasterTest.java @@ -261,7 +261,7 @@ public class ChunkWaterRasterTest { assertTrue("the chain ends at or below the first end grade", previous <= 60); List reversed = new ArrayList<>(scene.windows(31, 34)); Collections.reverse(reversed); - ChunkWaterRaster backward = new ChunkRasterizer(scene.settings).rasterize(31, 34, scene.natural, reversed, scene.regions(31, 34)); + ChunkWaterRaster backward = new ChunkRasterizer(scene.settings, ChannelFluids.TIER).rasterize(31, 34, scene.natural::applyAsInt, reversed, scene.regions(31, 34)); for (int lz = 0; lz < 16; lz++) { for (int lx = 0; lx < 16; lx++) { assertEquals(scene.raster(31, 34).waterHead(lx, lz), backward.waterHead(lx, lz)); @@ -403,7 +403,7 @@ public class ChunkWaterRasterTest { SeaInlet inlet = new SeaInlet(71L, xs, zs, halfWidth, SEA_LEVEL, floor, ceiling, 64, 8, 6); WindowKey key = new WindowKey(0, 0); IndexedWindow own = new IndexedWindow(new WindowPlan(key, List.of(), List.of(), List.of(inlet)), ReachIndex.build(List.of(), List.of())); - ChunkRasterizer rasterizer = new ChunkRasterizer(settings); + ChunkRasterizer rasterizer = new ChunkRasterizer(settings, ChannelFluids.TIER); ChunkWaterRaster mouth = rasterizer.rasterize(0, 0, (x, z) -> 70, List.of(own), regions(0, 0, List.of())); int[] column = new int[2]; int open = 0; @@ -440,8 +440,8 @@ public class ChunkWaterRasterTest { samples.incrementAndGet(); return 70; }; - ChunkRasterizer rasterizer = new ChunkRasterizer(settings); - ChunkWaterRaster raster = rasterizer.rasterize(0, 0, natural, List.of(own), regions(0, 0, List.of())); + ChunkRasterizer rasterizer = new ChunkRasterizer(settings, ChannelFluids.TIER); + ChunkWaterRaster raster = rasterizer.rasterize(0, 0, natural::applyAsInt, List.of(own), regions(0, 0, List.of())); assertTrue(raster.empty()); assertFalse(raster.provisional()); assertEquals("a riverless chunk never samples natural heights", 0, samples.get()); @@ -459,9 +459,94 @@ public class ChunkWaterRasterTest { assertThrows(IllegalStateException.class, () -> raster.terrainHeight(0, 0)); assertTrue(ChunkWaterRaster.PROVISIONAL.provisional()); assertTrue(ChunkWaterRaster.PROVISIONAL.empty()); - assertThrows(IllegalArgumentException.class, () -> rasterizer.rasterize(40, 0, natural, List.of(own), regions(40, 0, List.of()))); + assertThrows(IllegalArgumentException.class, () -> rasterizer.rasterize(40, 0, natural::applyAsInt, List.of(own), regions(40, 0, List.of()))); assertFalse("a chunk on the reach is not empty", - rasterizer.rasterize(27, 25, natural, List.of(own), regions(27, 25, List.of())).empty()); + rasterizer.rasterize(27, 25, natural::applyAsInt, List.of(own), regions(27, 25, List.of())).empty()); + } + + @Test + public void channelTakesTheAreaFluidWhereOneIsSet() { + HydrologySettings settings = settings(new IrisHydrology()); + Reach reach = line(61L, tier("stream", 1.5f, 0.5f, 0, IrisBankStyle.SMOOTH), 0L, 400f, 400f, 480f, 400f, (x, z) -> 69); + WindowKey key = new WindowKey(0, 0); + IndexedWindow own = new IndexedWindow(new WindowPlan(key, List.of(reach), List.of(), List.of()), ReachIndex.build(List.of(reach), List.of())); + ChannelFluids volcanic = (x, z) -> x < 440 ? "lava" : null; + ChunkWaterRaster raster = new ChunkRasterizer(settings, volcanic).rasterize(27, 25, (x, z) -> 70, List.of(own), regions(27, 25, List.of())); + int lava = 0; + int water = 0; + for (int lz = 0; lz < 16; lz++) { + for (int lx = 0; lx < 16; lx++) { + if (!isChannel(raster.role(lx, lz))) { + continue; + } + boolean inside = 27 * 16 + lx < 440; + assertEquals("column " + lx + "," + lz, inside ? "lava" : "water", raster.fluid(lx, lz)); + if (inside) { + lava++; + } else { + water++; + } + } + } + assertTrue("lava columns " + lava + ", water columns " + water, lava > 0 && water > 0); + } + + @Test + public void banksNeverDipAwayFromTheWater() { + HydrologySettings settings = settings(new IrisHydrology()); + Reach river = line(81L, tier("river", 3f, 0.5f, 32, IrisBankStyle.SMOOTH), 0L, 96f, 256f, 416f, 256f, (x, z) -> 69); + WindowKey key = new WindowKey(0, 0); + IndexedWindow own = new IndexedWindow(new WindowPlan(key, List.of(river), List.of(), List.of()), ReachIndex.build(List.of(river), List.of())); + NaturalHeights natural = (x, z) -> { + double away = Math.abs(z - 256); + if (x < 256) { + return 70D + 8D * Math.min(1D, away / 8D) + 0.15D * Math.max(0D, away - 8D); + } + return 70D + 8D * (1D - Math.exp(-away / 6D)) + (x < 336 ? 0D : 0.15D * away); + }; + ChunkRasterizer rasterizer = new ChunkRasterizer(settings, ChannelFluids.TIER); + Map terrain = new HashMap<>(); + Map roles = new HashMap<>(); + for (int chunkZ = 13; chunkZ <= 18; chunkZ++) { + for (int chunkX = 12; chunkX <= 25; chunkX++) { + ChunkWaterRaster raster = rasterizer.rasterize(chunkX, chunkZ, natural, List.of(own), regions(chunkX, chunkZ, List.of())); + for (int lz = 0; lz < 16; lz++) { + for (int lx = 0; lx < 16; lx++) { + int x = chunkX * 16 + lx; + int z = chunkZ * 16 + lz; + terrain.put(column(x, z), raster.empty() ? (int) Math.round(natural.height(x, z)) : raster.terrainHeight(lx, lz)); + roles.put(column(x, z), raster.empty() ? WaterRole.NONE : raster.role(lx, lz)); + } + } + } + } + int cut = 0; + List pits = new ArrayList<>(); + for (int z = 13 * 16 + 1; z < 19 * 16 - 1; z++) { + for (int x = 12 * 16 + 1; x < 26 * 16 - 1; x++) { + if (roles.get(column(x, z)) != WaterRole.BANK) { + continue; + } + int height = terrain.get(column(x, z)); + if (height < (int) Math.round(natural.height(x, z))) { + cut++; + } + int inward = z > 256 ? z - 1 : z + 1; + if (z == 256 || roles.get(column(x, inward)) != WaterRole.BANK) { + continue; + } + int nearer = terrain.get(column(x, inward)); + if (height < nearer) { + pits.add(x + "," + z + "@" + height + "<" + nearer); + } + } + } + assertTrue("cut bank columns " + cut, cut > 400); + assertTrue("bank dips " + pits.size() + " " + pits.subList(0, Math.min(10, pits.size())), pits.isEmpty()); + } + + private static long column(int x, int z) { + return ((long) x << 32) | (z & 0xFFFFFFFFL); } @Test @@ -525,7 +610,7 @@ public class ChunkWaterRasterTest { ChunkWaterRaster forward = scene.raster(31, 12); List reversed = new ArrayList<>(scene.windows(31, 12)); Collections.reverse(reversed); - ChunkWaterRaster backward = new ChunkRasterizer(scene.settings).rasterize(31, 12, scene.natural, reversed, scene.regions(31, 12)); + ChunkWaterRaster backward = new ChunkRasterizer(scene.settings, ChannelFluids.TIER).rasterize(31, 12, scene.natural::applyAsInt, reversed, scene.regions(31, 12)); for (int lz = 0; lz < 16; lz++) { for (int lx = 0; lx < 16; lx++) { assertEquals(forward.waterHead(lx, lz), backward.waterHead(lx, lz)); @@ -837,7 +922,7 @@ public class ChunkWaterRasterTest { private Scene(HydrologySettings settings, IntBinaryOperator natural) { this.settings = settings; this.natural = natural; - this.rasterizer = new ChunkRasterizer(settings); + this.rasterizer = new ChunkRasterizer(settings, ChannelFluids.TIER); } private Scene reach(Reach reach) { @@ -862,7 +947,7 @@ public class ChunkWaterRasterTest { private ChunkWaterRaster raster(int chunkX, int chunkZ) { return rasters.computeIfAbsent(pack(chunkX, chunkZ), - key -> rasterizer.rasterize(chunkX, chunkZ, natural, windows(chunkX, chunkZ), regions(chunkX, chunkZ))); + key -> rasterizer.rasterize(chunkX, chunkZ, natural::applyAsInt, windows(chunkX, chunkZ), regions(chunkX, chunkZ))); } private List windows(int chunkX, int chunkZ) { diff --git a/core/src/test/java/art/arcane/iris/generation/hydrology/water/PondPlannerTest.java b/core/src/test/java/art/arcane/iris/generation/hydrology/water/PondPlannerTest.java index df82518cf..71c97ad1d 100644 --- a/core/src/test/java/art/arcane/iris/generation/hydrology/water/PondPlannerTest.java +++ b/core/src/test/java/art/arcane/iris/generation/hydrology/water/PondPlannerTest.java @@ -15,6 +15,7 @@ import art.arcane.iris.generation.hydrology.plan.RegionKey; import art.arcane.iris.generation.hydrology.plan.RegionPlan; import art.arcane.iris.generation.hydrology.plan.WindowKey; import art.arcane.iris.generation.hydrology.plan.WindowPlan; +import art.arcane.iris.generation.hydrology.raster.ChannelFluids; import art.arcane.iris.generation.hydrology.raster.ChunkRasterizer; import art.arcane.iris.generation.hydrology.raster.ChunkWaterRaster; import art.arcane.iris.generation.hydrology.raster.ReachIndex; @@ -656,7 +657,7 @@ public class PondPlannerTest { private static void assertRasterRimContained(HydrologySettings settings, Pond pond, DoubleBinaryOperator natural) { Map rasters = new HashMap<>(); - ChunkRasterizer rasterizer = new ChunkRasterizer(settings); + ChunkRasterizer rasterizer = new ChunkRasterizer(settings, ChannelFluids.TIER); int raised = 0; int[][] sides = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; for (int[] wet : wetNodes(pond.mask())) { diff --git a/core/src/test/java/art/arcane/iris/generation/runtime/IrisEngineGenerationRuntimeScopeTest.java b/core/src/test/java/art/arcane/iris/generation/runtime/IrisEngineGenerationRuntimeScopeTest.java index 3326f511a..4157ce2d1 100644 --- a/core/src/test/java/art/arcane/iris/generation/runtime/IrisEngineGenerationRuntimeScopeTest.java +++ b/core/src/test/java/art/arcane/iris/generation/runtime/IrisEngineGenerationRuntimeScopeTest.java @@ -123,8 +123,8 @@ public class IrisEngineGenerationRuntimeScopeTest { AtomicInteger workerNaturals = new AtomicInteger(); doAnswer(invocation -> { observe(engine, historical, caller, naturals, workerNaturals, foreign); - return (int) Math.round(bowl(invocation.getArgument(0), invocation.getArgument(1))); - }).when(historicalComplex).naturalTrueHeight(anyInt(), anyInt()); + return bowl(invocation.getArgument(0), invocation.getArgument(1)); + }).when(historicalComplex).naturalHeight(anyInt(), anyInt()); IrisEngine.GenerationRuntimeBinding binding = detachedBinding(engine, historical.runtime); HydrologyRuntime hydrology; try (IrisEngine.GenerationRuntimeScope ignored = engine.openGenerationRuntimeScope(binding)) { diff --git a/core/src/test/java/art/arcane/iris/pack/schema/SchemaBuilderIdentityTest.java b/core/src/test/java/art/arcane/iris/pack/schema/SchemaBuilderIdentityTest.java index 5d5220f1e..a4d0b8579 100644 --- a/core/src/test/java/art/arcane/iris/pack/schema/SchemaBuilderIdentityTest.java +++ b/core/src/test/java/art/arcane/iris/pack/schema/SchemaBuilderIdentityTest.java @@ -84,7 +84,7 @@ public class SchemaBuilderIdentityTest { @ClassRule public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean(); - private static final String EXPECTED_SCHEMA_DIGEST = "b8dd16d1a3188e8150a68a9d5b2964aed5094d679c0f941e3703aec675f87e20"; + private static final String EXPECTED_SCHEMA_DIGEST = "2224e73a1e9d76231d4df8bb1992451340e5f533b3fbafae9f4e1437747e666c"; private static final List> SCHEMA_ROOTS = List.of( IrisDimension.class, diff --git a/core/src/test/java/art/arcane/iris/testsupport/FakeHydrology.java b/core/src/test/java/art/arcane/iris/testsupport/FakeHydrology.java index b6150b0ff..1a5ae3d82 100644 --- a/core/src/test/java/art/arcane/iris/testsupport/FakeHydrology.java +++ b/core/src/test/java/art/arcane/iris/testsupport/FakeHydrology.java @@ -83,7 +83,7 @@ public final class FakeHydrology implements AutoCloseable { }); HydrologySettings settings = settings(seaLevel); HydrologyPlanCache cache = new HydrologyPlanCache(settings, this::region, this::window, pool, mainThread); - this.runtime = new HydrologyRuntime(settings, cache, naturalHeight, mainThread, System::nanoTime); + this.runtime = new HydrologyRuntime(settings, cache, naturalHeight::applyAsInt, mainThread, System::nanoTime); } public static HydrologySettings settings(int seaLevel) { diff --git a/core/src/test/java/art/arcane/iris/testsupport/WaterScene.java b/core/src/test/java/art/arcane/iris/testsupport/WaterScene.java index 516b63e85..cf9a28715 100644 --- a/core/src/test/java/art/arcane/iris/testsupport/WaterScene.java +++ b/core/src/test/java/art/arcane/iris/testsupport/WaterScene.java @@ -9,6 +9,7 @@ import art.arcane.iris.generation.hydrology.plan.RegionKey; import art.arcane.iris.generation.hydrology.plan.RegionPlan; import art.arcane.iris.generation.hydrology.plan.WindowKey; import art.arcane.iris.generation.hydrology.plan.WindowPlan; +import art.arcane.iris.generation.hydrology.raster.ChannelFluids; import art.arcane.iris.generation.hydrology.raster.ChunkRasterizer; import art.arcane.iris.generation.hydrology.raster.ChunkWaterRaster; import art.arcane.iris.generation.hydrology.raster.ReachIndex; @@ -40,7 +41,7 @@ public final class WaterScene { public WaterScene(IrisHydrology config, IntBinaryOperator natural) { this.natural = natural; - this.rasterizer = new ChunkRasterizer(HydrologySettings.compile(config, SEED, SEA_LEVEL, MIN_Y, MAX_Y)); + this.rasterizer = new ChunkRasterizer(HydrologySettings.compile(config, SEED, SEA_LEVEL, MIN_Y, MAX_Y), ChannelFluids.TIER); } public WaterScene reach(Reach reach) { @@ -55,7 +56,7 @@ public final class WaterScene { public ChunkWaterRaster raster(int chunkX, int chunkZ) { return rasters.computeIfAbsent(((long) chunkX << 32) | (chunkZ & 0xFFFFFFFFL), - key -> rasterizer.rasterize(chunkX, chunkZ, natural, windows(chunkX, chunkZ), regions(chunkX, chunkZ))); + key -> rasterizer.rasterize(chunkX, chunkZ, natural::applyAsInt, windows(chunkX, chunkZ), regions(chunkX, chunkZ))); } public int terrain(int x, int z) { diff --git a/probe/src/main/java/art/arcane/iris/probe/HydrologyBlockMetrics.java b/probe/src/main/java/art/arcane/iris/probe/HydrologyBlockMetrics.java index f8b07c412..60d851a29 100644 --- a/probe/src/main/java/art/arcane/iris/probe/HydrologyBlockMetrics.java +++ b/probe/src/main/java/art/arcane/iris/probe/HydrologyBlockMetrics.java @@ -86,7 +86,7 @@ final class HydrologyBlockMetrics { @Override public int natural(int x, int z) { - return inputs.naturalHeight().applyAsInt(x, z); + return (int) Math.round(inputs.naturalHeight().height(x, z)); } }; int minX = key.wx() * WindowKey.SIZE; diff --git a/probe/src/main/java/art/arcane/iris/probe/HydrologyPlanHarness.java b/probe/src/main/java/art/arcane/iris/probe/HydrologyPlanHarness.java index ddc37a008..9937e540c 100644 --- a/probe/src/main/java/art/arcane/iris/probe/HydrologyPlanHarness.java +++ b/probe/src/main/java/art/arcane/iris/probe/HydrologyPlanHarness.java @@ -11,10 +11,12 @@ import art.arcane.iris.generation.hydrology.plan.RegionPlan; import art.arcane.iris.generation.hydrology.plan.WindowKey; import art.arcane.iris.generation.hydrology.plan.WindowPlan; import art.arcane.iris.generation.hydrology.plan.WindowPlanner; +import art.arcane.iris.generation.hydrology.raster.ChannelFluids; import art.arcane.iris.generation.hydrology.raster.ChunkRasterizer; import art.arcane.iris.generation.hydrology.raster.ChunkWaterRaster; import art.arcane.iris.generation.hydrology.raster.ReachIndex; import art.arcane.iris.generation.hydrology.water.Pond; +import art.arcane.iris.generation.hydrology.raster.NaturalHeights; import com.sun.management.OperatingSystemMXBean; import java.lang.management.ManagementFactory; @@ -32,7 +34,6 @@ import java.util.Set; import java.util.TreeSet; import java.util.concurrent.atomic.AtomicLong; import java.util.function.BiFunction; -import java.util.function.IntBinaryOperator; final class HydrologyPlanHarness { private static final double TILE_AREA = 1024D * 1024D; @@ -43,7 +44,7 @@ final class HydrologyPlanHarness { static final class Plans { private final HydrologySettings settings; - private final IntBinaryOperator naturalHeight; + private final NaturalHeights naturalHeight; private final HydrologyWindows.WindowRange planned; private final CountingTerrain counting; private final HydrologyPlanners planners; @@ -54,13 +55,13 @@ final class HydrologyPlanHarness { private final Map windowCpu = new HashMap<>(); private final List failures = new ArrayList<>(); - Plans(HydrologySettings settings, HydrologyTerrainSource terrain, IntBinaryOperator naturalHeight, HydrologyWindows.WindowRange range) { + Plans(HydrologySettings settings, HydrologyTerrainSource terrain, NaturalHeights naturalHeight, HydrologyWindows.WindowRange range) { this.settings = settings; this.naturalHeight = naturalHeight; this.planned = range.grown(1); this.counting = new CountingTerrain(terrain); this.planners = HydrologyPlanners.create(settings, counting); - this.rasterizer = new ChunkRasterizer(settings); + this.rasterizer = new ChunkRasterizer(settings, ChannelFluids.TIER); Set regionKeys = new TreeSet<>(REGION_ORDER); for (WindowKey key : planned.keys()) { regionKeys.addAll(WindowPlanner.lakeRegions(key)); diff --git a/probe/src/main/java/art/arcane/iris/probe/HydrologyProbe.java b/probe/src/main/java/art/arcane/iris/probe/HydrologyProbe.java index 72546f014..fcf688cb1 100644 --- a/probe/src/main/java/art/arcane/iris/probe/HydrologyProbe.java +++ b/probe/src/main/java/art/arcane/iris/probe/HydrologyProbe.java @@ -11,6 +11,7 @@ import art.arcane.iris.generation.hydrology.plan.WindowPlan; import art.arcane.iris.generation.hydrology.raster.ChunkWaterRaster; import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.terrain.IrisDimension; +import art.arcane.iris.generation.hydrology.raster.NaturalHeights; import java.util.ArrayList; import java.util.Collections; @@ -27,7 +28,6 @@ import java.util.concurrent.Executors; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.ForkJoinTask; import java.util.concurrent.Future; -import java.util.function.IntBinaryOperator; public final class HydrologyProbe { private static final int RASTER_CACHE = 8192; @@ -56,7 +56,7 @@ public final class HydrologyProbe { } } - record Inputs(HydrologySettings settings, HydrologyTerrainSource terrain, IntBinaryOperator naturalHeight, ForkJoinPool planningPool, + record Inputs(HydrologySettings settings, HydrologyTerrainSource terrain, NaturalHeights naturalHeight, ForkJoinPool planningPool, ExecutorService workers, HydrologyVoxels.ChunkSource blocks, HydrologyUnderground.Source underground, int worldHeight) { Inputs { Objects.requireNonNull(settings, "settings"); @@ -98,7 +98,7 @@ public final class HydrologyProbe { ForkJoinPool planningPool = MultiBurst.hydrology.submit((Callable) ForkJoinTask::getPool).get(); ExecutorService workers = Executors.newFixedThreadPool(configuration.threads()); try { - Inputs inputs = new Inputs(settings, terrain, terrain::naturalHeight, planningPool, workers, + Inputs inputs = new Inputs(settings, terrain, terrain::naturalSurface, planningPool, workers, (chunkX, chunkZ) -> HydrologyVoxels.generate(engine, chunkX, chunkZ), HydrologyUnderground.generated(engine, workers), engine.getTarget().getHeight()); HydrologyReport.Result measured = measure(inputs, target.windows()); diff --git a/probe/src/main/java/art/arcane/iris/probe/HydrologyRasterMetrics.java b/probe/src/main/java/art/arcane/iris/probe/HydrologyRasterMetrics.java index dea7e1e6f..e4b5be122 100644 --- a/probe/src/main/java/art/arcane/iris/probe/HydrologyRasterMetrics.java +++ b/probe/src/main/java/art/arcane/iris/probe/HydrologyRasterMetrics.java @@ -8,15 +8,15 @@ import art.arcane.iris.generation.hydrology.raster.WaterRole; import art.arcane.iris.generation.hydrology.water.Pond; import art.arcane.iris.generation.hydrology.water.Reach; import art.arcane.iris.generation.hydrology.water.ReachStyle; +import art.arcane.iris.generation.hydrology.raster.NaturalHeights; import java.util.function.Consumer; -import java.util.function.IntBinaryOperator; final class HydrologyRasterMetrics { private final HydrologySettings settings; private final HydrologyPlanMetrics plans; private final Rasters rasters; - private final IntBinaryOperator naturalHeight; + private final NaturalHeights naturalHeight; private final HydrologyWindows.WindowRange range; private final Consumer examples; @@ -25,7 +25,7 @@ final class HydrologyRasterMetrics { ChunkWaterRaster get(int chunkX, int chunkZ); } - HydrologyRasterMetrics(HydrologySettings settings, HydrologyPlanMetrics plans, Rasters rasters, IntBinaryOperator naturalHeight, + HydrologyRasterMetrics(HydrologySettings settings, HydrologyPlanMetrics plans, Rasters rasters, NaturalHeights naturalHeight, HydrologyWindows.WindowRange range, Consumer examples) { this.settings = settings; this.plans = plans; @@ -115,7 +115,7 @@ final class HydrologyRasterMetrics { continue; } int terrain = raster.terrainHeight(x & 15, z & 15); - int natural = naturalHeight.applyAsInt(x, z); + int natural = (int) Math.round(naturalHeight.height(x, z)); if (terrain > natural + maxFill) { overCap++; examples.accept("berm raised " + x + "," + z + " from " + natural + " to " + terrain); @@ -131,7 +131,7 @@ final class HydrologyRasterMetrics { int terrain(int x, int z) { ChunkWaterRaster raster = rasters.get(x >> 4, z >> 4); - return raster.empty() ? naturalHeight.applyAsInt(x, z) : raster.terrainHeight(x & 15, z & 15); + return raster.empty() ? (int) Math.round(naturalHeight.height(x, z)) : raster.terrainHeight(x & 15, z & 15); } private boolean seamMismatch(int x, int z, int nx, int nz) { diff --git a/probe/src/main/java/art/arcane/iris/probe/RiverTransectProbe.java b/probe/src/main/java/art/arcane/iris/probe/RiverTransectProbe.java index e626a9f44..ce8c284c6 100644 --- a/probe/src/main/java/art/arcane/iris/probe/RiverTransectProbe.java +++ b/probe/src/main/java/art/arcane/iris/probe/RiverTransectProbe.java @@ -16,6 +16,7 @@ import art.arcane.iris.generation.hydrology.water.Reach; import art.arcane.iris.generation.hydrology.water.ReachStyle; import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.terrain.IrisDimension; +import art.arcane.iris.generation.hydrology.raster.NaturalHeights; import javax.imageio.ImageIO; import java.awt.BasicStroke; @@ -33,7 +34,6 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.function.IntBinaryOperator; public final class RiverTransectProbe { static final int NO_WATER = Integer.MIN_VALUE; @@ -116,16 +116,16 @@ public final class RiverTransectProbe { engine.getHeight()); IrisHydrologyTerrainSource terrain = new IrisHydrologyTerrainSource(engine.getComplex(), dimension, settings, MultiBurst.hydrology, MultiBurst.hydrology.parallelism(), HydrologyBinding.UNBOUND); - HydrologyPlanHarness.Plans plans = new HydrologyPlanHarness.Plans(settings, terrain, terrain::naturalHeight, target.windows()); + HydrologyPlanHarness.Plans plans = new HydrologyPlanHarness.Plans(settings, terrain, terrain::naturalSurface, target.windows()); List rendered = new ArrayList<>(); for (WindowKey key : target.windows().keys()) { - rendered.add(render(plans, key, terrain::naturalHeight, target.windows(), configuration.output())); + rendered.add(render(plans, key, terrain::naturalSurface, target.windows(), configuration.output())); } return rendered; } } - static Rendered render(HydrologyPlanHarness.Plans plans, WindowKey key, IntBinaryOperator natural, HydrologyWindows.WindowRange range, + static Rendered render(HydrologyPlanHarness.Plans plans, WindowKey key, NaturalHeights natural, HydrologyWindows.WindowRange range, File output) throws Exception { Map rasters = new HashMap<>(); Columns columns = (x, z) -> { @@ -134,7 +134,7 @@ public final class RiverTransectProbe { } ChunkWaterRaster raster = rasters.computeIfAbsent(HydrologyVoxels.chunkKey(x >> 4, z >> 4), ignored -> plans.rasterize(x >> 4, z >> 4)); - int ground = natural.applyAsInt(x, z); + int ground = (int) Math.round(natural.height(x, z)); return new Column(ground, raster.empty() ? ground : raster.terrainHeight(x & 15, z & 15), raster.waterHead(x & 15, z & 15), raster.role(x & 15, z & 15)); };