Remove Hydrology

This commit is contained in:
Brian Neumann-Fopiano
2026-08-27 00:45:15 -04:00
parent bdaa84975a
commit f398c55091
188 changed files with 1291 additions and 21010 deletions
@@ -62,7 +62,6 @@ public class CustomBiomeSource extends BiomeSource {
private final ConcurrentHashMap<Long, Holder<Biome>> noiseBiomeCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Long, Holder<Biome>> structureBiomeCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Long, Holder<Biome>> surfaceStructureBiomeCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Long, Holder<Biome>> naturalSurfaceStructureBiomeCache = new ConcurrentHashMap<>();
private volatile KMap<String, Holder<Biome>> customBiomes;
private volatile Map<Biome, Holder<Biome>> vanillaSpawnBiomes;
private volatile IrisDimension cacheDimension;
@@ -332,18 +331,8 @@ public class CustomBiomeSource extends BiomeSource {
if (quartStep == 1) {
return super.findBiomeHorizontal(x, y, z, searchRadius, allowed, random, sampler);
}
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_structure_ring_biome");
if (lease == null) {
throw new IllegalStateException("Iris structure ring biome lookup was rejected during an engine transition");
}
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
if (!isRuntimeAvailable()) {
throw new IllegalStateException("Iris structure ring biome lookup has no active engine runtime");
}
ensureCachesCurrent();
return findNaturalSurfaceBiomeHorizontal(
x, y, z, searchRadius, quartStep, allowed, random);
}
return super.findBiomeHorizontal(
x, y, z, searchRadius, quartStep, allowed, random, false, sampler);
}
static int horizontalBiomeSearchQuartStep(int blockY, int searchRadius) {
@@ -401,60 +390,6 @@ public class CustomBiomeSource extends BiomeSource {
return resolvedSurfaceHolder;
}
private Pair<BlockPos, Holder<Biome>> findNaturalSurfaceBiomeHorizontal(
int x,
int y,
int z,
int searchRadius,
int quartStep,
Predicate<Holder<Biome>> allowed,
RandomSource random
) {
int centerQuartX = QuartPos.fromBlock(x);
int centerQuartZ = QuartPos.fromBlock(z);
int quartRadius = QuartPos.fromBlock(searchRadius);
Pair<BlockPos, Holder<Biome>> selected = null;
int matches = 0;
for (int radius = 0; radius <= quartRadius; radius += quartStep) {
for (int offsetZ = -radius; offsetZ <= radius; offsetZ += quartStep) {
for (int offsetX = -radius; offsetX <= radius; offsetX += quartStep) {
int quartX = centerQuartX + offsetX;
int quartZ = centerQuartZ + offsetZ;
Holder<Biome> holder = getNaturalSurfaceStructureBiomeHolder(quartX, quartZ);
if (!allowed.test(holder)) {
continue;
}
if (selected == null || random.nextInt(matches + 1) == 0) {
selected = Pair.of(new BlockPos(
QuartPos.toBlock(quartX),
y,
QuartPos.toBlock(quartZ)), holder);
}
matches++;
}
}
}
return selected;
}
private Holder<Biome> getNaturalSurfaceStructureBiomeHolder(int x, int z) {
long columnKey = packColumnKey(x, z);
Holder<Biome> cachedHolder = naturalSurfaceStructureBiomeCache.get(columnKey);
if (cachedHolder != null) {
return cachedHolder;
}
Holder<Biome> resolvedHolder = resolveNaturalSurfaceStructureBiomeHolder(x, z);
Holder<Biome> existingHolder = naturalSurfaceStructureBiomeCache.putIfAbsent(
columnKey, resolvedHolder);
if (existingHolder != null) {
return existingHolder;
}
if (naturalSurfaceStructureBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
naturalSurfaceStructureBiomeCache.clear();
}
return resolvedHolder;
}
private boolean isGuaranteedSurfaceBiome(int quartY) {
if (engine == null || engine.isClosed() || engine.getComplex() == null) {
return false;
@@ -482,23 +417,6 @@ public class CustomBiomeSource extends BiomeSource {
return holder;
}
private Holder<Biome> resolveNaturalSurfaceStructureBiomeHolder(int x, int z) {
int blockX = x << 2;
int blockZ = z << 2;
IrisBiome irisBiome = engine.getComplex().getNaturalTrueBiomeStream().get(blockX, blockZ);
if (irisBiome == null) {
throw new IllegalStateException("Iris returned no natural structure biome at block "
+ blockX + "," + blockZ);
}
Holder<Biome> holder = resolveBiomeHolder(biomeRegistry, irisBiome.getStructureDerivativeKey());
if (holder == null) {
throw new IllegalStateException("Iris natural structure biome derivative '"
+ irisBiome.getStructureDerivativeKey() + "' is not registered at block "
+ blockX + "," + blockZ);
}
return holder;
}
public Holder<Biome> getVisibleNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_visible_biome");
if (lease == null) {
@@ -588,7 +506,6 @@ public class CustomBiomeSource extends BiomeSource {
noiseBiomeCache.clear();
structureBiomeCache.clear();
surfaceStructureBiomeCache.clear();
naturalSurfaceStructureBiomeCache.clear();
customBiomes = refreshedCustomBiomes;
vanillaSpawnBiomes = refreshedSpawnBiomes;
cacheDimension = dimension;
@@ -45,36 +45,9 @@ public class CustomBiomeSourceStructureContractTest {
assertTrue(source.contains("private static final int STRONGHOLD_RING_SEARCH_Y = 0"));
assertTrue(source.contains("private static final int STRONGHOLD_RING_SEARCH_RADIUS = 112"));
assertTrue(source.contains("private static final int STRONGHOLD_RING_SEARCH_QUART_STEP = 4"));
assertTrue(source.contains("x, y, z, searchRadius, quartStep, allowed, random, false, sampler"));
assertTrue(source.contains(
"return super.findBiomeHorizontal(x, y, z, searchRadius, allowed, random, sampler)"));
assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_structure_ring_biome\")"));
assertTrue(source.contains("findNaturalSurfaceBiomeHorizontal("));
assertTrue(source.contains("radius += quartStep"));
assertTrue(source.contains("offsetZ += quartStep"));
assertTrue(source.contains("offsetX += quartStep"));
assertTrue(source.contains("random.nextInt(matches + 1) == 0"));
}
@Test
public void onlyConcentricRingSuitabilityUsesTheNaturalTerrainStream() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource")));
int realResolutionStart = source.indexOf("private Holder<Biome> resolveSurfaceStructureBiomeHolder(");
int naturalResolutionStart = source.indexOf(
"private Holder<Biome> resolveNaturalSurfaceStructureBiomeHolder(");
int resolutionEnd = source.indexOf("public Holder<Biome> getVisibleNoiseBiome(", naturalResolutionStart);
assertTrue(realResolutionStart >= 0);
assertTrue(naturalResolutionStart > realResolutionStart);
assertTrue(resolutionEnd > naturalResolutionStart);
String realResolution = source.substring(realResolutionStart, naturalResolutionStart);
String naturalResolution = source.substring(naturalResolutionStart, resolutionEnd);
assertTrue(realResolution.contains("engine.getComplex().getTrueBiomeStream().get(blockX, blockZ)"));
assertFalse(realResolution.contains("getNaturalTrueBiomeStream()"));
assertTrue(naturalResolution.contains(
"engine.getComplex().getNaturalTrueBiomeStream().get(blockX, blockZ)"));
assertFalse(naturalResolution.contains("getTrueBiomeStream()"));
assertFalse(source.contains("studioBootstrapSurfaceStructureBiomeCache"));
}
@Test
@@ -2,11 +2,6 @@ package art.arcane.iris.api.terrain;
public enum IrisColumnField {
SURFACE_HEIGHT,
NATURAL_HEIGHT,
SURFACE_KIND,
BIOME_KEY,
RIVER_STATE,
RIVER_DISTANCE,
RIVER_FLOW,
RIVER_WATER_SURFACE_Y
BIOME_KEY
}
@@ -1,64 +0,0 @@
package art.arcane.iris.api.terrain;
import java.util.Objects;
public record IrisColumnSample(
int blockX,
int blockZ,
int surfaceHeight,
int naturalHeight,
IrisSurfaceKind surfaceKind,
String biomeKey,
IrisRiverState riverState,
double riverDistance,
int riverFlow,
int riverWaterSurfaceY
) {
public static final int UNAVAILABLE_HEIGHT = Integer.MIN_VALUE;
public static final double UNAVAILABLE_RIVER_DISTANCE = Double.NaN;
public static final int UNAVAILABLE_RIVER_FLOW = -1;
public IrisColumnSample {
Objects.requireNonNull(surfaceKind, "surfaceKind");
Objects.requireNonNull(riverState, "riverState");
biomeKey = biomeKey == null || biomeKey.isBlank() ? null : biomeKey;
if (!Double.isNaN(riverDistance) && (!Double.isFinite(riverDistance) || riverDistance < 0D)) {
throw new IllegalArgumentException("riverDistance must be non-negative, finite, or unavailable");
}
if (riverFlow < UNAVAILABLE_RIVER_FLOW) {
throw new IllegalArgumentException("riverFlow must be non-negative or unavailable");
}
}
public boolean hasSurfaceHeight() {
return surfaceHeight != UNAVAILABLE_HEIGHT;
}
public boolean hasNaturalHeight() {
return naturalHeight != UNAVAILABLE_HEIGHT;
}
public boolean hasSurfaceKind() {
return surfaceKind != IrisSurfaceKind.UNKNOWN;
}
public boolean hasBiomeKey() {
return biomeKey != null;
}
public boolean hasRiverState() {
return riverState != IrisRiverState.NONE;
}
public boolean hasRiverDistance() {
return !Double.isNaN(riverDistance);
}
public boolean hasRiverFlow() {
return riverFlow != UNAVAILABLE_RIVER_FLOW;
}
public boolean hasRiverWaterSurfaceY() {
return riverWaterSurfaceY != UNAVAILABLE_HEIGHT;
}
}
@@ -2,5 +2,5 @@ package art.arcane.iris.api.terrain;
@FunctionalInterface
public interface IrisColumnSink {
void accept(IrisColumnSample sample);
void accept(int blockX, int blockZ, int surfaceHeight, IrisSurfaceKind kind, String biomeKey);
}
@@ -1,7 +0,0 @@
package art.arcane.iris.api.terrain;
public enum IrisRiverState {
NONE,
WET,
DRY
}
@@ -4,9 +4,6 @@ public enum IrisSurfaceKind {
UNKNOWN,
LAND,
SHORE,
RIVER,
RIVER_SHORE,
DRY_CHANNEL,
OCEAN,
VOID
}
@@ -318,7 +318,7 @@ public final class BukkitVisionOverlay implements GuiOverlay {
public String openInEditor(double worldX, double worldZ, RenderType type) {
IrisComplex complex = engine.getComplex();
File file = switch (type) {
case BIOME, LAYER_LOAD, DECORATOR_LOAD, OBJECT_LOAD, HEIGHT, RIVER ->
case BIOME, LAYER_LOAD, DECORATOR_LOAD, OBJECT_LOAD, HEIGHT ->
complex.getTrueBiomeStream().get(worldX, worldZ).openInVSCode();
case BIOME_LAND -> complex.getLandBiomeStream().get(worldX, worldZ).openInVSCode();
case BIOME_SEA -> complex.getSeaBiomeStream().get(worldX, worldZ).openInVSCode();
@@ -2,9 +2,7 @@ package art.arcane.iris.core.service;
import art.arcane.iris.api.terrain.IrisColumnField;
import art.arcane.iris.api.terrain.IrisColumnQuery;
import art.arcane.iris.api.terrain.IrisColumnSample;
import art.arcane.iris.api.terrain.IrisColumnSink;
import art.arcane.iris.api.terrain.IrisRiverState;
import art.arcane.iris.api.terrain.IrisSurfaceKind;
import art.arcane.iris.api.terrain.IrisTerrainService;
import art.arcane.iris.api.terrain.IrisWorldInfo;
@@ -19,8 +17,6 @@ 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.platform.PlatformChunkGenerator;
import art.arcane.iris.engine.river.RiverRouteState;
import art.arcane.iris.engine.river.runtime.IrisRiverSurfaceSample;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisServices;
@@ -105,14 +101,13 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService {
try {
int surface = engine.getHeight(blockX, blockZ);
IrisRiverSurfaceSample riverSurface = engine.getComplex().getRiverSurfaceStream().get(blockX, blockZ);
int fluid = (int) Math.round(riverSurface.waterSurfaceY());
int fluid = engine.getDimension().getFluidHeight();
InferredType inferredType = null;
if (IrisSurfaceClassifier.requiresSurfaceBiome(surface, fluid)) {
IrisBiome biome = engine.getSurfaceBiome(blockX, blockZ);
inferredType = biome == null ? null : biome.getInferredType();
}
return IrisSurfaceClassifier.classify(surface, fluid, inferredType, riverSurface);
return IrisSurfaceClassifier.classify(surface, fluid, inferredType);
} catch (Throwable error) {
reportQueryFault("surfaceKind", world, error);
return IrisSurfaceKind.UNKNOWN;
@@ -229,72 +224,26 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService {
EnumSet<IrisColumnField> fields = query.fields();
boolean wantHeight = fields.contains(IrisColumnField.SURFACE_HEIGHT);
boolean wantNaturalHeight = fields.contains(IrisColumnField.NATURAL_HEIGHT);
boolean wantKind = fields.contains(IrisColumnField.SURFACE_KIND);
boolean wantBiome = fields.contains(IrisColumnField.BIOME_KEY);
boolean wantRiverState = fields.contains(IrisColumnField.RIVER_STATE);
boolean wantRiverDistance = fields.contains(IrisColumnField.RIVER_DISTANCE);
boolean wantRiverFlow = fields.contains(IrisColumnField.RIVER_FLOW);
boolean wantRiverWaterSurface = fields.contains(IrisColumnField.RIVER_WATER_SURFACE_Y);
boolean wantRiver = wantKind || wantRiverState || wantRiverDistance || wantRiverFlow
|| wantRiverWaterSurface;
try {
int minHeight = engine.getMinHeight();
int fluid = engine.getDimension().getFluidHeight();
long visited = IrisColumnWalk.walk(query, (int blockX, int blockZ) -> {
if (engine.isClosed()) {
return false;
}
IrisRiverSurfaceSample riverSurface = wantRiver
? engine.getComplex().getRiverSurfaceStream().get(blockX, blockZ)
: null;
int surface = wantHeight || wantKind ? engine.getHeight(blockX, blockZ) : 0;
int fluid = riverSurface == null
? engine.getDimension().getFluidHeight()
: (int) Math.round(riverSurface.waterSurfaceY());
boolean needsBiome = wantBiome
|| (wantKind && IrisSurfaceClassifier.requiresSurfaceBiome(surface, fluid));
IrisBiome biome = needsBiome ? engine.getSurfaceBiome(blockX, blockZ) : null;
IrisSurfaceKind kind = wantKind
? IrisSurfaceClassifier.classify(
surface,
fluid,
biome == null ? null : biome.getInferredType(),
riverSurface
)
? IrisSurfaceClassifier.classify(surface, fluid, biome == null ? null : biome.getInferredType())
: IrisSurfaceKind.UNKNOWN;
String biomeKey = wantBiome && biome != null ? biome.getLoadKey() : null;
int natural = wantNaturalHeight
? (int) Math.round(engine.getComplex().getNaturalHeightStream().get(blockX, blockZ)) + minHeight
: IrisColumnSample.UNAVAILABLE_HEIGHT;
boolean riverPresent = riverSurface != null && riverSurface.river().present();
IrisRiverState riverState = wantRiverState && riverSurface != null
? riverState(riverSurface)
: IrisRiverState.NONE;
double riverDistance = wantRiverDistance && riverPresent
? riverSurface.river().distance()
: IrisColumnSample.UNAVAILABLE_RIVER_DISTANCE;
int riverFlow = wantRiverFlow && riverPresent
? riverSurface.river().flow()
: IrisColumnSample.UNAVAILABLE_RIVER_FLOW;
int riverWaterSurfaceY = wantRiverWaterSurface
&& riverPresent
&& riverSurface.river().state() == RiverRouteState.WET
? fluid + minHeight
: IrisColumnSample.UNAVAILABLE_HEIGHT;
sink.accept(new IrisColumnSample(
blockX,
blockZ,
wantHeight ? surface + minHeight : IrisColumnSample.UNAVAILABLE_HEIGHT,
natural,
kind,
biomeKey,
riverState,
riverDistance,
riverFlow,
riverWaterSurfaceY
));
sink.accept(blockX, blockZ, wantHeight ? surface + minHeight : -1, kind, biomeKey);
return true;
});
return visited == query.columnCount();
@@ -304,17 +253,6 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService {
}
}
static IrisRiverState riverState(IrisRiverSurfaceSample surface) {
if (!surface.river().present()) {
return IrisRiverState.NONE;
}
return switch (surface.river().state()) {
case WET -> IrisRiverState.WET;
case DRY -> IrisRiverState.DRY;
case SUPPRESSED -> IrisRiverState.NONE;
};
}
private static Optional<String> key(IrisBiome biome) {
String loadKey = biome == null ? null : biome.getLoadKey();
return loadKey == null || loadKey.isEmpty() ? Optional.empty() : Optional.of(loadKey);
@@ -2,9 +2,6 @@ package art.arcane.iris.core.service.terrain;
import art.arcane.iris.api.terrain.IrisSurfaceKind;
import art.arcane.iris.engine.object.InferredType;
import art.arcane.iris.engine.river.RiverRouteState;
import art.arcane.iris.engine.river.RiverSection;
import art.arcane.iris.engine.river.runtime.IrisRiverSurfaceSample;
public final class IrisSurfaceClassifier {
private IrisSurfaceClassifier() {
@@ -25,32 +22,4 @@ public final class IrisSurfaceClassifier {
return inferredType == InferredType.SHORE ? IrisSurfaceKind.SHORE : IrisSurfaceKind.LAND;
}
public static IrisSurfaceKind classify(
int engineSurfaceHeight,
int engineFluidHeight,
InferredType inferredType,
IrisRiverSurfaceSample riverSurface
) {
if (engineSurfaceHeight <= 0) {
return IrisSurfaceKind.VOID;
}
if (riverSurface != null && riverSurface.river().present() && !riverSurface.subterranean()) {
if (riverSurface.river().state() == RiverRouteState.DRY) {
return riverSurface.river().section() == RiverSection.DRY_CHANNEL
? IrisSurfaceKind.DRY_CHANNEL
: IrisSurfaceKind.LAND;
}
if (riverSurface.river().state() == RiverRouteState.WET) {
RiverSection section = riverSurface.river().section();
if (section == RiverSection.BANK) {
return IrisSurfaceKind.RIVER_SHORE;
}
if (section == RiverSection.CHANNEL || section == RiverSection.MOUTH) {
return IrisSurfaceKind.RIVER;
}
}
}
return classify(engineSurfaceHeight, engineFluidHeight, inferredType);
}
}
@@ -1,145 +0,0 @@
package art.arcane.iris.api.terrain;
import org.junit.Test;
import java.util.concurrent.atomic.AtomicReference;
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.junit.Assert.fail;
public class IrisColumnSampleTest {
@Test
public void unavailableFieldsHaveUnambiguousSentinels() {
IrisColumnSample sample = sample(
IrisColumnSample.UNAVAILABLE_HEIGHT,
IrisColumnSample.UNAVAILABLE_HEIGHT,
IrisSurfaceKind.UNKNOWN,
null,
IrisRiverState.NONE,
IrisColumnSample.UNAVAILABLE_RIVER_DISTANCE,
IrisColumnSample.UNAVAILABLE_RIVER_FLOW,
IrisColumnSample.UNAVAILABLE_HEIGHT
);
assertFalse(sample.hasSurfaceHeight());
assertFalse(sample.hasNaturalHeight());
assertFalse(sample.hasSurfaceKind());
assertFalse(sample.hasBiomeKey());
assertFalse(sample.hasRiverState());
assertFalse(sample.hasRiverDistance());
assertFalse(sample.hasRiverFlow());
assertFalse(sample.hasRiverWaterSurfaceY());
}
@Test
public void negativeWorldHeightsRemainAvailableValues() {
IrisColumnSample sample = sample(
-1,
-64,
IrisSurfaceKind.DRY_CHANNEL,
"test:river",
IrisRiverState.DRY,
0D,
0,
-1
);
assertTrue(sample.hasSurfaceHeight());
assertTrue(sample.hasNaturalHeight());
assertTrue(sample.hasSurfaceKind());
assertTrue(sample.hasBiomeKey());
assertTrue(sample.hasRiverState());
assertTrue(sample.hasRiverDistance());
assertTrue(sample.hasRiverFlow());
assertTrue(sample.hasRiverWaterSurfaceY());
}
@Test
public void blankBiomeKeysNormalizeToUnavailable() {
IrisColumnSample sample = sample(
64,
65,
IrisSurfaceKind.LAND,
" ",
IrisRiverState.NONE,
IrisColumnSample.UNAVAILABLE_RIVER_DISTANCE,
IrisColumnSample.UNAVAILABLE_RIVER_FLOW,
IrisColumnSample.UNAVAILABLE_HEIGHT
);
assertNull(sample.biomeKey());
assertFalse(sample.hasBiomeKey());
}
@Test
public void theSinkReceivesTheTypedSample() {
IrisColumnSample sample = sample(
64,
65,
IrisSurfaceKind.RIVER,
"test:river",
IrisRiverState.WET,
0.5D,
3,
67
);
AtomicReference<IrisColumnSample> received = new AtomicReference<>();
IrisColumnSink sink = received::set;
sink.accept(sample);
assertSame(sample, received.get());
}
@Test
public void invalidHydrologyValuesAreRejected() {
assertInvalid(Double.POSITIVE_INFINITY, 1);
assertInvalid(-0.1D, 1);
assertInvalid(0D, -2);
}
private static void assertInvalid(double distance, int flow) {
try {
sample(
64,
65,
IrisSurfaceKind.RIVER,
"test:river",
IrisRiverState.WET,
distance,
flow,
67
);
fail("Expected invalid hydrology values to be rejected");
} catch (IllegalArgumentException expected) {
assertTrue(expected.getMessage().startsWith("river"));
}
}
private static IrisColumnSample sample(
int surfaceHeight,
int naturalHeight,
IrisSurfaceKind surfaceKind,
String biomeKey,
IrisRiverState riverState,
double riverDistance,
int riverFlow,
int riverWaterSurfaceY
) {
return new IrisColumnSample(
12,
-7,
surfaceHeight,
naturalHeight,
surfaceKind,
biomeKey,
riverState,
riverDistance,
riverFlow,
riverWaterSurfaceY
);
}
}
@@ -1,10 +1,8 @@
package art.arcane.iris.core.commands;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IObjectPlacer;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.util.project.stream.ProceduralStream;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
@@ -15,31 +13,22 @@ import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class CommandObjectFluidHeightTest {
@Test
public void absoluteObjectPlacementShiftsTheColumnRiverHeadFromNegativeMinY() {
public void absoluteObjectPlacementShiftsFluidHeightFromNegativeMinY() {
World world = mock(World.class);
Engine engine = mock(Engine.class);
IrisDimension dimension = mock(IrisDimension.class);
IrisComplex complex = mock(IrisComplex.class);
@SuppressWarnings("unchecked")
ProceduralStream<Double> riverHead = mock(ProceduralStream.class);
Map<Block, BlockData> future = new HashMap<>();
when(engine.getMinHeight()).thenReturn(-64);
when(engine.getDimension()).thenReturn(dimension);
when(engine.getComplex()).thenReturn(complex);
when(dimension.getFluidHeight()).thenReturn(127);
when(complex.getRiverWaterSurfaceStream()).thenReturn(riverHead);
when(riverHead.get(12, -7)).thenReturn(131D);
IObjectPlacer placer = CommandObject.createPlacer(world, future, engine);
assertEquals(63, placer.getFluidHeight());
assertEquals(67, placer.getFluidHeight(12, -7));
verify(riverHead).get(12, -7);
}
}
@@ -2,15 +2,8 @@ package art.arcane.iris.core.service;
import art.arcane.iris.api.terrain.IrisColumnField;
import art.arcane.iris.api.terrain.IrisColumnQuery;
import art.arcane.iris.api.terrain.IrisRiverState;
import art.arcane.iris.api.terrain.IrisSurfaceKind;
import art.arcane.iris.api.terrain.IrisTerrainService;
import art.arcane.iris.engine.river.RiverEdgeId;
import art.arcane.iris.engine.river.RiverNodeId;
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.util.common.plugin.IrisService;
import org.bukkit.World;
import org.junit.Test;
@@ -89,7 +82,9 @@ public class IrisTerrainSVCTest {
IrisTerrainSVC service = new IrisTerrainSVC();
AtomicInteger sinkCalls = new AtomicInteger();
boolean answered = service.sampleColumns(null, SMALL, sample -> sinkCalls.incrementAndGet());
boolean answered = service.sampleColumns(null, SMALL,
(int blockX, int blockZ, int surfaceHeight, IrisSurfaceKind kind, String biomeKey)
-> sinkCalls.incrementAndGet());
assertFalse(answered);
assertEquals(0, sinkCalls.get());
@@ -102,38 +97,4 @@ public class IrisTerrainSVCTest {
assertFalse(service.sampleColumns(null, null, null));
assertFalse(service.sampleColumns(null, SMALL, null));
}
@Test
public void riverRouteStatesMapToThePublicDiagnosticStates() {
assertEquals(IrisRiverState.NONE, IrisTerrainSVC.riverState(
new IrisRiverSurfaceSample(RiverSample.none(), 70D, 70D, 70D, false, false)
));
assertEquals(IrisRiverState.WET, IrisTerrainSVC.riverState(river(RiverRouteState.WET)));
assertEquals(IrisRiverState.DRY, IrisTerrainSVC.riverState(river(RiverRouteState.DRY)));
assertEquals(IrisRiverState.NONE, IrisTerrainSVC.riverState(river(RiverRouteState.SUPPRESSED)));
}
private static IrisRiverSurfaceSample river(RiverRouteState state) {
RiverSection section = switch (state) {
case WET -> RiverSection.CHANNEL;
case DRY -> RiverSection.DRY_CHANNEL;
case SUPPRESSED -> RiverSection.NONE;
};
RiverSample sample = new RiverSample(
true,
state,
section,
0D,
0.5D,
1D,
1,
1,
8D,
4D,
3D,
false,
RiverEdgeId.of(new RiverNodeId(0, 0), new RiverNodeId(1, 0))
);
return new IrisRiverSurfaceSample(sample, 70D, 60D, 63D, false, state == RiverRouteState.WET);
}
}
@@ -2,12 +2,6 @@ package art.arcane.iris.core.service.terrain;
import art.arcane.iris.api.terrain.IrisSurfaceKind;
import art.arcane.iris.engine.object.InferredType;
import art.arcane.iris.engine.river.RiverEdgeId;
import art.arcane.iris.engine.river.RiverNodeId;
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 org.junit.Test;
import static org.junit.Assert.assertEquals;
@@ -64,86 +58,4 @@ public class IrisSurfaceClassifierTest {
}
}
}
@Test
public void activeRiverGeometryOverridesGenericOceanAndLandKinds() {
assertEquals(IrisSurfaceKind.RIVER, IrisSurfaceClassifier.classify(
60,
63,
InferredType.SEA,
river(RiverRouteState.WET, RiverSection.CHANNEL)
));
assertEquals(IrisSurfaceKind.RIVER, IrisSurfaceClassifier.classify(
60,
63,
InferredType.SEA,
river(RiverRouteState.WET, RiverSection.MOUTH)
));
assertEquals(IrisSurfaceKind.RIVER_SHORE, IrisSurfaceClassifier.classify(
64,
63,
InferredType.SHORE,
river(RiverRouteState.WET, RiverSection.BANK)
));
assertEquals(IrisSurfaceKind.DRY_CHANNEL, IrisSurfaceClassifier.classify(
60,
60,
InferredType.LAND,
river(RiverRouteState.DRY, RiverSection.DRY_CHANNEL)
));
assertEquals(IrisSurfaceKind.LAND, IrisSurfaceClassifier.classify(
60,
60,
InferredType.LAND,
river(RiverRouteState.DRY, RiverSection.DRY_BANK)
));
}
@Test
public void voidClassificationWinsOverRiverGeometry() {
for (RiverSection section : RiverSection.values()) {
if (section == RiverSection.NONE) {
continue;
}
RiverRouteState state = section == RiverSection.DRY_CHANNEL || section == RiverSection.DRY_BANK
? RiverRouteState.DRY
: RiverRouteState.WET;
assertEquals(IrisSurfaceKind.VOID, IrisSurfaceClassifier.classify(
0,
63,
InferredType.LAND,
river(state, section)
));
}
}
@Test
public void suppressedRoutesDoNotCreatePublicRiverSurfaceKinds() {
assertEquals(IrisSurfaceKind.LAND, IrisSurfaceClassifier.classify(
64,
63,
InferredType.LAND,
river(RiverRouteState.SUPPRESSED, RiverSection.CHANNEL)
));
}
private static IrisRiverSurfaceSample river(RiverRouteState state, RiverSection section) {
RiverSample sample = new RiverSample(
true,
state,
section,
0D,
0.5D,
1D,
1,
1,
8D,
4D,
3D,
false,
RiverEdgeId.of(new RiverNodeId(0, 0), new RiverNodeId(1, 0))
);
double waterSurface = state == RiverRouteState.WET ? 63D : 60D;
return new IrisRiverSurfaceSample(sample, 70D, 60D, waterSurface, false, state == RiverRouteState.WET);
}
}
@@ -126,7 +126,7 @@ public final class ModdedVisionOverlay implements GuiOverlay {
}
IrisComplex complex = engine.getComplex();
File file = switch (type) {
case BIOME, LAYER_LOAD, DECORATOR_LOAD, OBJECT_LOAD, HEIGHT, RIVER ->
case BIOME, LAYER_LOAD, DECORATOR_LOAD, OBJECT_LOAD, HEIGHT ->
complex.getTrueBiomeStream().get(worldX, worldZ).openInVSCode();
case BIOME_LAND -> complex.getLandBiomeStream().get(worldX, worldZ).openInVSCode();
case BIOME_SEA -> complex.getSeaBiomeStream().get(worldX, worldZ).openInVSCode();
@@ -26,7 +26,6 @@ import art.arcane.iris.engine.framework.render.RenderType;
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.river.RiverSection;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.localization.MessageArgument;
@@ -70,7 +69,6 @@ import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
@@ -475,33 +473,11 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi
}
private void renderLegend(Graphics2D canvas) {
if (currentType == RenderType.RIVER) {
renderRiverLegend(canvas);
} else if (currentType == RenderType.HEIGHT) {
if (currentType == RenderType.HEIGHT) {
renderHeightLegend(canvas);
}
}
private void renderRiverLegend(Graphics2D canvas) {
RiverSection[] sections = RiverSection.values();
int lineHeight = 18;
int width = 148;
int height = sections.length * lineHeight + CARD_PADDING * 2;
int x = getWidth() - width - CARD_PADDING;
int y = getHeight() - STATUS_HEIGHT - PROGRESS_HEIGHT - height - CARD_PADDING;
drawCardBackground(canvas, x, y, width, height);
canvas.setFont(BODY_FONT);
for (int index = 0; index < sections.length; index++) {
RiverSection section = sections[index];
int rowY = y + CARD_PADDING + index * lineHeight;
canvas.setColor(new Color(IrisRenderer.riverColor(section)));
canvas.fillRoundRect(x + CARD_PADDING, rowY + 2, 12, 12, 4, 4);
canvas.setColor(TEXT_SECONDARY);
String label = section.name().toLowerCase(Locale.ROOT).replace('_', ' ');
canvas.drawString(label, x + CARD_PADDING + 20, rowY + 13);
}
}
private void renderHeightLegend(Graphics2D canvas) {
int width = 244;
int height = 54;
@@ -1027,7 +1003,6 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi
case BIOME_SEA -> DesktopUiMessages.VISION_MODE_BIOME_SEA;
case REGION -> DesktopUiMessages.VISION_MODE_REGION;
case CAVE_LAND -> DesktopUiMessages.VISION_MODE_CAVE_LAND;
case RIVER -> DesktopUiMessages.VISION_MODE_RIVER;
case HEIGHT -> DesktopUiMessages.VISION_MODE_HEIGHT;
case OBJECT_LOAD -> DesktopUiMessages.VISION_MODE_OBJECT_LOAD;
case DECORATOR_LOAD -> DesktopUiMessages.VISION_MODE_DECORATOR_LOAD;
@@ -275,8 +275,7 @@ final class VisionRenderController implements AutoCloseable {
if (!isCurrent(work)) {
return;
}
int admissionLimit = work.frame().spec().type() == RenderType.RIVER ? 1 : renderWorkerCount;
while (work.inFlight() < admissionLimit) {
while (work.inFlight() < renderWorkerCount) {
VisibleTile tile = work.nextMissing();
if (tile == null) {
return;
@@ -49,7 +49,6 @@ public final class DesktopUiMessages {
public static final TextKey VISION_MODE_BIOME_SEA = TextKey.of("iris.desktop.vision.mode.biome_sea", "Biome sea");
public static final TextKey VISION_MODE_REGION = TextKey.of("iris.desktop.vision.mode.region", "Region");
public static final TextKey VISION_MODE_CAVE_LAND = TextKey.of("iris.desktop.vision.mode.cave_land", "Cave land");
public static final TextKey VISION_MODE_RIVER = TextKey.of("iris.desktop.vision.mode.river", "River network");
public static final TextKey VISION_MODE_HEIGHT = TextKey.of("iris.desktop.vision.mode.height", "Height");
public static final TextKey VISION_MODE_OBJECT_LOAD = TextKey.of("iris.desktop.vision.mode.object_load", "Object load");
public static final TextKey VISION_MODE_DECORATOR_LOAD = TextKey.of("iris.desktop.vision.mode.decorator_load", "Decorator load");
@@ -168,7 +167,7 @@ public final class DesktopUiMessages {
VISION_HELP_RESET_ZOOM, VISION_HELP_CYCLE_MODE, VISION_HELP_FPS,
VISION_HELP_GRID, VISION_HELP_BIOME, VISION_HELP_TELEPORT, VISION_HELP_EDITOR, VISION_OPENED,
VISION_TELEPORTING, VISION_MODE_BIOME, VISION_MODE_BIOME_LAND, VISION_MODE_BIOME_SEA,
VISION_MODE_REGION, VISION_MODE_CAVE_LAND, VISION_MODE_RIVER, VISION_MODE_HEIGHT, VISION_MODE_OBJECT_LOAD,
VISION_MODE_REGION, VISION_MODE_CAVE_LAND, VISION_MODE_HEIGHT, VISION_MODE_OBJECT_LOAD,
VISION_MODE_DECORATOR_LOAD, VISION_MODE_CONTINENT, VISION_MODE_LAYER_LOAD, NOISE_TITLE,
NOISE_TITLE_GENERATOR, NOISE_SEARCH, NOISE_STATUS, NOISE_CATEGORY_CUSTOM,
NOISE_CATEGORY_PACK_GENERATORS, NOISE_CATEGORY_SIMPLEX, NOISE_CATEGORY_PERLIN,
File diff suppressed because it is too large Load Diff
@@ -84,9 +84,6 @@ public final class PackValidator {
packFolder, dimensionFiles, validateLiveRegistries);
addDistinct(blockingErrors, imageMaps.errors());
addDistinct(warnings, imageMaps.warnings());
PackRiverValidator.Validation riverValidation = PackRiverValidator.validate(packFolder, dimensionFiles);
addDistinct(blockingErrors, riverValidation.errors());
addDistinct(warnings, riverValidation.warnings());
blockingErrors.addAll(PackCaveProfileValidator.validateLegacyFields(packFolder));
PackLootValidator.LootGraphIssues lootIssues = PackLootValidator.validateLootGraph(packFolder);
addDistinct(blockingErrors, lootIssues.errors());
@@ -29,22 +29,8 @@ import art.arcane.iris.engine.object.IrisDecorationPart;
import art.arcane.iris.engine.object.IrisDecorator;
import art.arcane.iris.engine.object.IrisGenerator;
import art.arcane.iris.engine.object.IrisInterpolator;
import art.arcane.iris.engine.object.IrisMaterialPalette;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisRiverCaves;
import art.arcane.iris.engine.object.IrisRiverDeepPools;
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.IrisRiverWater;
import art.arcane.iris.engine.object.IrisShapedGeneratorStyle;
import art.arcane.iris.engine.river.runtime.IrisRiverRuntime;
import art.arcane.iris.engine.river.runtime.IrisRiverRuntimeContext;
import art.arcane.iris.engine.river.runtime.IrisRiverSurfaceSample;
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.cave.RiverCaveFluidKind;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.PlatformBiome;
@@ -67,6 +53,8 @@ import lombok.ToString;
import java.io.File;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
@@ -75,28 +63,37 @@ import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BiFunction;
@Data
@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "frozenInterpolators", "frozenGenerators", "riverRuntime", "imageMapRuntime"})
@ToString(exclude = {"data", "gridBoundsCache", "frozenInterpolators", "frozenGenerators", "riverRuntime", "imageMapRuntime"})
@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "imageMapRuntime"})
@ToString(exclude = {"data", "gridBoundsCache", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "imageMapRuntime"})
public class IrisComplex implements DataProvider {
private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D);
private static final AtomicLong lastBoundsFailureLog = new AtomicLong(0L);
private static final int GRID_BOUNDS_CACHE_SIZE = 8192;
private static final int HEIGHT_BOUNDS_GRID = 4;
private static final Comparator<IrisInterpolator> INTERPOLATOR_ORDER = Comparator
.comparing((IrisInterpolator interpolator) -> interpolator.getFunction().name())
.thenComparingDouble(IrisInterpolator::getHorizontalScale);
private static final Comparator<IrisGenerator> GENERATOR_ORDER = Comparator.comparing(
IrisGenerator::getLoadKey,
Comparator.nullsFirst(Comparator.naturalOrder())
);
private static final InferredType[] INFERRED_BIOME_PREPARATION_ORDER = {
InferredType.LAND,
InferredType.CAVE,
InferredType.SEA,
InferredType.SHORE
};
@Getter(AccessLevel.NONE)
private final transient ThreadLocal<GridBoundsCache> gridBoundsCache = ThreadLocal.withInitial(GridBoundsCache::new);
/**
* Immutable snapshot of {@link #generators} taken once at the end of construction, in the exact
* iteration order the map produces. The per-column height paths walk these arrays instead of
* allocating map/set iterators, and the frozen order keeps the floating point accumulation order
* identical to the map iteration it replaces. Mutating {@link #generators} after construction is
* not reflected here.
*/
@Getter(AccessLevel.NONE)
private final transient IrisInterpolator[] frozenInterpolators;
@Getter(AccessLevel.NONE)
private final transient IrisGenerator[][] frozenGenerators;
@Getter(AccessLevel.NONE)
private final transient Map<IrisRegion, Map<InferredType, ProceduralStream<IrisBiome>>> inferredBiomeStreams;
private RNG rng;
private double fluidHeight;
private IrisData data;
@@ -112,22 +109,14 @@ public class IrisComplex implements DataProvider {
private ProceduralStream<IrisBiome> shoreBiomeStream;
private ProceduralStream<IrisBiome> baseBiomeStream;
private ProceduralStream<UUID> baseBiomeIDStream;
private ProceduralStream<IrisBiome> naturalTrueBiomeStream;
private ProceduralStream<IrisBiome> trueBiomeStream;
private ProceduralStream<PlatformBiome> trueBiomeDerivativeStream;
private ProceduralStream<Double> naturalHeightStream;
private ProceduralStream<Double> heightStream;
private ProceduralStream<Integer> roundedHeighteightStream;
private ProceduralStream<Double> maxHeightStream;
private ProceduralStream<Double> overlayStream;
private ProceduralStream<Double> heightFluidStream;
private ProceduralStream<Double> naturalSlopeStream;
private ProceduralStream<Double> slopeStream;
private ProceduralStream<IrisRiverSurfaceSample> riverSurfaceStream;
private ProceduralStream<Double> riverDistanceStream;
private ProceduralStream<Double> riverFlowStream;
private ProceduralStream<Double> riverCarveWeightStream;
private ProceduralStream<Double> riverWaterSurfaceStream;
private ProceduralStream<Integer> topSurfaceStream;
private ProceduralStream<IrisDecorator> terrainSurfaceDecoration;
private ProceduralStream<IrisDecorator> terrainCeilingDecoration;
@@ -138,13 +127,10 @@ public class IrisComplex implements DataProvider {
private ProceduralStream<IrisDecorator> shoreSurfaceDecoration;
private ProceduralStream<PlatformBlockState> rockStream;
private ProceduralStream<PlatformBlockState> fluidStream;
private ProceduralStream<PlatformBlockState> riverFluidStream;
private ProceduralStream<PlatformBlockState> riverDeepPoolFluidStream;
private IrisBiome focusBiome;
private IrisRegion focusRegion;
private Map<IrisInterpolator, IdentityHashMap<IrisBiome, GeneratorBounds>> generatorBounds;
private Set<IrisBiome> generatorBiomes;
private IrisRiverRuntime riverRuntime;
private transient IrisImageMapRuntime imageMapRuntime;
// Copy-on-write: reads happen per column on every burst thread; the synchronizedMap
// monitor was taken on every HIT. Writes are once per biome and bounded, so a fresh map
@@ -170,6 +156,7 @@ public class IrisComplex implements DataProvider {
focusBiome = engine.getFocus();
focusRegion = engine.getFocusRegion();
Map<InferredType, ProceduralStream<IrisBiome>> inferredStreams = new HashMap<>();
KList<IrisRegion> preparedRegions = new KList<>();
if (focusBiome != null) {
focusBiome = focusBiome.withInferredType(InferredType.LAND);
@@ -178,31 +165,34 @@ public class IrisComplex implements DataProvider {
//@builder
if (focusRegion != null) {
prepareInferredBiomes(focusRegion);
focusRegion.getNaturalBiomes(this).forEach(this::registerGenerators);
prepareInferredBiomes(focusRegion, preparedRegions);
focusRegion.getAllBiomes(this).forEach(this::registerGenerators);
} else {
engine.getDimension().getRegions().forEach(regionKey -> {
IrisRegion region = data.getRegionLoader().load(regionKey);
if (region == null) {
return;
}
prepareInferredBiomes(region);
region.getNaturalBiomes(this).forEach(this::registerGenerators);
prepareInferredBiomes(region, preparedRegions);
region.getAllBiomes(this).forEach(this::registerGenerators);
});
for (IrisRegion region : imageMapRuntime.getMappedRegions()) {
prepareInferredBiomes(region);
region.getNaturalBiomes(this).forEach(this::registerGenerators);
prepareInferredBiomes(region, preparedRegions);
region.getAllBiomes(this).forEach(this::registerGenerators);
}
imageMapRuntime.getMappedBiomes().forEach(this::registerGenerators);
}
int interpolatorCount = generators.size();
inferredBiomeStreams = compileInferredBiomeStreams(
preparedRegions,
(region, inferredType) -> compileInferredBiomeStream(engine, region, inferredType, emptyBiome)
);
GeneratorGroup[] generatorGroups = freezeGeneratorGroups(generators);
int interpolatorCount = generatorGroups.length;
frozenInterpolators = new IrisInterpolator[interpolatorCount];
frozenGenerators = new IrisGenerator[interpolatorCount][];
int frozenIndex = 0;
for (Map.Entry<IrisInterpolator, Set<IrisGenerator>> entry : generators.entrySet()) {
frozenInterpolators[frozenIndex] = entry.getKey();
frozenGenerators[frozenIndex] = entry.getValue().toArray(new IrisGenerator[0]);
frozenIndex++;
for (int frozenIndex = 0; frozenIndex < generatorGroups.length; frozenIndex++) {
frozenInterpolators[frozenIndex] = generatorGroups[frozenIndex].interpolator();
frozenGenerators[frozenIndex] = generatorGroups[frozenIndex].generators();
}
generatorBounds = buildGeneratorBounds(engine);
KList<IrisShapedGeneratorStyle> overlayNoise = engine.getDimension().getOverlayNoise();
@@ -221,29 +211,6 @@ public class IrisComplex implements DataProvider {
.select(engine.getDimension().getRockPalette().getBlockData(data));
fluidStream = engine.getDimension().getFluidPalette().getLayerGenerator(rng.nextParallelRNG(78), data).stream()
.select(engine.getDimension().getFluidPalette().getBlockData(data));
IrisRiverNetwork configuredRivers = engine.getDimension().getRivers();
IrisRiverWater configuredRiverWater = configuredRivers == null ? null : configuredRivers.getWater();
riverFluidStream = configuredRivers != null && configuredRivers.isEnabled()
? configuredFluidStream(
Objects.requireNonNull(configuredRiverWater).getFluidPalette(),
rng.nextParallelRNG(79),
"River water"
)
: fluidStream;
IrisRiverCaves configuredRiverCaves = configuredRivers == null ? null : configuredRivers.getCaves();
IrisRiverDeepPools configuredDeepPools = configuredRiverCaves == null
? null
: configuredRiverCaves.getDeepPools();
riverDeepPoolFluidStream = configuredRivers != null
&& configuredRivers.isEnabled()
&& configuredDeepPools != null
&& configuredDeepPools.isEnabled()
? configuredFluidStream(
configuredDeepPools.getFluidPalette(),
rng.nextParallelRNG(80),
"River deep-pool"
)
: riverFluidStream;
regionStyleStream = engine.getDimension().getRegionStyle().create(rng.nextParallelRNG(883), getData()).stream()
.zoom(engine.getDimension().getRegionZoom());
regionIdentityStream = regionStyleStream.fit(Integer.MIN_VALUE, Integer.MAX_VALUE);
@@ -262,41 +229,22 @@ public class IrisComplex implements DataProvider {
regionIDStream = regionIdentityStream.convertCached((i) -> new UUID(Double.doubleToLongBits(i),
String.valueOf(i * 38445).hashCode() * 3245556666L));
caveBiomeStream = regionStream.contextInjecting(engine, (c, x, z) -> c.getRegion().get(x, z))
.convert((r)
-> engine.getDimension().getCaveBiomeStyle().create(rng.nextParallelRNG(InferredType.CAVE.ordinal()), getData()).stream()
.zoom(engine.getDimension().getBiomeZoom())
.zoom(r.getCaveBiomeZoom())
.selectRarity(loadInferredBiomes(r.getCaveBiomes(), InferredType.CAVE))
.onNull(emptyBiome)
).convertAware2D(ProceduralStream::get).cache2D("caveBiomeStream", engine, cacheSize);
.convert((r) -> createInferredBiomeStream(r, InferredType.CAVE))
.convertAware2D(ProceduralStream::get).cache2D("caveBiomeStream", engine, cacheSize);
inferredStreams.put(InferredType.CAVE, caveBiomeStream);
landBiomeStream = regionStream.contextInjecting(engine, (c, x, z) -> c.getRegion().get(x, z))
.convert((r)
-> engine.getDimension().getLandBiomeStyle().create(rng.nextParallelRNG(InferredType.LAND.ordinal()), getData()).stream()
.zoom(engine.getDimension().getBiomeZoom())
.zoom(engine.getDimension().getLandZoom())
.zoom(r.getLandBiomeZoom())
.selectRarity(loadInferredBiomes(r.getLandBiomes(), InferredType.LAND))
).convertAware2D(ProceduralStream::get)
.convert((r) -> createInferredBiomeStream(r, InferredType.LAND))
.convertAware2D(ProceduralStream::get)
.cache2D("landBiomeStream", engine, cacheSize);
inferredStreams.put(InferredType.LAND, landBiomeStream);
seaBiomeStream = regionStream.contextInjecting(engine, (c, x, z) -> c.getRegion().get(x, z))
.convert((r)
-> engine.getDimension().getSeaBiomeStyle().create(rng.nextParallelRNG(InferredType.SEA.ordinal()), getData()).stream()
.zoom(engine.getDimension().getBiomeZoom())
.zoom(engine.getDimension().getSeaZoom())
.zoom(r.getSeaBiomeZoom())
.selectRarity(loadInferredBiomes(r.getSeaBiomes(), InferredType.SEA))
).convertAware2D(ProceduralStream::get)
.convert((r) -> createInferredBiomeStream(r, InferredType.SEA))
.convertAware2D(ProceduralStream::get)
.cache2D("seaBiomeStream", engine, cacheSize);
inferredStreams.put(InferredType.SEA, seaBiomeStream);
shoreBiomeStream = regionStream.contextInjecting(engine, (c, x, z) -> c.getRegion().get(x, z))
.convert((r)
-> engine.getDimension().getShoreBiomeStyle().create(rng.nextParallelRNG(InferredType.SHORE.ordinal()), getData()).stream()
.zoom(engine.getDimension().getBiomeZoom())
.zoom(r.getShoreBiomeZoom())
.selectRarity(loadInferredBiomes(r.getShoreBiomes(), InferredType.SHORE))
).convertAware2D(ProceduralStream::get).cache2D("shoreBiomeStream", engine, cacheSize);
.convert((r) -> createInferredBiomeStream(r, InferredType.SHORE))
.convertAware2D(ProceduralStream::get).cache2D("shoreBiomeStream", engine, cacheSize);
inferredStreams.put(InferredType.SHORE, shoreBiomeStream);
bridgeStream = focusBiome != null ? ProceduralStream.of((x, z) -> focusBiome.getInferredType(),
Interpolated.of(a -> 0D, a -> focusBiome.getInferredType())) :
@@ -315,90 +263,28 @@ public class IrisComplex implements DataProvider {
return mapped == null ? biome : mapped;
})
.cache2D("imageMappedBaseBiomeStream", engine, cacheSize);
naturalHeightStream = ProceduralStream.of((x, z) -> {
IrisBiome b = focusBiome != null ? focusBiome : baseBiomeStream.get(x, z);
double proceduralHeight = getHeight(engine, b, x, z, engine.getSeedManager().getHeight());
heightStream = ProceduralStream.of((x, z) -> {
double proceduralHeight = getHeight(engine, x, z, engine.getSeedManager().getHeight());
return imageMapRuntime.sampleTerrainHeight(x, z, proceduralHeight);
}, Interpolated.DOUBLE).cache2DDouble("naturalHeightStream", engine, cacheSize);
naturalSlopeStream = naturalHeightStream.slope(3)
.cache2DDouble("naturalSlopeStream", engine, cacheSize);
naturalTrueBiomeStream = focusBiome != null ? ProceduralStream.of((x, y) -> focusBiome, Interpolated.of(a -> 0D,
}, Interpolated.DOUBLE).cache2DDouble("heightStream", engine, cacheSize);
slopeStream = heightStream.slope(3)
.cache2DDouble("slopeStream", engine, cacheSize);
trueBiomeStream = focusBiome != null ? ProceduralStream.of((x, y) -> focusBiome, Interpolated.of(a -> 0D,
b -> focusBiome))
.cache2D("naturalTrueBiomeStream-focus", engine, cacheSize) : naturalHeightStream
.cache2D("trueBiomeStream-focus", engine, cacheSize) : heightStream
.convertAware2D((h, x, z) -> {
IrisBiome mapped = imageMapRuntime.sampleBiome(x, z);
return mapped == null
? fixBiomeType(h, baseBiomeStream.get(x, z), regionStream.get(x, z), x, z, fluidHeight)
: mapped;
})
.cache2D("naturalTrueBiomeStream", engine, cacheSize);
if (engine.getDimension().getRivers() != null && engine.getDimension().getRivers().isEnabled()) {
ProceduralStream<Boolean> naturalOceanStream = createNaturalOceanStream(
bridgeStream,
focusBiome
).cache2D("naturalOceanStream", engine, cacheSize);
int riverFluidHeight = engine.getDimension().getRivers().getWater().getFluidHeight()
- engine.getDimension().getMinHeight();
riverRuntime = new IrisRiverRuntime(new IrisRiverRuntimeContext(
engine.getSeedManager().getBodies(),
engine.getDimension().getRivers(),
data,
riverFluidHeight,
(int) Math.round(fluidHeight),
IrisEngineMantle.isRiverHydrologyEnabled(engine.getDimension()),
IrisEngineMantle.isRiverCaveHydrologyEnabled(engine.getDimension()),
blockingRiverRoutingPossible(engine),
variableMaxIncisionPossible(engine),
biomeRiverOverridesPossible(focusBiome, generatorBiomes),
(blockX, blockZ) -> naturalHeightBounds(engine, overlayNoise, blockX, blockZ),
naturalHeightStream,
naturalSlopeStream,
naturalOceanStream,
naturalTrueBiomeStream,
regionStream
));
riverSurfaceStream = ProceduralStream.of(
(x, z) -> riverRuntime.sample(x, z),
Interpolated.of(
IrisRiverSurfaceSample::terrainHeight,
value -> IrisRiverSurfaceSample.none(value, fluidHeight)
)
)
.cache2D("riverSurfaceStream", engine, cacheSize);
} else {
riverSurfaceStream = naturalHeightStream.convert(
value -> IrisRiverSurfaceSample.none(value, fluidHeight)
)
.cache2D("riverSurfaceStream-disabled", engine, cacheSize);
}
heightStream = riverSurfaceStream.convert(IrisRiverSurfaceSample::terrainHeight)
.cache2DDouble("heightStream", engine, cacheSize);
.cache2D("trueBiomeStream", engine, cacheSize);
roundedHeighteightStream = heightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z))
.round();
slopeStream = heightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z))
.slope(3).cache2DDouble("slopeStream", engine, cacheSize);
trueBiomeStream = focusBiome != null ? ProceduralStream.of((x, y) -> focusBiome, Interpolated.of(a -> 0D,
b -> focusBiome))
.cache2D("trueBiomeStream-focus", engine, cacheSize) : riverSurfaceStream
.convertAware2D((sample, x, z) -> resolveRiverSurfaceBiome(sample, x, z))
.cache2D("trueBiomeStream", engine, cacheSize);
trueBiomeDerivativeStream = trueBiomeStream.contextInjecting(engine, (c, x, z) -> c.getBiome().get(x, z))
.convert((b) -> IrisPlatforms.get().registries().biome(b.getDerivativeKey())).cache2D("trueBiomeDerivativeStream", engine, cacheSize);
riverDistanceStream = riverSurfaceStream.convert(sample -> sample.river().present()
? sample.river().distance()
: Double.MAX_VALUE)
.cache2DDouble("riverDistanceStream", engine, cacheSize);
riverFlowStream = riverSurfaceStream.convert(sample -> (double) sample.river().flow())
.cache2DDouble("riverFlowStream", engine, cacheSize);
riverCarveWeightStream = riverSurfaceStream.convert(sample -> sample.river().carveWeight())
.cache2DDouble("riverCarveWeightStream", engine, cacheSize);
riverWaterSurfaceStream = riverSurfaceStream.convert(IrisRiverSurfaceSample::waterSurfaceY)
.cache2DDouble("riverWaterSurfaceStream", engine, cacheSize);
heightFluidStream = ProceduralStream.ofDouble((x, z) -> Math.max(
heightStream.get(x, z),
riverWaterSurfaceStream.get(x, z)
))
.cache2DDouble("heightFluidStream", engine, cacheSize);
heightFluidStream = heightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z))
.max(fluidHeight).cache2DDouble("heightFluidStream", engine, cacheSize);
maxHeightStream = ProceduralStream.ofDouble((x, z) -> height);
terrainSurfaceDecoration = trueBiomeStream.contextInjecting(engine, (c, x, z) -> c.getBiome().get(x, z))
.convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.NONE)).cache2D("terrainSurfaceDecoration", engine, cacheSize);
@@ -424,116 +310,6 @@ public class IrisComplex implements DataProvider {
//@done
}
private boolean blockingRiverRoutingPossible(Engine engine) {
if (blocksRiverRouting(focusRegion == null ? null : focusRegion.getRiverOverride())
|| blocksRiverRouting(focusBiome == null ? null : focusBiome.getRiverOverride())) {
return true;
}
KList<IrisRegion> regions = engine.getDimension().getAllRegions(engine);
for (IrisRegion loadedRegion : regions) {
if (loadedRegion != null && blocksRiverRouting(loadedRegion.getRiverOverride())) {
return true;
}
}
KList<IrisBiome> biomes = engine.getDimension().getReachableBiomes(engine);
for (IrisBiome biome : biomes) {
if (biome != null && blocksRiverRouting(biome.getRiverOverride())) {
return true;
}
}
return false;
}
private static boolean blocksRiverRouting(IrisRiverOverride override) {
return override != null && override.getRoutingPolicy() == IrisRiverRoutingPolicy.BLOCK;
}
private boolean variableMaxIncisionPossible(Engine engine) {
if (changesMaxIncision(focusRegion == null ? null : focusRegion.getRiverOverride())
|| changesMaxIncision(focusBiome == null ? null : focusBiome.getRiverOverride())) {
return true;
}
KList<IrisRegion> regions = engine.getDimension().getAllRegions(engine);
for (IrisRegion loadedRegion : regions) {
if (loadedRegion != null && changesMaxIncision(loadedRegion.getRiverOverride())) {
return true;
}
}
KList<IrisBiome> biomes = engine.getDimension().getReachableBiomes(engine);
for (IrisBiome biome : biomes) {
if (biome != null && changesMaxIncision(biome.getRiverOverride())) {
return true;
}
}
return false;
}
static boolean changesMaxIncision(IrisRiverOverride override) {
if (override == null || override.getMaxIncisionMultiplier() == null) {
return false;
}
double multiplier = override.getMaxIncisionMultiplier();
return Double.isFinite(multiplier) && Double.compare(Math.max(0D, multiplier), 1D) != 0;
}
static boolean biomeRiverOverridesPossible(IrisBiome focusBiome, Iterable<IrisBiome> naturalBiomes) {
if (focusBiome != null) {
return focusBiome.getRiverOverride() != null;
}
for (IrisBiome biome : naturalBiomes) {
if (biome != null && biome.getRiverOverride() != null) {
return true;
}
}
return false;
}
static ProceduralStream<Boolean> createNaturalOceanStream(
ProceduralStream<InferredType> bridgeStream,
IrisBiome focusBiome
) {
if (focusBiome != null) {
boolean ocean = focusBiome.getInferredType() == InferredType.SEA;
return ProceduralStream.of((x, z) -> ocean, Interpolated.BOOLEAN);
}
return bridgeStream.convert(type -> type == InferredType.SEA);
}
private ProceduralStream<PlatformBlockState> configuredFluidStream(
IrisMaterialPalette palette,
RNG fluidRng,
String configurationName
) {
Objects.requireNonNull(palette, configurationName + " fluidPalette must be configured");
KList<PlatformBlockState> blocks = palette.getBlockData(data);
if (blocks.isEmpty()) {
throw new IllegalArgumentException(
configurationName + " fluidPalette must resolve at least one fluid block");
}
for (PlatformBlockState block : blocks) {
if (block == null || !block.isFluid()) {
throw new IllegalArgumentException(
configurationName + " fluidPalette may contain only fluid blocks");
}
}
return palette.getLayerGenerator(fluidRng, data).stream().select(blocks);
}
public PlatformBlockState resolveRiverCaveFluid(RiverCaveFluidKind fluidKind, double x, double z) {
return switch (Objects.requireNonNull(fluidKind)) {
case RIVER -> riverFluidStream.get(x, z);
case DEEP_POOL -> riverDeepPoolFluidStream.get(x, z);
};
}
public PlatformBlockState resolveSurfaceFluid(double x, double z) {
IrisRiverSurfaceSample sample = riverSurfaceStream.get(x, z);
if (sample.river().present() && sample.surfaceFluid()) {
return riverFluidStream.get(x, z);
}
return fluidStream.get(x, z);
}
public ProceduralStream<IrisBiome> getBiomeStream(InferredType type) {
switch (type) {
case CAVE:
@@ -555,7 +331,7 @@ public class IrisComplex implements DataProvider {
if (engine == null) {
throw new IllegalArgumentException("Engine is required to sample procedural terrain height");
}
return getHeight(engine, null, worldX, worldZ, engine.getSeedManager().getHeight());
return getHeight(engine, worldX, worldZ, engine.getSeedManager().getHeight());
}
private IrisRegion findRegion(IrisBiome focus, Engine engine) {
@@ -594,67 +370,6 @@ public class IrisComplex implements DataProvider {
return null;
}
private IrisBiome resolveRiverSurfaceBiome(IrisRiverSurfaceSample sample, double x, double z) {
IrisBiome mapped = imageMapRuntime.sampleBiome(x, z);
if (mapped != null && (riverRuntime == null || !sample.river().present())) {
return mapped;
}
if (riverRuntime != null) {
if (sample.subterranean()) {
return fixBiomeType(
sample.naturalHeight(),
baseBiomeStream.get(x, z),
regionStream.get(x, z),
x,
z,
fluidHeight
);
}
IrisBiome riverBiome = riverRuntime.selectSurfaceBiome(sample, x, z);
if (riverBiome != null) {
return implode(riverBiome, x, z);
}
InferredType directFallback = directRiverFallback(sample.river());
if (directFallback != null) {
IrisBiome baseBiome = baseBiomeStream.get(x, z);
return implode(baseBiome.withInferredType(directFallback), x, z);
}
if (sample.river().present()
&& sample.river().state() == RiverRouteState.WET
&& sample.river().section() == RiverSection.BANK) {
return fixBiomeType(
sample.terrainHeight(),
baseBiomeStream.get(x, z),
regionStream.get(x, z),
x,
z,
sample.waterSurfaceY()
);
}
}
return fixBiomeType(
sample.terrainHeight(),
baseBiomeStream.get(x, z),
regionStream.get(x, z),
x,
z,
fluidHeight
);
}
static InferredType directRiverFallback(RiverSample river) {
if (!river.present()) {
return null;
}
if (river.state() == RiverRouteState.DRY) {
return InferredType.LAND;
}
return switch (river.section()) {
case CHANNEL, MOUTH -> InferredType.SEA;
default -> null;
};
}
private IrisBiome fixBiomeType(Double height, IrisBiome biome, IrisRegion region, Double x, Double z, double fluidHeight) {
IrisBiome resolved = resolveSurfaceBiome(
height,
@@ -713,13 +428,7 @@ public class IrisComplex implements DataProvider {
double hi = sampledBounds.max();
double lo = sampledBounds.min();
double d = 0;
for (IrisGenerator i : generators) {
d += M.lerp(lo, hi, i.getHeight(x, z, seed + 239945));
}
return d / generators.length;
return averageGeneratorHeights(generators, lo, hi, x, z, seed + 239945);
}
private NoiseBounds gridSampleBounds(Engine engine, IrisInterpolator interpolator, int interpolatorIndex, IrisGenerator[] generators, double x, double z) {
@@ -742,6 +451,22 @@ public class IrisComplex implements DataProvider {
return new NoiseBounds(boundsLow(b00), boundsHigh(b00));
}
if (fz == 0D) {
long b10 = cornerBounds(cache, engine, interpolator, interpolatorIndex, generators, gx + grid, gz);
return new NoiseBounds(
biLerp(boundsLow(b00), boundsLow(b10), boundsLow(b00), boundsLow(b10), fx, fz),
biLerp(boundsHigh(b00), boundsHigh(b10), boundsHigh(b00), boundsHigh(b10), fx, fz)
);
}
if (fx == 0D) {
long b01 = cornerBounds(cache, engine, interpolator, interpolatorIndex, generators, gx, gz + grid);
return new NoiseBounds(
biLerp(boundsLow(b00), boundsLow(b00), boundsLow(b01), boundsLow(b01), fx, fz),
biLerp(boundsHigh(b00), boundsHigh(b00), boundsHigh(b01), boundsHigh(b01), fx, fz)
);
}
long b10 = cornerBounds(cache, engine, interpolator, interpolatorIndex, generators, gx + grid, gz);
long b01 = cornerBounds(cache, engine, interpolator, interpolatorIndex, generators, gx, gz + grid);
long b11 = cornerBounds(cache, engine, interpolator, interpolatorIndex, generators, gx + grid, gz + grid);
@@ -803,47 +528,94 @@ public class IrisComplex implements DataProvider {
return h;
}
private NoiseBounds naturalHeightBounds(
Engine engine,
KList<IrisShapedGeneratorStyle> overlayNoise,
double x,
double z
) {
double minimum = fluidHeight;
double maximum = fluidHeight;
for (int interpolatorIndex = 0; interpolatorIndex < frozenInterpolators.length; interpolatorIndex++) {
NoiseBounds bounds = gridSampleBounds(
engine,
frozenInterpolators[interpolatorIndex],
interpolatorIndex,
frozenGenerators[interpolatorIndex],
x,
z
);
minimum += Math.min(bounds.min(), bounds.max());
maximum += Math.max(bounds.min(), bounds.max());
}
for (IrisShapedGeneratorStyle style : overlayNoise) {
minimum += Math.min(style.getMin(), style.getMax());
maximum += Math.max(style.getMin(), style.getMax());
}
minimum = imageMapRuntime.sampleTerrainHeight(x, z, minimum);
maximum = imageMapRuntime.sampleTerrainHeight(x, z, maximum);
return new NoiseBounds(
Math.max(0D, Math.min(engine.getHeight(), Math.min(minimum, maximum))),
Math.max(0D, Math.min(engine.getHeight(), Math.max(minimum, maximum)))
);
}
private double getHeight(Engine engine, IrisBiome b, double x, double z, long seed) {
private double getHeight(Engine engine, double x, double z, long seed) {
return Math.max(Math.min(getInterpolatedHeight(engine, x, z, seed) + fluidHeight + overlayStream.get(x, z), engine.getHeight()), 0);
}
private void prepareInferredBiomes(IrisRegion region) {
private void prepareInferredBiomes(IrisRegion region, KList<IrisRegion> preparedRegions) {
loadInferredBiomes(region.getLandBiomes(), InferredType.LAND);
loadInferredBiomes(region.getCaveBiomes(), InferredType.CAVE);
loadInferredBiomes(region.getSeaBiomes(), InferredType.SEA);
loadInferredBiomes(region.getShoreBiomes(), InferredType.SHORE);
preparedRegions.add(region);
}
private ProceduralStream<IrisBiome> createInferredBiomeStream(
IrisRegion region,
InferredType inferredType
) {
return preparedInferredBiomeStream(inferredBiomeStreams, region, inferredType);
}
private ProceduralStream<IrisBiome> compileInferredBiomeStream(
Engine engine,
IrisRegion region,
InferredType inferredType,
IrisBiome emptyBiome
) {
return switch (inferredType) {
case CAVE -> engine.getDimension().getCaveBiomeStyle()
.create(rng.nextParallelRNG(InferredType.CAVE.ordinal()), getData()).stream()
.zoom(engine.getDimension().getBiomeZoom())
.zoom(region.getCaveBiomeZoom())
.selectRarity(loadInferredBiomes(region.getCaveBiomes(), InferredType.CAVE))
.onNull(emptyBiome);
case LAND -> engine.getDimension().getLandBiomeStyle()
.create(rng.nextParallelRNG(InferredType.LAND.ordinal()), getData()).stream()
.zoom(engine.getDimension().getBiomeZoom())
.zoom(engine.getDimension().getLandZoom())
.zoom(region.getLandBiomeZoom())
.selectRarity(loadInferredBiomes(region.getLandBiomes(), InferredType.LAND));
case SEA -> engine.getDimension().getSeaBiomeStyle()
.create(rng.nextParallelRNG(InferredType.SEA.ordinal()), getData()).stream()
.zoom(engine.getDimension().getBiomeZoom())
.zoom(engine.getDimension().getSeaZoom())
.zoom(region.getSeaBiomeZoom())
.selectRarity(loadInferredBiomes(region.getSeaBiomes(), InferredType.SEA));
case SHORE -> engine.getDimension().getShoreBiomeStyle()
.create(rng.nextParallelRNG(InferredType.SHORE.ordinal()), getData()).stream()
.zoom(engine.getDimension().getBiomeZoom())
.zoom(region.getShoreBiomeZoom())
.selectRarity(loadInferredBiomes(region.getShoreBiomes(), InferredType.SHORE));
};
}
static Map<IrisRegion, Map<InferredType, ProceduralStream<IrisBiome>>> compileInferredBiomeStreams(
Iterable<IrisRegion> regions,
BiFunction<IrisRegion, InferredType, ProceduralStream<IrisBiome>> compiler
) {
IdentityHashMap<IrisRegion, Map<InferredType, ProceduralStream<IrisBiome>>> compiled = new IdentityHashMap<>();
for (IrisRegion region : regions) {
if (compiled.containsKey(region)) {
continue;
}
EnumMap<InferredType, ProceduralStream<IrisBiome>> regionStreams = new EnumMap<>(InferredType.class);
for (InferredType inferredType : INFERRED_BIOME_PREPARATION_ORDER) {
regionStreams.put(inferredType, Objects.requireNonNull(compiler.apply(region, inferredType)));
}
compiled.put(region, Collections.unmodifiableMap(regionStreams));
}
return Collections.unmodifiableMap(compiled);
}
static ProceduralStream<IrisBiome> preparedInferredBiomeStream(
Map<IrisRegion, Map<InferredType, ProceduralStream<IrisBiome>>> streams,
IrisRegion region,
InferredType inferredType
) {
Map<InferredType, ProceduralStream<IrisBiome>> regionStreams = streams.get(region);
if (regionStreams == null) {
String regionKey = region == null || region.getLoadKey() == null || region.getLoadKey().isBlank()
? "<unkeyed>"
: region.getLoadKey();
throw new IllegalStateException("Inferred-biome streams were not prepared for region '"
+ regionKey + "'.");
}
ProceduralStream<IrisBiome> stream = regionStreams.get(inferredType);
if (stream == null) {
throw new IllegalStateException("Inferred-biome stream was not prepared for type " + inferredType + ".");
}
return stream;
}
private KList<IrisBiome> loadInferredBiomes(KList<String> keys, InferredType type) {
@@ -863,6 +635,37 @@ public class IrisComplex implements DataProvider {
generators.computeIfAbsent(cachedGenerator.getInterpolator(), (k) -> new HashSet<>()).add(cachedGenerator);
}
static GeneratorGroup[] freezeGeneratorGroups(Map<IrisInterpolator, Set<IrisGenerator>> generators) {
GeneratorGroup[] groups = new GeneratorGroup[generators.size()];
int groupIndex = 0;
for (Map.Entry<IrisInterpolator, Set<IrisGenerator>> entry : generators.entrySet()) {
IrisGenerator[] groupGenerators = entry.getValue().toArray(new IrisGenerator[0]);
Arrays.sort(groupGenerators, GENERATOR_ORDER);
groups[groupIndex] = new GeneratorGroup(entry.getKey(), groupGenerators);
groupIndex++;
}
Arrays.sort(groups, Comparator.comparing(GeneratorGroup::interpolator, INTERPOLATOR_ORDER));
return groups;
}
static double averageGeneratorHeights(
IrisGenerator[] generators,
double low,
double high,
double x,
double z,
long seed
) {
if (generators.length == 0) {
return 0D;
}
double height = 0D;
for (IrisGenerator generator : generators) {
height += M.lerp(low, high, generator.getHeight(x, z, seed));
}
return height / generators.length;
}
private Map<IrisInterpolator, IdentityHashMap<IrisBiome, GeneratorBounds>> buildGeneratorBounds(Engine engine) {
Map<IrisInterpolator, IdentityHashMap<IrisBiome, GeneratorBounds>> bounds = new HashMap<>();
KList<IrisBiome> allBiomes = new KList<>(generatorBiomes);
@@ -1265,9 +1068,10 @@ public class IrisComplex implements DataProvider {
}
}
public void close() {
if (riverRuntime != null) {
riverRuntime.close();
}
record GeneratorGroup(IrisInterpolator interpolator, IrisGenerator[] generators) {
}
public void close() {
}
}
@@ -30,9 +30,7 @@ import art.arcane.iris.engine.mantle.MantlePass;
import art.arcane.iris.engine.mantle.components.MantleCarvingComponent;
import art.arcane.iris.engine.mantle.components.MantleFloatingObjectComponent;
import art.arcane.iris.engine.mantle.components.MantleObjectComponent;
import art.arcane.iris.engine.mantle.components.MantleRiverHydrologyComponent;
import art.arcane.iris.engine.mantle.components.IrisStructureComponent;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.project.matter.IrisMatterContext;
@@ -90,7 +88,6 @@ public class IrisEngineMantle implements EngineMantle {
this.mantle = createMantle(engine);
components = new KMap<>();
registerComponent(new MantleCarvingComponent(this));
registerComponent(new MantleRiverHydrologyComponent(this));
object = new MantleObjectComponent(this);
registerComponent(object);
registerComponent(new MantleFloatingObjectComponent(this));
@@ -179,22 +176,10 @@ public class IrisEngineMantle implements EngineMantle {
if (!getDimension().isCarvingEnabled()) {
disabled.addIfMissing(ReservedFlag.CARVED);
}
if (disabled.contains(ReservedFlag.CARVED)
|| !isRiverHydrologyEnabled(getDimension())) {
disabled.addIfMissing(ReservedFlag.RIVER_HYDROLOGY);
}
return Set.copyOf(disabled);
});
}
static boolean isRiverHydrologyEnabled(IrisDimension dimension) {
return MantleRiverHydrologyComponent.isEnabledFor(dimension);
}
static boolean isRiverCaveHydrologyEnabled(IrisDimension dimension) {
return MantleRiverHydrologyComponent.isCaveConnectionsEnabledFor(dimension);
}
@Override
public MantleObjectComponent getObjectComponent() {
return object;
@@ -14,7 +14,6 @@ import art.arcane.iris.engine.object.IrisShapedGeneratorStyle;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.util.common.data.DataProvider;
import art.arcane.volmlib.util.math.M;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.iris.util.project.interpolation.NoiseBounds;
import art.arcane.iris.spi.PlatformBlockState;
@@ -67,8 +66,8 @@ public class UpperDimensionContext implements DataProvider {
engine.getDimension(),
engine.getData(),
chunkHeight,
complex.getNaturalHeightStream(),
complex.getNaturalTrueBiomeStream(),
complex.getHeightStream(),
complex.getTrueBiomeStream(),
complex.getRegionStream(),
complex.getRockStream(),
complex.getImageMapRuntime(),
@@ -100,25 +99,26 @@ public class UpperDimensionContext implements DataProvider {
upperDim.getRegions().forEach(regionKey -> {
IrisRegion region = upperData.getRegionLoader().load(regionKey);
if (region != null) {
region.getNaturalBiomes(dataProvider).forEach(biome -> registerBiomeGenerators(
region.getAllBiomes(dataProvider).forEach(biome -> registerBiomeGenerators(
biome, dataProvider, allBiomes, generators));
}
});
for (IrisRegion mappedRegion : imageMapRuntime.getMappedRegions()) {
mappedRegion.getNaturalBiomes(dataProvider).forEach(biome -> registerBiomeGenerators(
mappedRegion.getAllBiomes(dataProvider).forEach(biome -> registerBiomeGenerators(
biome, dataProvider, allBiomes, generators));
}
for (IrisBiome mappedBiome : imageMapRuntime.getMappedBiomes()) {
registerBiomeGenerators(mappedBiome, dataProvider, allBiomes, generators);
}
IrisComplex.GeneratorGroup[] generatorGroups = IrisComplex.freezeGeneratorGroups(generators);
Map<IrisInterpolator, IdentityHashMap<IrisBiome, NoiseBounds>> generatorBounds = new HashMap<>();
for (Map.Entry<IrisInterpolator, Set<IrisGenerator>> entry : generators.entrySet()) {
for (IrisComplex.GeneratorGroup group : generatorGroups) {
IdentityHashMap<IrisBiome, NoiseBounds> interpolatorBounds = new IdentityHashMap<>(Math.max(allBiomes.size(), 16));
for (IrisBiome biome : allBiomes) {
double min = 0D;
double max = 0D;
for (IrisGenerator gen : entry.getValue()) {
for (IrisGenerator gen : group.generators()) {
String key = gen.getLoadKey();
if (key == null || key.isBlank()) {
continue;
@@ -128,7 +128,7 @@ public class UpperDimensionContext implements DataProvider {
}
interpolatorBounds.put(biome, new NoiseBounds(min, max));
}
generatorBounds.put(entry.getKey(), interpolatorBounds);
generatorBounds.put(group.interpolator(), interpolatorBounds);
}
ProceduralStream<Double> regionStyleStream = upperDim.getRegionStyle()
@@ -204,10 +204,10 @@ public class UpperDimensionContext implements DataProvider {
return mappedTerrainHeight(imageMapRuntime, fluidHeight, x, z);
}
double interpolatedHeight = 0;
for (Map.Entry<IrisInterpolator, Set<IrisGenerator>> entry : generators.entrySet()) {
IrisInterpolator interpolator = entry.getKey();
Set<IrisGenerator> gens = entry.getValue();
if (gens.isEmpty()) {
for (IrisComplex.GeneratorGroup group : generatorGroups) {
IrisInterpolator interpolator = group.interpolator();
IrisGenerator[] groupGenerators = group.generators();
if (groupGenerators.length == 0) {
continue;
}
IdentityHashMap<IrisBiome, NoiseBounds> cachedBounds = generatorBounds.get(interpolator);
@@ -223,7 +223,7 @@ public class UpperDimensionContext implements DataProvider {
}
double bMin = 0D;
double bMax = 0D;
for (IrisGenerator gen : gens) {
for (IrisGenerator gen : groupGenerators) {
String key = gen.getLoadKey();
if (key == null || key.isBlank()) {
continue;
@@ -239,11 +239,14 @@ public class UpperDimensionContext implements DataProvider {
});
double hi = sampledBounds.max();
double lo = sampledBounds.min();
double d = 0;
for (IrisGenerator gen : gens) {
d += M.lerp(lo, hi, gen.getHeight(x, z, heightSeed + 239945));
}
interpolatedHeight += d / gens.size();
interpolatedHeight += IrisComplex.averageGeneratorHeights(
groupGenerators,
lo,
hi,
x,
z,
heightSeed + 239945
);
}
double proceduralHeight = Math.max(
Math.min(interpolatedHeight + fluidHeight + overlayStream.get(x, z), chunkHeight),
@@ -27,8 +27,6 @@ import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineAssignedActuator;
import art.arcane.iris.engine.framework.EngineDecorator;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.river.RiverRouteState;
import art.arcane.iris.engine.river.runtime.IrisRiverSurfaceSample;
import art.arcane.iris.util.common.data.B;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.volmlib.util.documentation.BlockCoordinates;
@@ -66,12 +64,6 @@ public class IrisDecorantActuator extends EngineAssignedActuator<PlatformBlockSt
seaFloorDecorator = new IrisSeaFloorDecorator(getEngine());
}
static boolean shouldDecorateShoreline(IrisRiverSurfaceSample sample, int height) {
return !sample.subterranean()
&& height == Math.round(sample.waterSurfaceY())
&& (!sample.river().present() || sample.river().state() != RiverRouteState.DRY);
}
@BlockCoordinates
@Override
public void onActuate(int x, int z, Hunk<PlatformBlockState> output, boolean multicore, ChunkContext context) {
@@ -94,25 +86,23 @@ public class IrisDecorantActuator extends EngineAssignedActuator<PlatformBlockSt
height = context.getRoundedHeight(i, j);
biome = context.getBiome().get(i, j);
cave = shouldRay ? context.getCave().get(i, j) : null;
IrisRiverSurfaceSample riverSurface = getComplex().getRiverSurfaceStream().get(realX, realZ);
int surfaceFluidHeight = (int) Math.round(riverSurface.waterSurfaceY());
if (biome.getDecorators().isEmpty() && (cave == null || cave.getDecorators().isEmpty())) {
continue;
}
if (height < surfaceFluidHeight && PREDICATE_SOLID.test(output.get(i, height, j))
&& height + 1 < output.getHeight() && B.isFluid(output.get(i, height + 1, j))) {
if (height < getDimension().getFluidHeight() && PREDICATE_SOLID.test(output.get(i, height, j))
&& height + 1 < output.getHeight() && B.isWater(output.get(i, height + 1, j))) {
getSeaSurfaceDecorator().decorate(i, j,
realX, Math.round(i + 1), Math.round(x + i - 1),
realZ, Math.round(z + j + 1), Math.round(z + j - 1),
output, biome, surfaceFluidHeight, getEngine().getHeight());
output, biome, getDimension().getFluidHeight(), getEngine().getHeight());
getSeaFloorDecorator().decorate(i, j,
realX, realZ, output, biome, height + 1,
surfaceFluidHeight + 1);
getDimension().getFluidHeight() + 1);
}
if (shouldDecorateShoreline(riverSurface, height)) {
if (height == getDimension().getFluidHeight()) {
getShoreLineDecorator().decorate(i, j,
realX, Math.round(x + i + 1), Math.round(x + i - 1),
realZ, Math.round(z + j + 1), Math.round(z + j - 1),
@@ -88,10 +88,13 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
IrisData data = getData();
IrisComplex complex = getComplex();
RNG localRng = rng;
int fluidHeight = dimension.getFluidHeight();
int clampedFluidHeight = Math.min(chunkHeight, fluidHeight);
boolean bedrockEnabled = dimension.isBedrock();
boolean hideOres = dimension.isHideOresForHiddenOre();
ChunkedDataCache<IrisBiome> biomeCache = context.getBiome();
ChunkedDataCache<IrisRegion> regionCache = context.getRegion();
ChunkedDataCache<PlatformBlockState> fluidCache = context.getFluid();
ChunkedDataCache<PlatformBlockState> rockCache = context.getRock();
int realX = xf + x;
UpperDimensionContext upperContext = getEngine().getUpperContext();
@@ -107,17 +110,13 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
IrisBiome biome = biomeCache.get(xf, zf);
IrisRegion region = regionCache.get(xf, zf);
int he = Math.min(chunkHeight, context.getRoundedHeight(xf, zf));
int surfaceFluidHeight = Math.min(
chunkHeight,
(int) Math.round(complex.getRiverWaterSurfaceStream().get(realX, realZ))
);
int hf = Math.max(surfaceFluidHeight, he);
int hf = Math.max(clampedFluidHeight, he);
if (hf < 0) {
continue;
}
int topY = Math.min(hf, chunkHeight - 1);
PlatformBlockState fluid = complex.resolveSurfaceFluid(realX, realZ);
PlatformBlockState fluid = fluidCache.get(xf, zf);
PlatformBlockState rock = rockCache.get(xf, zf);
PlatformBlockState mappedSurfaceBlock = complex.getImageMapRuntime().sampleSurfaceBlock(realX, realZ);
KList<IrisOreGenerator> biomeSurfaceOres = hideOres ? null : biome.getSurfaceOreGenerators();
@@ -40,17 +40,16 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator {
@Override
public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1,
Hunk<PlatformBlockState> data, IrisBiome biome, int height, int max) {
double localFluidHeight = getComplex().getRiverWaterSurfaceStream().get(realX, realZ);
if (height != Math.round(localFluidHeight)) {
if (height != getDimension().getFluidHeight()) {
return;
}
double complexFluidHeight = getComplex().getFluidHeight();
ProceduralStream<Double> heightStream = getComplex().getHeightStream();
ProceduralStream<Double> fluidStream = getComplex().getRiverWaterSurfaceStream();
if (Math.round(heightStream.get(realX1, realZ)) >= Math.round(fluidStream.get(realX1, realZ))
&& Math.round(heightStream.get(realX_1, realZ)) >= Math.round(fluidStream.get(realX_1, realZ))
&& Math.round(heightStream.get(realX, realZ1)) >= Math.round(fluidStream.get(realX, realZ1))
&& Math.round(heightStream.get(realX, realZ_1)) >= Math.round(fluidStream.get(realX, realZ_1))) {
if (Math.round(heightStream.get(realX1, realZ)) >= complexFluidHeight
&& Math.round(heightStream.get(realX_1, realZ)) >= complexFluidHeight
&& Math.round(heightStream.get(realX, realZ1)) >= complexFluidHeight
&& Math.round(heightStream.get(realX, realZ_1)) >= complexFluidHeight) {
return;
}
@@ -58,7 +58,7 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator {
@BlockCoordinates
public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1,
Hunk<PlatformBlockState> data, IrisBiome biome, InferredType inferredType, int height, int max) {
int fluidHeight = (int) Math.round(getComplex().getRiverWaterSurfaceStream().get(realX, realZ));
int fluidHeight = getDimension().getFluidHeight();
if (inferredType == InferredType.SHORE && height < fluidHeight) {
return;
}
@@ -42,8 +42,6 @@ import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisStructure;
import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
import art.arcane.iris.engine.river.cave.RiverCaveHydrologyStorage;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState;
@@ -246,14 +244,6 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
@BlockCoordinates
default IrisBiome getCaveOrMantleBiome(int x, int y, int z) {
RiverCaveHydrology hydrology = RiverCaveHydrologyStorage.getIfPresent(
getMantle().getMantle(), x, y, z);
if (hydrology != null && !hydrology.floodedBiomeKey().isEmpty()) {
IrisBiome biome = getData().getBiomeLoader().load(hydrology.floodedBiomeKey());
if (biome != null) {
return biome;
}
}
MatterCavern m = getMantle().getMantle().get(x, y, z, MatterCavern.class);
if (m != null && m.getCustomBiome() != null && !m.getCustomBiome().isEmpty()) {
@@ -157,10 +157,7 @@ public final class NativeStructurePlacementPlanner {
}
static boolean isSubmerged(Engine engine, int blockX, int blockZ) {
int localFluidHeight = engine.getComplex() == null
? engine.getDimension().getFluidHeight()
: (int) Math.round(engine.getComplex().getRiverWaterSurfaceStream().get(blockX, blockZ));
return engine.getHeight(blockX, blockZ, true) < localFluidHeight;
return engine.getHeight(blockX, blockZ, true) < engine.getDimension().getFluidHeight();
}
private static int comparePlacementPriority(IrisStructurePlacement left, IrisStructurePlacement right) {
@@ -3,8 +3,6 @@ package art.arcane.iris.engine.framework;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisStructureAnchorMode;
import art.arcane.iris.engine.object.IrisStructurePlacement;
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
import art.arcane.iris.engine.river.cave.RiverCaveHydrologyStorage;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.matter.MatterCavern;
@@ -288,12 +286,10 @@ public final class StructureCaveAnchorResolver {
int mantleY,
int blockZ
) {
RiverCaveHydrology hydrology = hydrologyAt(engine, blockX, mantleY, blockZ);
MatterCavern cavern = cavernAt(engine, blockX, mantleY, blockZ);
return acceptsAnchorFluid(
placement.isUnderwater(),
cavern,
hydrology,
mantleY,
engine.getDimension().getCaveLavaHeight());
}
@@ -304,19 +300,6 @@ public final class StructureCaveAnchorResolver {
int mantleY,
int defaultLavaHeight
) {
return acceptsAnchorFluid(underwater, cavern, null, mantleY, defaultLavaHeight);
}
static boolean acceptsAnchorFluid(
boolean underwater,
MatterCavern cavern,
RiverCaveHydrology hydrology,
int mantleY,
int defaultLavaHeight
) {
if (hydrology != null && hydrology.protectsPlacement()) {
return false;
}
if (cavern == null || !cavern.isCavern()) {
return false;
}
@@ -330,15 +313,7 @@ public final class StructureCaveAnchorResolver {
}
private static MatterCavern cavernAt(Engine engine, int blockX, int mantleY, int blockZ) {
MatterCavern baseline = engine.getMantle().getMantle()
.get(blockX, mantleY, blockZ, MatterCavern.class);
RiverCaveHydrology hydrology = hydrologyAt(engine, blockX, mantleY, blockZ);
return hydrology == null ? baseline : hydrology.asCavern();
}
private static RiverCaveHydrology hydrologyAt(Engine engine, int blockX, int mantleY, int blockZ) {
return RiverCaveHydrologyStorage.getIfPresent(
engine.getMantle().getMantle(), blockX, mantleY, blockZ);
return engine.getMantle().getMantle().get(blockX, mantleY, blockZ, MatterCavern.class);
}
static int toMantleY(int worldY, int worldMinHeight) {
@@ -21,6 +21,7 @@ import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.util.common.data.IrisCustomData;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.matter.MatterCavern;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import org.bukkit.Bukkit;
@@ -111,7 +112,7 @@ public class WorldObjectPlacer implements IObjectPlacer {
@Override
public boolean isCarved(int x, int y, int z) {
return mantle.isCarved(x, y, z);
return mantle.getMantle().get(x, y, z, MatterCavern.class) != null;
}
@Override
@@ -24,9 +24,6 @@ import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeGeneratorLink;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisRegion;
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 java.awt.Color;
@@ -43,12 +40,6 @@ public final class IrisRenderer {
private static final int BLUE = new Color(45, 91, 156).getRGB();
private static final int YELLOW = new Color(211, 164, 67).getRGB();
private static final int GREEN = new Color(78, 137, 83).getRGB();
private static final int RIVER_CHANNEL = new Color(48, 112, 190).getRGB();
private static final int RIVER_MOUTH = new Color(54, 164, 205).getRGB();
private static final int RIVER_BANK = new Color(92, 146, 78).getRGB();
private static final int DRY_CHANNEL = new Color(171, 128, 68).getRGB();
private static final int DRY_BANK = new Color(132, 105, 62).getRGB();
private static final int NO_RIVER = new Color(28, 31, 38).getRGB();
private static final int DEEP_WATER = new Color(20, 48, 92).getRGB();
private static final int SHALLOW_WATER = new Color(50, 112, 154).getRGB();
private static final int LOWLAND = new Color(78, 128, 76).getRGB();
@@ -114,17 +105,9 @@ public final class IrisRenderer {
renderHeightAtlas(pixels, resolution, sx, sz, step, renderer, cancelled);
return image;
}
PixelShader shader = shader(currentType, step, studio);
if (studio && currentType == RenderType.RIVER) {
Arrays.fill(pixels, NO_RIVER);
renderRiverAtlas(pixels, resolution, sx, sz, step, renderer.getComplex(), cancelled, false);
return image;
}
PixelShader shader = shader(currentType, studio);
if (studio && adaptiveStudioType(currentType)) {
renderAdaptiveAtlas(pixels, resolution, sx, sz, step, shader, cancelled);
if (currentType == RenderType.BIOME) {
renderRiverAtlas(pixels, resolution, sx, sz, step, renderer.getComplex(), cancelled, true);
}
return image;
}
int groupSize = sampleGroup(step, resolution);
@@ -150,18 +133,6 @@ public final class IrisRenderer {
return image;
}
public static int riverColor(RiverSection section) {
Objects.requireNonNull(section, "section");
return switch (section) {
case CHANNEL -> RIVER_CHANNEL;
case MOUTH -> RIVER_MOUTH;
case BANK -> RIVER_BANK;
case DRY_CHANNEL -> DRY_CHANNEL;
case DRY_BANK -> DRY_BANK;
case NONE -> NO_RIVER;
};
}
public static int heightColor(double height, double maximumHeight, double fluidHeight) {
double boundedMaximum = Math.max(1D, maximumHeight);
double boundedHeight = clamp(height, 0D, boundedMaximum);
@@ -189,7 +160,7 @@ public final class IrisRenderer {
return Math.max(1, Math.min(resolution, (int) Math.floor(16D / absoluteStep)));
}
private PixelShader shader(RenderType currentType, double step, boolean studio) {
private PixelShader shader(RenderType currentType, boolean studio) {
IrisComplex complex = renderer.getComplex();
return switch (currentType) {
case BIOME, DECORATOR_LOAD, OBJECT_LOAD, LAYER_LOAD -> biomeShader(
@@ -198,8 +169,7 @@ public final class IrisRenderer {
case BIOME_SEA -> biomeShader(complex.getSeaBiomeStream(), currentType);
case REGION -> regionShader(complex, currentType);
case CAVE_LAND -> biomeShader(complex.getCaveBiomeStream(), currentType);
case HEIGHT -> heightShader(studio ? complex.getNaturalHeightStream() : complex.getHeightStream());
case RIVER -> (double x, double z) -> riverColor(complex, x, z, step);
case HEIGHT -> heightShader(complex.getHeightStream());
case CONTINENT -> studio
? continentShader(complex.getBaseBiomeStream())
: this::continentColor;
@@ -210,7 +180,7 @@ public final class IrisRenderer {
return switch (type) {
case BIOME, DECORATOR_LOAD, OBJECT_LOAD, LAYER_LOAD, BIOME_LAND, BIOME_SEA, REGION, CAVE_LAND,
CONTINENT -> true;
case HEIGHT, RIVER -> false;
case HEIGHT -> false;
};
}
@@ -254,7 +224,7 @@ public final class IrisRenderer {
startX,
startZ,
step,
complex.getNaturalHeightStream(),
complex.getHeightStream(),
engine.getHeight(),
fluidHeight,
cancelled
@@ -267,103 +237,6 @@ public final class IrisRenderer {
}
}
private static void renderRiverAtlas(
int[] pixels,
int resolution,
double startX,
double startZ,
double step,
IrisComplex complex,
BooleanSupplier cancelled,
boolean composite
) {
IrisRiverRuntime runtime = complex.getRiverRuntime();
if (runtime == null) {
return;
}
int maximumPixels = Math.max(1, Math.min(16, (int) Math.floor(64D / step)));
int blockPixels = Integer.highestOneBit(maximumPixels);
for (int pixelZ = 0; pixelZ < resolution; pixelZ += blockPixels) {
int height = Math.min(blockPixels, resolution - pixelZ);
for (int pixelX = 0; pixelX < resolution; pixelX += blockPixels) {
renderRiverBlock(
pixels,
resolution,
startX,
startZ,
step,
runtime,
cancelled,
composite,
pixelX,
pixelZ,
Math.min(blockPixels, resolution - pixelX),
height
);
}
}
}
private static void renderRiverBlock(
int[] pixels,
int resolution,
double startX,
double startZ,
double step,
IrisRiverRuntime runtime,
BooleanSupplier cancelled,
boolean composite,
int pixelX,
int pixelZ,
int width,
int height
) {
checkCancelled(cancelled);
RiverSample sample = runtime.sampleFootprint(
startX + pixelX * step,
startZ + pixelZ * step,
startX + (pixelX + width) * step,
startZ + (pixelZ + height) * step
);
if (!sample.present()) {
return;
}
if (width == 1 && height == 1) {
int index = pixelZ * resolution + pixelX;
int color = riverColor(sample.section());
pixels[index] = composite ? riverCompositeColor(pixels[index], sample.section(), color) : color;
return;
}
int leftWidth = Math.max(1, width / 2);
int rightWidth = width - leftWidth;
int topHeight = Math.max(1, height / 2);
int bottomHeight = height - topHeight;
renderRiverBlock(pixels, resolution, startX, startZ, step, runtime, cancelled, composite,
pixelX, pixelZ, leftWidth, topHeight);
if (rightWidth > 0) {
renderRiverBlock(pixels, resolution, startX, startZ, step, runtime, cancelled, composite,
pixelX + leftWidth, pixelZ, rightWidth, topHeight);
}
if (bottomHeight > 0) {
renderRiverBlock(pixels, resolution, startX, startZ, step, runtime, cancelled, composite,
pixelX, pixelZ + topHeight, leftWidth, bottomHeight);
if (rightWidth > 0) {
renderRiverBlock(pixels, resolution, startX, startZ, step, runtime, cancelled, composite,
pixelX + leftWidth, pixelZ + topHeight, rightWidth, bottomHeight);
}
}
}
private static int riverCompositeColor(int base, RiverSection section, int river) {
return switch (section) {
case CHANNEL, MOUTH -> river;
case BANK -> blend(base, river, 0.68D);
case DRY_CHANNEL -> blend(base, river, 0.88D);
case DRY_BANK -> blend(base, river, 0.58D);
case NONE -> base;
};
}
private PixelShader biomeShader(ProceduralStream<IrisBiome> stream, RenderType currentType) {
IdentityHashMap<IrisBiome, Integer> colors = new IdentityHashMap<>();
return (double x, double z) -> {
@@ -398,22 +271,6 @@ public final class IrisRenderer {
return (double x, double z) -> heightColor(stream.getDouble(x, z), maximumHeight, fluidHeight);
}
private int riverColor(IrisComplex complex, double x, double z, double step) {
IrisRiverRuntime runtime = complex.getRiverRuntime();
if (runtime == null) {
return riverColor(RiverSection.NONE);
}
double endX = x + step;
double endZ = z + step;
RiverSample sample = runtime.sampleFootprint(
StrictMath.min(x, endX),
StrictMath.min(z, endZ),
StrictMath.max(x, endX),
StrictMath.max(z, endZ)
);
return riverColor(sample.section());
}
private int continentColor(double x, double z) {
IrisBiome biome = renderer.getBiome(
(int) Math.round(x),
@@ -19,5 +19,5 @@
package art.arcane.iris.engine.framework.render;
public enum RenderType {
BIOME, BIOME_LAND, BIOME_SEA, REGION, CAVE_LAND, RIVER, HEIGHT, OBJECT_LOAD, DECORATOR_LOAD, CONTINENT, LAYER_LOAD
BIOME, BIOME_LAND, BIOME_SEA, REGION, CAVE_LAND, HEIGHT, OBJECT_LOAD, DECORATOR_LOAD, CONTINENT, LAYER_LOAD
}
@@ -27,8 +27,6 @@ import art.arcane.iris.engine.framework.EngineTarget;
import art.arcane.iris.engine.framework.TreeBlockMaterial;
import art.arcane.iris.engine.mantle.components.MantleObjectComponent;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
import art.arcane.iris.engine.river.cave.RiverCaveHydrologyStorage;
import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.util.common.data.B;
@@ -104,7 +102,7 @@ public interface EngineMantle extends MatterGenerator {
}
default int getHighest(int x, int z, IrisData data, boolean ignoreFluid) {
return ignoreFluid ? trueHeight(x, z) : Math.max(trueHeight(x, z), getFluidHeight(x, z));
return ignoreFluid ? trueHeight(x, z) : Math.max(trueHeight(x, z), getEngine().getDimension().getFluidHeight());
}
default int trueHeight(int x, int z) {
@@ -112,10 +110,6 @@ public interface EngineMantle extends MatterGenerator {
}
default boolean isCarved(int x, int h, int z) {
RiverCaveHydrology hydrology = RiverCaveHydrologyStorage.getIfPresent(getMantle(), x, h, z);
if (hydrology != null) {
return hydrology.carves();
}
return getMantle().get(x, h, z, MatterCavern.class) != null;
}
@@ -131,17 +125,13 @@ public interface EngineMantle extends MatterGenerator {
}
default boolean isUnderwater(int x, int z) {
return getHighest(x, z, true) < getFluidHeight(x, z);
return getHighest(x, z, true) <= getFluidHeight();
}
default int getFluidHeight() {
return getEngine().getDimension().getFluidHeight();
}
default int getFluidHeight(int x, int z) {
return (int) Math.round(getComplex().getRiverWaterSurfaceStream().get(x, z));
}
default boolean isDebugSmartBore() {
return getEngine().getDimension().isDebugSmartBore();
}
@@ -31,7 +31,6 @@ import art.arcane.iris.engine.object.IObjectPlacer;
import art.arcane.iris.engine.object.IrisGeneratorStyle;
import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.iris.engine.object.TileData;
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
import art.arcane.volmlib.util.collection.KSet;
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
import art.arcane.volmlib.util.function.Function3;
@@ -207,10 +206,6 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
if (chunk == null) return;
Matter matter = chunk.getOrCreate(y >> 4);
if ((t instanceof PlatformBlockState || t instanceof MatterCavern)
&& hasProtectedHydrology(matter, x, y, z)) {
return;
}
if (t instanceof PlatformBlockState) {
clearDeferredPlacement(matter, x, y, z);
}
@@ -236,9 +231,6 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
}
Matter matter = chunk.getOrCreate(y >> 4);
if (hasProtectedHydrology(matter, x, y, z)) {
return false;
}
MatterCavern existing = matter.<MatterCavern>slice(MatterCavern.class).get(x & 15, y & 15, z & 15);
if (existing != null) {
return false;
@@ -259,9 +251,6 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
}
Matter matter = chunk.getOrCreate(y >> 4);
if (hasProtectedHydrology(matter, x, y, z)) {
return false;
}
if (matter.hasSlice(PlatformBlockState.class)) {
matter.<PlatformBlockState>getSlice(PlatformBlockState.class).set(x & 15, y & 15, z & 15, null);
}
@@ -284,9 +273,6 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
+ x + "," + y + "," + z);
}
Matter matter = chunk.getOrCreate(y >> 4);
if (hasProtectedHydrology(matter, x, y, z)) {
return;
}
if (matter.hasSlice(PlatformBlockState.class)) {
matter.<PlatformBlockState>getSlice(PlatformBlockState.class).set(x & 15, y & 15, z & 15, null);
}
@@ -310,9 +296,6 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
if (matter == null) {
return;
}
if (hasProtectedHydrology(matter, x, y, z)) {
return;
}
if (matter.hasSlice(PlatformBlockState.class)) {
matter.<PlatformBlockState>getSlice(PlatformBlockState.class).set(x & 15, y & 15, z & 15, null);
}
@@ -370,22 +353,9 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
if (matter == null || !matter.hasSlice(type)) {
return;
}
if ((type == PlatformBlockState.class || type == MatterCavern.class)
&& hasProtectedHydrology(matter, x, y, z)) {
return;
}
matter.getSlice(type).set(x & 15, y & 15, z & 15, null);
}
private static boolean hasProtectedHydrology(Matter matter, int x, int y, int z) {
if (!matter.hasSlice(RiverCaveHydrology.class)) {
return false;
}
RiverCaveHydrology hydrology = matter.<RiverCaveHydrology>getSlice(RiverCaveHydrology.class)
.get(x & 15, y & 15, z & 15);
return hydrology != null && hydrology.protectsPlacement();
}
private static void clearDeferredPlacement(Matter matter, int x, int y, int z) {
if (matter.hasSlice(Identifier.class)) {
matter.<Identifier>getSlice(Identifier.class).set(x & 15, y & 15, z & 15, null);
@@ -480,10 +450,6 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
@Override
public boolean isCarved(int x, int y, int z) {
RiverCaveHydrology hydrology = getDataIfPresent(x, y, z, RiverCaveHydrology.class);
if (hydrology != null) {
return hydrology.carves();
}
return getDataIfPresent(x, y, z, MatterCavern.class) != null;
}
@@ -508,28 +474,14 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
}
Matter matter = chunk.get(section);
if (matter == null) {
continue;
}
MatterSlice<MatterCavern> cavernSlice = matter.hasSlice(MatterCavern.class)
? matter.getSlice(MatterCavern.class)
: null;
MatterSlice<RiverCaveHydrology> hydrologySlice = matter.hasSlice(RiverCaveHydrology.class)
? matter.getSlice(RiverCaveHydrology.class)
: null;
if (cavernSlice == null && hydrologySlice == null) {
if (matter == null || !matter.hasSlice(MatterCavern.class)) {
continue;
}
MatterSlice<MatterCavern> slice = matter.getSlice(MatterCavern.class);
int sectionBaseY = section << 4;
int sectionMaxY = Math.min(cappedHeight, sectionBaseY + 16);
for (int y = sectionBaseY; y < sectionMaxY; y++) {
RiverCaveHydrology hydrology = hydrologySlice == null
? null
: hydrologySlice.get(localX, y & 15, localZ);
if (hydrology != null) {
carvedColumn[y] = hydrology.carves() ? (byte) 1 : 0;
} else if (cavernSlice != null && cavernSlice.get(localX, y & 15, localZ) != null) {
if (slice.get(localX, y & 15, localZ) != null) {
carvedColumn[y] = 1;
}
}
@@ -61,14 +61,15 @@ public final class CarveOrphanSweep {
int[] surfaceHeights,
int maxSurfaceBreakDepth,
int worldCeilingY,
long[] surfaceFluidBoundaries
int[] surfaceFluidBoundaryStartY,
int fluidHeight
) {
if (chunk == null) {
return 0;
}
return sweep(surfaceHeights, maxSurfaceBreakDepth, 0, worldCeilingY,
new MantleCarveAccess(chunk, surfaceFluidBoundaries));
new MantleCarveAccess(chunk, surfaceFluidBoundaryStartY, fluidHeight));
}
public static int sweep(int[] surfaceHeights, int maxSurfaceBreakDepth, int worldFloorY, int worldCeilingY, CarveAccess access) {
@@ -228,13 +229,15 @@ public final class CarveOrphanSweep {
private static final class MantleCarveAccess implements CarveAccess {
private final MantleChunk<Matter> chunk;
private final long[] surfaceFluidBoundaries;
private final int[] surfaceFluidBoundaryStartY;
private final int fluidHeight;
private MatterSlice<MatterCavern> cachedSlice;
private int cachedSectionIndex = -1;
private MantleCarveAccess(MantleChunk<Matter> chunk, long[] surfaceFluidBoundaries) {
private MantleCarveAccess(MantleChunk<Matter> chunk, int[] surfaceFluidBoundaryStartY, int fluidHeight) {
this.chunk = chunk;
this.surfaceFluidBoundaries = surfaceFluidBoundaries;
this.surfaceFluidBoundaryStartY = surfaceFluidBoundaryStartY;
this.fluidHeight = fluidHeight;
}
@Override
@@ -258,7 +261,7 @@ public final class CarveOrphanSweep {
@Override
public boolean isProtected(int localX, int y, int localZ) {
int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ);
return SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaries, columnIndex, y);
return SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaryStartY, columnIndex, y, fluidHeight);
}
@Override
@@ -23,7 +23,6 @@ import art.arcane.iris.engine.data.cache.Cache;
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.RiverCaveHydrology;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.B;
import art.arcane.volmlib.util.collection.KList;
@@ -72,12 +71,6 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
discard();
return CommitResult.REJECTED_BOUNDS;
}
RiverCaveHydrology hydrology = delegate.getData(
mutation.x(), mutation.y(), mutation.z(), RiverCaveHydrology.class);
if (hydrology != null && hydrology.protectsPlacement()) {
discard();
return CommitResult.REJECTED_HYDROLOGY;
}
}
for (BufferedMutation mutation : mutations) {
@@ -252,8 +245,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
enum CommitResult {
COMMITTED,
EMPTY,
REJECTED_BOUNDS,
REJECTED_HYDROLOGY
REJECTED_BOUNDS
}
private interface BufferedMutation {
@@ -1,69 +0,0 @@
package art.arcane.iris.engine.mantle.components;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.object.IrisGeneratorStyle;
import art.arcane.iris.engine.object.NoiseStyle;
import art.arcane.iris.engine.river.cave.RiverCaveGrottoShape;
import art.arcane.iris.engine.river.cave.RiverCavePlannerSettings;
import art.arcane.iris.engine.river.cave.RiverCaveSource;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.volmlib.util.math.RNG;
final class ConfiguredRiverGrottoShape implements RiverCaveGrottoShape {
private static final long SHAPE_SALT = 0x3C6EF372FE94F82BL;
private static final long WARP_X_SALT = 0xA54FF53A5F1D36F1L;
private static final long WARP_Y_SALT = 0x510E527FADE682D1L;
private static final long WARP_Z_SALT = 0x9B05688C2B3E6C1FL;
private final CNG shape;
private final CNG warpX;
private final CNG warpY;
private final CNG warpZ;
private final double warpStrength;
private final double boundaryVariation;
ConfiguredRiverGrottoShape(
long seed,
IrisData data,
IrisGeneratorStyle shapeStyle,
IrisGeneratorStyle warpStyle,
double warpStrength,
double boundaryVariation
) {
IrisGeneratorStyle resolvedShape = shapeStyle == null
? new IrisGeneratorStyle(NoiseStyle.FLAT)
: shapeStyle;
IrisGeneratorStyle resolvedWarp = warpStyle == null
? new IrisGeneratorStyle(NoiseStyle.FLAT)
: warpStyle;
shape = resolvedShape.createNoCache(new RNG(seed ^ SHAPE_SALT), data);
warpX = resolvedWarp.createNoCache(new RNG(seed ^ WARP_X_SALT), data);
warpY = resolvedWarp.createNoCache(new RNG(seed ^ WARP_Y_SALT), data);
warpZ = resolvedWarp.createNoCache(new RNG(seed ^ WARP_Z_SALT), data);
this.warpStrength = Math.max(0D, warpStrength);
this.boundaryVariation = Math.max(0D, Math.min(0.75D, boundaryVariation));
}
@Override
public boolean contains(
RiverCaveSource source,
RiverCavePlannerSettings settings,
int offsetX,
int offsetY,
int offsetZ
) {
double worldX = source.target().x() + offsetX;
double worldY = source.target().y() + offsetY;
double worldZ = source.target().z() + offsetZ;
double warpedX = offsetX + warpX.fitDouble(-warpStrength, warpStrength, worldX, worldY, worldZ);
double warpedY = offsetY + warpY.fitDouble(-warpStrength, warpStrength, worldY, worldZ, worldX);
double warpedZ = offsetZ + warpZ.fitDouble(-warpStrength, warpStrength, worldZ, worldX, worldY);
double horizontalRadius = settings.grottoHorizontalRadius();
double verticalRadius = settings.grottoVerticalRadius();
double normalized = (warpedX * warpedX / (horizontalRadius * horizontalRadius))
+ (warpedY * warpedY / (verticalRadius * verticalRadius))
+ (warpedZ * warpedZ / (horizontalRadius * horizontalRadius));
double boundary = shape.fitDouble(-boundaryVariation, boundaryVariation, worldX, worldY, worldZ);
return normalized <= 1D + boundary;
}
}
@@ -199,7 +199,7 @@ public class IrisCaveCarver3D {
double thresholdPenalty,
IrisRange worldYRange,
int[] precomputedSurfaceHeights,
long[] surfaceFluidBoundaries,
int[] surfaceFluidBoundaryStartY,
IrisRange overrideVerticalRange,
CaveFluidSupportPlan fluidSupportPlan
) {
@@ -324,7 +324,7 @@ public class IrisCaveCarver3D {
surfaceBreakThresholdBoost,
columnMaxY,
fluidMaxY,
surfaceFluidBoundaries,
surfaceFluidBoundaryStartY,
surfaceBreakFloorY,
surfaceBreakColumn,
columnThreshold,
@@ -347,7 +347,7 @@ public class IrisCaveCarver3D {
surfaceBreakThresholdBoost,
columnMaxY,
fluidMaxY,
surfaceFluidBoundaries,
surfaceFluidBoundaryStartY,
surfaceBreakFloorY,
surfaceBreakColumn,
columnThreshold,
@@ -373,7 +373,7 @@ public class IrisCaveCarver3D {
surfaceBreakThresholdBoost,
columnMaxY,
fluidMaxY,
surfaceFluidBoundaries,
surfaceFluidBoundaryStartY,
surfaceBreakFloorY,
surfaceBreakColumn,
columnThreshold,
@@ -397,7 +397,7 @@ public class IrisCaveCarver3D {
surfaceBreakThresholdBoost,
columnMaxY,
fluidMaxY,
surfaceFluidBoundaries,
surfaceFluidBoundaryStartY,
surfaceBreakFloorY,
surfaceBreakColumn,
columnThreshold,
@@ -428,7 +428,7 @@ public class IrisCaveCarver3D {
double surfaceBreakThresholdBoost,
int[] columnMaxY,
int[] fluidMaxY,
long[] surfaceFluidBoundaries,
int[] surfaceFluidBoundaryStartY,
int[] surfaceBreakFloorY,
boolean[] surfaceBreakColumn,
double[] columnThreshold,
@@ -484,7 +484,7 @@ public class IrisCaveCarver3D {
}
int columnIndex = activeColumnIndices[activeIndex];
if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaries, columnIndex, y)) {
if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaryStartY, columnIndex, y, fluidHeight)) {
continue;
}
planeColumnIndices[planeCount] = columnIndex;
@@ -560,7 +560,7 @@ public class IrisCaveCarver3D {
double surfaceBreakThresholdBoost,
int[] columnMaxY,
int[] fluidMaxY,
long[] surfaceFluidBoundaries,
int[] surfaceFluidBoundaryStartY,
int[] surfaceBreakFloorY,
boolean[] surfaceBreakColumn,
double[] columnThreshold,
@@ -622,7 +622,7 @@ public class IrisCaveCarver3D {
}
int columnIndex = activeColumnIndices[activeIndex];
if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaries, columnIndex, y)) {
if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaryStartY, columnIndex, y, fluidHeight)) {
continue;
}
planeColumnIndices[planeCount] = columnIndex;
@@ -720,7 +720,7 @@ public class IrisCaveCarver3D {
double surfaceBreakThresholdBoost,
int[] columnMaxY,
int[] fluidMaxY,
long[] surfaceFluidBoundaries,
int[] surfaceFluidBoundaryStartY,
int[] surfaceBreakFloorY,
boolean[] surfaceBreakColumn,
double[] columnThreshold,
@@ -822,7 +822,7 @@ public class IrisCaveCarver3D {
}
int index = tileIndices[columnIndex];
if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaries, index, yy)) {
if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaryStartY, index, yy, fluidHeight)) {
continue;
}
double localThreshold = passThreshold[index];
@@ -870,7 +870,7 @@ public class IrisCaveCarver3D {
double surfaceBreakThresholdBoost,
int[] columnMaxY,
int[] fluidMaxY,
long[] surfaceFluidBoundaries,
int[] surfaceFluidBoundaryStartY,
int[] surfaceBreakFloorY,
boolean[] surfaceBreakColumn,
double[] columnThreshold,
@@ -909,7 +909,7 @@ public class IrisCaveCarver3D {
double density = sampleDensityOptimized(scratch, x, y, z);
int carveMaxY = Math.min(columnTopY, y + sampleStep - 1);
for (int yy = y; yy <= carveMaxY; yy++) {
if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaries, index, yy)) {
if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaryStartY, index, yy, fluidHeight)) {
continue;
}
double localThreshold = threshold;
@@ -70,7 +70,7 @@ public class IrisStructureComponent extends IrisMantleComponent {
private static final MatterCavern CARVE_CAVERN = new MatterCavern(true, "", (byte) 3);
public IrisStructureComponent(EngineMantle engineMantle) {
super(engineMantle, ReservedFlag.JIGSAW, 4);
super(engineMantle, ReservedFlag.JIGSAW, 3);
}
@Override
@@ -104,19 +104,20 @@ public class MantleCarvingComponent extends IrisMantleComponent {
PrecisionStopwatch resolveStopwatch = PrecisionStopwatch.start();
List<WeightedProfile> weightedProfiles = resolveWeightedProfiles(x, z, complex, resolverState);
getEngineMantle().getEngine().getMetrics().getCarveResolve().put(resolveStopwatch.getMilliseconds());
long[] surfaceFluidBoundaries = blendScratch.surfaceFluidBoundaries;
int fluidHeight = getDimension().getFluidHeight();
int[] surfaceFluidBoundaryStartY = blendScratch.surfaceFluidBoundaryStartY;
SurfaceFluidBoundaryPlan.fill(
chunkSurfaceHeights,
blendScratch.fieldSurfaceHeights,
blendScratch.fieldHasFluid,
blendScratch.fieldFluidHeights,
FIELD_SIZE,
BLEND_RADIUS,
surfaceFluidBoundaries
fluidHeight,
surfaceFluidBoundaryStartY
);
CaveFluidSupportPlan fluidSupportPlan = new CaveFluidSupportPlan();
for (WeightedProfile weightedProfile : weightedProfiles) {
carveProfile(weightedProfile, writer, x, z, chunkSurfaceHeights, surfaceFluidBoundaries, fluidSupportPlan);
carveProfile(weightedProfile, writer, x, z, chunkSurfaceHeights, surfaceFluidBoundaryStartY, fluidSupportPlan);
}
UpperDimensionContext upperCtx = getEngineMantle().getEngine().getUpperContext();
@@ -131,7 +132,8 @@ public class MantleCarvingComponent extends IrisMantleComponent {
chunkSurfaceHeights,
maxSurfaceBreakDepth(weightedProfiles),
writer.getMantle().getWorldHeight() - 1,
surfaceFluidBoundaries
surfaceFluidBoundaryStartY,
fluidHeight
);
}
}
@@ -146,11 +148,11 @@ public class MantleCarvingComponent extends IrisMantleComponent {
@ChunkCoordinates
private void carveProfile(WeightedProfile weightedProfile, MantleWriter writer, int cx, int cz,
int[] chunkSurfaceHeights, long[] surfaceFluidBoundaries,
int[] chunkSurfaceHeights, int[] surfaceFluidBoundaryStartY,
CaveFluidSupportPlan fluidSupportPlan) {
IrisCaveCarver3D carver = getCarver(weightedProfile.profile);
carver.carve(writer, cx, cz, weightedProfile.columnWeights, MIN_WEIGHT, THRESHOLD_PENALTY,
weightedProfile.worldYRange, chunkSurfaceHeights, surfaceFluidBoundaries, null, fluidSupportPlan);
weightedProfile.worldYRange, chunkSurfaceHeights, surfaceFluidBoundaryStartY, null, fluidSupportPlan);
}
private void carveUpperTerrain(UpperDimensionContext upperCtx, List<WeightedProfile> normalProfiles,
@@ -473,9 +475,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
private void prefillProfileFieldSamples(int startX, int startZ, IrisComplex complex, BlendScratch blendScratch) {
fillFieldHeights(complex.getHeightStream(), startX, startZ, blendScratch.fieldSurfaceHeights);
fillFieldHeights(complex.getRiverWaterSurfaceStream(), startX, startZ, blendScratch.fieldFluidHeights);
fillFieldFluidPresence(complex, startX, startZ, blendScratch.fieldSurfaceHeights,
blendScratch.fieldFluidHeights, blendScratch.fieldHasFluid);
fillFieldFluidPresence(complex.getFluidStream(), startX, startZ, blendScratch.fieldHasFluid);
fillFieldObjects(complex.getRegionStream(), startX, startZ, blendScratch.fieldRegions);
fillFieldObjects(complex.getTrueBiomeStream(), startX, startZ, blendScratch.fieldSurfaceBiomes);
fillFieldObjects(complex.getCaveBiomeStream(), startX, startZ, blendScratch.fieldCaveBiomes);
@@ -499,20 +499,11 @@ public class MantleCarvingComponent extends IrisMantleComponent {
}
}
private void fillFieldFluidPresence(
IrisComplex complex,
int startX,
int startZ,
double[] surfaceHeights,
double[] fluidHeights,
boolean[] target
) {
private void fillFieldFluidPresence(ProceduralStream<PlatformBlockState> stream, int startX, int startZ, boolean[] target) {
for (int fieldX = 0; fieldX < FIELD_SIZE; fieldX++) {
int worldX = startX + fieldX;
for (int fieldZ = 0; fieldZ < FIELD_SIZE; fieldZ++) {
int fieldIndex = (fieldX * FIELD_SIZE) + fieldZ;
target[fieldIndex] = B.isFluid(complex.resolveSurfaceFluid(worldX, startZ + fieldZ))
&& Math.round(surfaceHeights[fieldIndex]) < Math.round(fluidHeights[fieldIndex]);
target[(fieldX * FIELD_SIZE) + fieldZ] = B.isFluid(stream.get(worldX, startZ + fieldZ));
}
}
}
@@ -706,13 +697,12 @@ public class MantleCarvingComponent extends IrisMantleComponent {
private final IdentityHashMap<IrisCaveProfile, Boolean> activeProfiles = new IdentityHashMap<>();
private final List<IrisCaveProfile> profileOrder = new ArrayList<>();
private final double[] fieldSurfaceHeights = new double[FIELD_SIZE * FIELD_SIZE];
private final double[] fieldFluidHeights = new double[FIELD_SIZE * FIELD_SIZE];
private final boolean[] fieldHasFluid = new boolean[FIELD_SIZE * FIELD_SIZE];
private final long[] surfaceFluidBoundaries = new long[CHUNK_AREA];
private final IrisRegion[] fieldRegions = new IrisRegion[FIELD_SIZE * FIELD_SIZE];
private final IrisBiome[] fieldSurfaceBiomes = new IrisBiome[FIELD_SIZE * FIELD_SIZE];
private final IrisBiome[] fieldCaveBiomes = new IrisBiome[FIELD_SIZE * FIELD_SIZE];
private final int[] chunkSurfaceHeights = new int[CHUNK_AREA];
private final int[] surfaceFluidBoundaryStartY = new int[CHUNK_AREA];
private final double[] chunkSurfaceHeightSamples = new double[CHUNK_AREA];
}
}
@@ -59,7 +59,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
private static final IrisObjectRotation ROTATION_NONE = IrisObjectRotation.of(0, 0, 0);
public MantleFloatingObjectComponent(EngineMantle engineMantle) {
super(engineMantle, ReservedFlag.FLOATING_OBJECT, 3);
super(engineMantle, ReservedFlag.FLOATING_OBJECT, 2);
}
@Override
@@ -48,7 +48,6 @@ import art.arcane.iris.engine.object.IrisProceduralPlacement;
import art.arcane.iris.engine.object.IrisProceduralTree;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.ObjectPlaceMode;
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
@@ -84,7 +83,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
private static final Set<String> MISSING_LOAD_KEY_WARNED = ConcurrentHashMap.newKeySet();
public MantleObjectComponent(EngineMantle engineMantle) {
super(engineMantle, ReservedFlag.OBJECT, 2);
super(engineMantle, ReservedFlag.OBJECT, 1);
}
private static String placementMarker(IrisObject object, int id, String context) {
@@ -577,18 +576,11 @@ public class MantleObjectComponent extends IrisMantleComponent {
minDepthBelowSurface,
anchorCache
);
RiverCaveHydrology hydrology = candidateY < 0
? null
: writer.getDataIfPresent(candidateX, candidateY, candidateZ, RiverCaveHydrology.class);
MatterCavern cavern = candidateY < 0
? null
: writer.getDataIfPresent(candidateX, candidateY, candidateZ, MatterCavern.class);
if (candidateY < 0
|| caveAnchorBiomeConflicts(candidateX, candidateY, candidateZ, expectedCaveBiomeKey)
|| !acceptsCaveAnchorFluid(
underwater,
hydrology == null ? cavern : hydrology.asCavern(),
hydrology,
writer.getDataIfPresent(candidateX, candidateY, candidateZ, MatterCavern.class),
candidateY,
getDimension().getCaveLavaHeight())) {
continue;
@@ -599,19 +591,6 @@ public class MantleObjectComponent extends IrisMantleComponent {
}
static boolean acceptsCaveAnchorFluid(boolean underwater, MatterCavern cavern, int y, int lavaHeight) {
return acceptsCaveAnchorFluid(underwater, cavern, null, y, lavaHeight);
}
static boolean acceptsCaveAnchorFluid(
boolean underwater,
MatterCavern cavern,
RiverCaveHydrology hydrology,
int y,
int lavaHeight
) {
if (hydrology != null && hydrology.protectsPlacement()) {
return false;
}
if (cavern == null || !cavern.isCavern()) {
return false;
}
@@ -1,174 +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.CaveVoxelView;
import art.arcane.iris.engine.river.cave.RiverCaveFluidKind;
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
import art.arcane.iris.engine.data.cache.Cache;
import art.arcane.iris.engine.object.IrisProceduralBlocks;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.volmlib.util.function.Function2;
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.MatterCavern;
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import java.util.Objects;
import java.util.function.BiConsumer;
final class MantleRiverCaveVoxelView implements MantleRiverHydrologyComponent.TunnelVoxelView {
private static final int CLOSED_COLUMN = Integer.MAX_VALUE;
private static final int CACHE_MISS = Integer.MIN_VALUE;
private final Mantle<Matter> mantle;
private final int worldHeight;
private final Function2<Integer, Integer, Integer> surfaceHeight;
private final Function2<Integer, Integer, PlatformBlockState> compatibleFluid;
private final RiverCaveFluidKind planningFluidKind;
private final BiConsumer<Integer, Integer> chunkLoader;
private final LongOpenHashSet loadedChunks;
private final Long2IntOpenHashMap openFloorCache;
private final Long2IntOpenHashMap surfaceHeightCache;
MantleRiverCaveVoxelView(
Mantle<Matter> mantle,
int worldHeight,
Function2<Integer, Integer, Integer> surfaceHeight,
Function2<Integer, Integer, PlatformBlockState> compatibleFluid,
RiverCaveFluidKind planningFluidKind,
BiConsumer<Integer, Integer> chunkLoader
) {
this.mantle = Objects.requireNonNull(mantle);
this.worldHeight = worldHeight;
this.surfaceHeight = Objects.requireNonNull(surfaceHeight);
this.compatibleFluid = Objects.requireNonNull(compatibleFluid);
this.planningFluidKind = Objects.requireNonNull(planningFluidKind);
this.chunkLoader = Objects.requireNonNull(chunkLoader);
loadedChunks = new LongOpenHashSet();
openFloorCache = new Long2IntOpenHashMap();
openFloorCache.defaultReturnValue(CACHE_MISS);
surfaceHeightCache = new Long2IntOpenHashMap();
surfaceHeightCache.defaultReturnValue(CACHE_MISS);
}
@Override
public boolean isInWorld(CavePosition position) {
return position.y() > 0 && position.y() < worldHeight - 1;
}
@Override
public CaveVoxel voxelAt(CavePosition position) {
RiverCaveHydrology hydrology = dataIfPresent(position, RiverCaveHydrology.class);
if (hydrology != null && hydrology.fluidKind() != planningFluidKind) {
return CaveVoxel.INCOMPATIBLE_FLUID;
}
MatterCavern cavern = dataIfPresent(position, MatterCavern.class);
if (cavern != null) {
if (cavern.isLava()) {
PlatformBlockState expected = compatibleFluid.apply(position.x(), position.z());
return expected != null
&& IrisProceduralBlocks.materialKey(expected).endsWith(":lava")
? CaveVoxel.COMPATIBLE_FLUID
: CaveVoxel.LAVA;
}
if (cavern.getLiquid() == 1) {
return CaveVoxel.COMPATIBLE_FLUID;
}
return CaveVoxel.CAVE_AIR;
}
PlatformBlockState block = dataIfPresent(position, PlatformBlockState.class);
if (block == null) {
return position.y() > surfaceY(position.x(), position.z())
? CaveVoxel.CAVE_AIR
: CaveVoxel.SOLID;
}
if (!block.isFluid()) {
return CaveVoxel.SOLID;
}
PlatformBlockState expected = compatibleFluid.apply(position.x(), position.z());
if (expected != null
&& IrisProceduralBlocks.materialKey(expected).equals(IrisProceduralBlocks.materialKey(block))) {
return CaveVoxel.COMPATIBLE_FLUID;
}
return IrisProceduralBlocks.materialKey(block).endsWith(":lava")
? CaveVoxel.LAVA
: CaveVoxel.INCOMPATIBLE_FLUID;
}
@Override
public boolean isOpenToSurface(CavePosition position) {
if (!isInWorld(position) || voxelAt(position) == CaveVoxel.SOLID) {
return false;
}
if (position.y() > surfaceY(position.x(), position.z())) {
return true;
}
long key = Cache.key(position.x(), position.z());
int openFloor = openFloorCache.get(key);
if (openFloor == CACHE_MISS) {
openFloor = resolveOpenFloor(position.x(), position.z());
openFloorCache.put(key, openFloor);
}
return openFloor != CLOSED_COLUMN && position.y() >= openFloor;
}
@Override
public RiverCaveHydrology riverHydrologyAt(CavePosition position) {
return dataIfPresent(position, RiverCaveHydrology.class);
}
private int resolveOpenFloor(int x, int z) {
int top = surfaceY(x, z);
CavePosition surface = new CavePosition(x, top, z);
if (voxelAt(surface) == CaveVoxel.SOLID) {
return CLOSED_COLUMN;
}
int y = top;
while (y > 0 && voxelAt(new CavePosition(x, y - 1, z)) != CaveVoxel.SOLID) {
y--;
}
return y;
}
private int surfaceY(int x, int z) {
long key = Cache.key(x, z);
int cached = surfaceHeightCache.get(key);
if (cached != CACHE_MISS) {
return cached;
}
int resolved = Math.max(1, Math.min(worldHeight - 2, surfaceHeight.apply(x, z)));
surfaceHeightCache.put(key, resolved);
return resolved;
}
private <T> T dataIfPresent(CavePosition position, Class<T> type) {
int chunkX = position.x() >> 4;
int chunkZ = position.z() >> 4;
long chunkKey = Mantle.key(chunkX, chunkZ);
if (loadedChunks.add(chunkKey)) {
chunkLoader.accept(chunkX, chunkZ);
}
TectonicPlate<Matter> plate = mantle.getLoadedRegions().get(Mantle.key(chunkX >> 5, chunkZ >> 5));
if (plate == null || plate.isClosed()) {
return null;
}
MantleChunk<Matter> chunk = plate.get(chunkX & 31, chunkZ & 31);
int section = position.y() >> 4;
if (chunk == null || !chunk.exists(section)) {
return null;
}
Matter matter = chunk.get(section);
if (matter == null || !matter.hasSlice(type)) {
return null;
}
return matter.<T>getSlice(type).get(
position.x() & 15,
position.y() & 15,
position.z() & 15
);
}
}
@@ -32,17 +32,16 @@ final class SurfaceFluidBoundaryPlan {
int[] chunkSurfaceHeights,
double[] fieldSurfaceHeights,
boolean[] fieldHasFluid,
double[] fieldFluidHeights,
int fieldSize,
int padding,
long[] boundaries
int fluidHeight,
int[] boundaryStartY
) {
if (chunkSurfaceHeights == null || chunkSurfaceHeights.length < CHUNK_AREA
|| boundaries == null || boundaries.length < CHUNK_AREA
|| boundaryStartY == null || boundaryStartY.length < CHUNK_AREA
|| padding < 1 || fieldSize < CHUNK_SIZE + (padding * 2)
|| fieldSurfaceHeights == null || fieldSurfaceHeights.length < fieldSize * fieldSize
|| fieldHasFluid == null || fieldHasFluid.length < fieldSize * fieldSize
|| fieldFluidHeights == null || fieldFluidHeights.length < fieldSize * fieldSize) {
|| fieldHasFluid == null || fieldHasFluid.length < fieldSize * fieldSize) {
throw new IllegalArgumentException("Surface fluid boundary fields do not cover a padded chunk");
}
@@ -52,67 +51,44 @@ final class SurfaceFluidBoundaryPlan {
int fieldZ = localZ + padding;
int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ);
int boundaryY = NO_BOUNDARY;
int boundaryEndY = Integer.MIN_VALUE;
int surfaceY = chunkSurfaceHeights[columnIndex];
int fieldIndex = (fieldX * fieldSize) + fieldZ;
int fluidHeight = roundedHeight(fieldFluidHeights[fieldIndex]);
if (fieldHasFluid[fieldIndex] && surfaceY < fluidHeight) {
boundaryY = surfaceY;
boundaryEndY = fluidHeight;
}
long boundary = expandBoundary(boundaryY, boundaryEndY, fieldSurfaceHeights,
fieldHasFluid, fieldFluidHeights, ((fieldX - 1) * fieldSize) + fieldZ);
boundary = expandBoundary(startY(boundary), endY(boundary), fieldSurfaceHeights,
fieldHasFluid, fieldFluidHeights, ((fieldX + 1) * fieldSize) + fieldZ);
boundary = expandBoundary(startY(boundary), endY(boundary), fieldSurfaceHeights,
fieldHasFluid, fieldFluidHeights, (fieldX * fieldSize) + fieldZ - 1);
boundaries[columnIndex] = expandBoundary(startY(boundary), endY(boundary), fieldSurfaceHeights,
fieldHasFluid, fieldFluidHeights, (fieldX * fieldSize) + fieldZ + 1);
boundaryY = lowerBoundary(boundaryY, fieldSurfaceHeights, fieldHasFluid,
((fieldX - 1) * fieldSize) + fieldZ, fluidHeight);
boundaryY = lowerBoundary(boundaryY, fieldSurfaceHeights, fieldHasFluid,
((fieldX + 1) * fieldSize) + fieldZ, fluidHeight);
boundaryY = lowerBoundary(boundaryY, fieldSurfaceHeights, fieldHasFluid,
(fieldX * fieldSize) + fieldZ - 1, fluidHeight);
boundaryY = lowerBoundary(boundaryY, fieldSurfaceHeights, fieldHasFluid,
(fieldX * fieldSize) + fieldZ + 1, fluidHeight);
boundaryStartY[columnIndex] = boundaryY;
}
}
}
static boolean protects(long[] boundaries, int columnIndex, int y) {
if (boundaries == null || columnIndex < 0 || columnIndex >= boundaries.length) {
return false;
}
long boundary = boundaries[columnIndex];
return y >= startY(boundary) && y <= endY(boundary);
static boolean protects(int[] boundaryStartY, int columnIndex, int y, int fluidHeight) {
return boundaryStartY != null
&& columnIndex >= 0
&& columnIndex < boundaryStartY.length
&& y >= boundaryStartY[columnIndex]
&& y <= fluidHeight;
}
static int startY(long boundary) {
return (int) (boundary >> 32);
}
static int endY(long boundary) {
return (int) boundary;
}
private static long expandBoundary(
private static int lowerBoundary(
int currentBoundaryY,
int currentBoundaryEndY,
double[] fieldSurfaceHeights,
boolean[] fieldHasFluid,
double[] fieldFluidHeights,
int fieldIndex
int fieldIndex,
int fluidHeight
) {
int fluidHeight = roundedHeight(fieldFluidHeights[fieldIndex]);
int neighborSurfaceY = (int) Math.round(fieldSurfaceHeights[fieldIndex]);
if (!fieldHasFluid[fieldIndex] || neighborSurfaceY >= fluidHeight) {
return boundary(currentBoundaryY, currentBoundaryEndY);
return currentBoundaryY;
}
return boundary(
Math.min(currentBoundaryY, neighborSurfaceY + 1),
Math.max(currentBoundaryEndY, fluidHeight)
);
}
private static int roundedHeight(double height) {
return Double.isFinite(height) ? (int) Math.round(height) : Integer.MIN_VALUE;
}
static long boundary(int startY, int endY) {
return ((long) startY << 32) | (endY & 0xffffffffL);
return Math.min(currentBoundaryY, neighborSurfaceY + 1);
}
}
@@ -28,9 +28,6 @@ import art.arcane.iris.engine.object.IrisDecorationPart;
import art.arcane.iris.engine.object.IrisDecorator;
import art.arcane.iris.engine.object.IrisDimensionCarvingResolver;
import art.arcane.iris.engine.object.IrisProceduralBlocks;
import art.arcane.iris.engine.object.IrisRiverCaves;
import art.arcane.iris.engine.river.cave.RiverCaveAction;
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.util.common.data.B;
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
@@ -59,8 +56,6 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
private static final int CAVE_BIOME_BLEND_RADIUS = 3;
private static final int CAVE_BIOME_BLEND_CENTER_WEIGHT = 4;
private static final int CAVE_BIOME_BLEND_TOTAL_WEIGHT = 8;
private static final int RIVER_BIOME_INHERITANCE_CELL_SIZE = 4;
private static final long RIVER_BIOME_INHERITANCE_SALT = 0x4CF5AD432745937FL;
private static final MatterCavern BASIC_CAVERN = new MatterCavern(true, "", (byte) 0);
private final RNG rng;
private final PlatformBlockState AIR = B.getState("CAVE_AIR");
@@ -110,28 +105,49 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
PrecisionStopwatch resolveStopwatch = PrecisionStopwatch.start();
int worldHeightSpan = getEngine().getWorld().maxHeight() - getEngine().getWorld().minHeight();
int caveLavaHeight = getEngine().getDimension().getCaveLavaHeight();
CarveResolutionContext resolutionContext = new CarveResolutionContext(
output,
context,
scratch,
columnMasks,
upperSurfaceHeights,
worldHeightSpan,
caveLavaHeight,
chunkBlockX,
chunkBlockZ
);
CarveResolver carveResolver = new CarveResolver(resolutionContext);
mantleChunk.iterate(MatterCavern.class, (xx, yy, zz, cavern) -> carveResolver.apply(
xx,
yy,
zz,
cavern,
dataIfPresent(mantleChunk, xx, yy, zz, RiverCaveHydrology.class)
));
mantleChunk.iterate(RiverCaveHydrology.class, (xx, yy, zz, hydrology) -> {
if (dataIfPresent(mantleChunk, xx, yy, zz, MatterCavern.class) == null) {
carveResolver.apply(xx, yy, zz, null, hydrology);
mantleChunk.iterate(MatterCavern.class, (xx, yy, zz, cavern) -> {
if (cavern == null) {
return;
}
if (yy >= worldHeightSpan || yy <= 0) {
return;
}
int rx = xx & 15;
int rz = zz & 15;
int columnIndex = PowerOfTwoCoordinates.packLocal16(rx, rz);
if (upperSurfaceHeights != null && yy >= upperSurfaceHeights[columnIndex]) {
return;
}
PlatformBlockState current = output.getRaw(rx, yy, rz);
boolean explicitCarveIntent = hasExplicitCarveIntent(cavern);
if (shouldPreserveExistingFluid(cavern, current)) {
return;
}
columnMasks[columnIndex].add(yy);
if (!cavern.getCustomBiome().isEmpty()) {
scratch.customCaveBiomePresent = true;
}
if (current.isAir() && !explicitCarveIntent) {
return;
}
if (explicitCarveIntent) {
// Only a fluid cavern consumes the fluid sample, and on the maintenance path that
// sample is a full procedural stream evaluation, so never take it per voxel.
PlatformBlockState fluid = isFluidIntent(cavern) ? context.getFluid().get(rx, rz) : null;
output.setRaw(rx, yy, rz, resolveExplicitCarveState(cavern, fluid, LAVA, AIR));
} else if (usesDefaultLava(caveLavaHeight, yy)) {
output.setRaw(rx, yy, rz, LAVA);
} else {
output.setRaw(rx, yy, rz, AIR);
}
});
if (scratch.customCaveBiomePresent) {
@@ -144,12 +160,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
PrecisionStopwatch applyStopwatch = PrecisionStopwatch.start();
try {
walls.forEach((rx, yy, rz, cavern, riverBoundary) -> {
RiverCaveHydrology hydrology = dataIfPresent(
mantleChunk, rx, yy, rz, RiverCaveHydrology.class);
if (hydrology != null && hydrology.protectsPlacement()) {
return;
}
walls.forEach((rx, yy, rz, cavern) -> {
int worldX = rx + chunkBlockX;
int worldZ = rz + chunkBlockZ;
String customBiome = cavern.getCustomBiome();
@@ -168,36 +179,14 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
});
for (int columnIndex = 0; columnIndex < 256; columnIndex++) {
processColumnFromMask(
output,
mantleChunk,
mantle,
columnMasks[columnIndex],
columnIndex,
x,
z,
resolverState,
caveBiomeCache,
customBiomeCache
);
processColumnFromMask(output, mantleChunk, mantle, columnMasks[columnIndex], columnIndex, x, z, resolverState, caveBiomeCache, customBiomeCache);
}
for (int columnIndex = 0; columnIndex < 256; columnIndex++) {
if (boundaryMasks[columnIndex].isEmpty() || !columnMasks[columnIndex].isEmpty()) {
continue;
}
processBoundaryColumnFromMask(
output,
mantleChunk,
boundaryMasks[columnIndex],
walls,
columnIndex,
x,
z,
resolverState,
caveBiomeCache,
customBiomeCache
);
processBoundaryColumnFromMask(output, boundaryMasks[columnIndex], walls, columnIndex, x, z, resolverState, caveBiomeCache, customBiomeCache);
}
// Surface-break carving must not leave an ore cap suspended across the opening.
@@ -263,150 +252,6 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
return cavern.getLiquid() == 3 ? air : null;
}
static MatterCavern composeCavern(MatterCavern baseline, RiverCaveHydrology hydrology) {
return hydrology == null ? baseline : hydrology.asCavern();
}
static PlatformBlockState resolveHydrologyState(
RiverCaveHydrology hydrology,
PlatformBlockState current,
PlatformBlockState fluid,
PlatformBlockState air
) {
if (hydrology == null) {
return null;
}
return switch (hydrology.action()) {
case WET_SOURCE -> fluid;
case FALLING_FLUID -> fallingFluidState(fluid);
case DRY_AIR -> air;
case SEAL_GUARD -> normalizeWaterlogging(current, null);
};
}
static PlatformBlockState normalizeWaterlogging(PlatformBlockState state, PlatformBlockState resultingFluid) {
if (state == null || B.isFluid(state) || !IrisProceduralBlocks.hasProperty(state, "waterlogged")) {
return state;
}
String target = resultingFluid != null && resultingFluid.isWater() ? "true" : "false";
if (target.equals(IrisProceduralBlocks.propertyValue(state, "waterlogged"))) {
return state;
}
return state.withProperty("waterlogged", target);
}
static PlatformBlockState normalizeHydrologyWaterlogging(
PlatformBlockState state,
MatterCavern baseline,
RiverCaveHydrology hydrology,
PlatformBlockState columnFluid
) {
if (hydrology == null) {
return state;
}
MatterCavern composed = composeCavern(baseline, hydrology);
PlatformBlockState resultingFluid = isFluidIntent(composed) ? columnFluid : null;
return normalizeWaterlogging(state, resultingFluid);
}
private static PlatformBlockState fallingFluidState(PlatformBlockState fluid) {
if (fluid == null || !IrisProceduralBlocks.hasProperty(fluid, "level")) {
return fluid;
}
if ("8".equals(IrisProceduralBlocks.propertyValue(fluid, "level"))) {
return fluid;
}
return fluid.withProperty("level", "8");
}
private final class CarveResolver {
private final CarveResolutionContext context;
private CarveResolver(CarveResolutionContext context) {
this.context = context;
}
private void apply(
int x,
int y,
int z,
MatterCavern baseline,
RiverCaveHydrology hydrology
) {
if (y >= context.worldHeightSpan() || y <= 0) {
return;
}
int localX = x & 15;
int localZ = z & 15;
int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ);
if (context.upperSurfaceHeights() != null && y >= context.upperSurfaceHeights()[columnIndex]) {
return;
}
PlatformBlockState current = context.output().getRaw(localX, y, localZ);
if (hydrology != null && hydrology.action() == RiverCaveAction.SEAL_GUARD) {
PlatformBlockState normalized = resolveHydrologyState(hydrology, current, null, AIR);
if (normalized != current) {
context.output().setRaw(localX, y, localZ, normalized);
}
return;
}
MatterCavern cavern = composeCavern(baseline, hydrology);
if (cavern == null || shouldPreserveExistingFluid(cavern, current)) {
return;
}
context.columnMasks()[columnIndex].add(y);
if (!cavern.getCustomBiome().isEmpty()) {
context.scratch().customCaveBiomePresent = true;
}
boolean explicitCarveIntent = hasExplicitCarveIntent(cavern);
if (current.isAir() && !explicitCarveIntent) {
return;
}
PlatformBlockState fluid = null;
if (isFluidIntent(cavern)) {
fluid = hydrology == null
? context.chunkContext().getFluid().get(localX, localZ)
: getComplex().resolveRiverCaveFluid(
hydrology.fluidKind(),
context.chunkBlockX() + localX,
context.chunkBlockZ() + localZ
);
}
if (hydrology != null) {
context.output().setRaw(localX, y, localZ,
resolveHydrologyState(hydrology, current, fluid, AIR));
return;
}
if (explicitCarveIntent) {
context.output().setRaw(localX, y, localZ,
resolveExplicitCarveState(cavern, fluid, LAVA, AIR));
} else if (usesDefaultLava(context.caveLavaHeight(), y)) {
context.output().setRaw(localX, y, localZ, LAVA);
} else {
context.output().setRaw(localX, y, localZ, AIR);
}
}
}
private record CarveResolutionContext(
Hunk<PlatformBlockState> output,
ChunkContext chunkContext,
IrisCarveScratch scratch,
CarveColumnMask[] columnMasks,
int[] upperSurfaceHeights,
int worldHeightSpan,
int caveLavaHeight,
int chunkBlockX,
int chunkBlockZ
) {
}
private void addInternalWallsFromMasks(CarveWallBuffer walls, CarveColumnMask[] columnMasks) {
for (int columnIndex = 0; columnIndex < 256; columnIndex++) {
CarveColumnMask columnMask = columnMasks[columnIndex];
@@ -419,16 +264,16 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
int yy = columnMask.nextSetBit(0);
while (yy >= 0) {
if (rz < 15 && !columnMasks[columnIndex + 1].contains(yy)) {
walls.put(rx, yy, rz + 1, BASIC_CAVERN, false);
walls.put(rx, yy, rz + 1, BASIC_CAVERN);
}
if (rx < 15 && !columnMasks[columnIndex + 16].contains(yy)) {
walls.put(rx + 1, yy, rz, BASIC_CAVERN, false);
walls.put(rx + 1, yy, rz, BASIC_CAVERN);
}
if (rz > 0 && !columnMasks[columnIndex - 1].contains(yy)) {
walls.put(rx, yy, rz - 1, BASIC_CAVERN, false);
walls.put(rx, yy, rz - 1, BASIC_CAVERN);
}
if (rx > 0 && !columnMasks[columnIndex - 16].contains(yy)) {
walls.put(rx - 1, yy, rz, BASIC_CAVERN, false);
walls.put(rx - 1, yy, rz, BASIC_CAVERN);
}
yy = columnMask.nextSetBit(yy + 1);
}
@@ -446,21 +291,19 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
int rz = columnIndex & 15;
int yy = columnMask.nextSetBit(0);
while (yy >= 0) {
MatterCavern cavern = composedCavernAt(mc, rx, yy, rz);
MatterCavern cavern = mc.get(rx, yy, rz, MatterCavern.class);
if (cavern != null) {
RiverCaveHydrology hydrology = dataIfPresent(mc, rx, yy, rz, RiverCaveHydrology.class);
boolean riverBoundary = hydrology != null && !hydrology.floodedBiomeKey().isEmpty();
if (rz < 15 && composedCavernAt(mc, rx, yy, rz + 1) == null) {
walls.put(rx, yy, rz + 1, cavern, riverBoundary);
if (rz < 15 && mc.get(rx, yy, rz + 1, MatterCavern.class) == null) {
walls.put(rx, yy, rz + 1, cavern);
}
if (rx < 15 && composedCavernAt(mc, rx + 1, yy, rz) == null) {
walls.put(rx + 1, yy, rz, cavern, riverBoundary);
if (rx < 15 && mc.get(rx + 1, yy, rz, MatterCavern.class) == null) {
walls.put(rx + 1, yy, rz, cavern);
}
if (rz > 0 && composedCavernAt(mc, rx, yy, rz - 1) == null) {
walls.put(rx, yy, rz - 1, cavern, riverBoundary);
if (rz > 0 && mc.get(rx, yy, rz - 1, MatterCavern.class) == null) {
walls.put(rx, yy, rz - 1, cavern);
}
if (rx > 0 && composedCavernAt(mc, rx - 1, yy, rz) == null) {
walls.put(rx - 1, yy, rz, cavern, riverBoundary);
if (rx > 0 && mc.get(rx - 1, yy, rz, MatterCavern.class) == null) {
walls.put(rx - 1, yy, rz, cavern);
}
}
yy = columnMask.nextSetBit(yy + 1);
@@ -527,19 +370,16 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
int neighborX,
int neighborZ
) {
if (composedCavernAt(mc, localX, yy, localZ) != null) {
if (mc.get(localX, yy, localZ, MatterCavern.class) != null) {
return;
}
MatterCavern neighbor = composedCavernAt(neighborChunk, neighborX, yy, neighborZ);
MatterCavern neighbor = neighborChunk.get(neighborX, yy, neighborZ, MatterCavern.class);
if (neighbor == null) {
return;
}
RiverCaveHydrology hydrology = dataIfPresent(
neighborChunk, neighborX, yy, neighborZ, RiverCaveHydrology.class);
boolean riverBoundary = hydrology != null && !hydrology.floodedBiomeKey().isEmpty();
walls.put(localX, yy, localZ, neighbor, riverBoundary);
walls.put(localX, yy, localZ, neighbor);
int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ);
boundaryMasks[columnIndex].add(yy);
}
@@ -552,24 +392,6 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
return plate.get(chunkX & 31, chunkZ & 31);
}
private MatterCavern composedCavernAt(MantleChunk<Matter> mantleChunk, int x, int y, int z) {
MatterCavern baseline = dataIfPresent(mantleChunk, x, y, z, MatterCavern.class);
RiverCaveHydrology hydrology = dataIfPresent(mantleChunk, x, y, z, RiverCaveHydrology.class);
return composeCavern(baseline, hydrology);
}
private static <T> T dataIfPresent(MantleChunk<Matter> mantleChunk, int x, int y, int z, Class<T> type) {
int section = y >> 4;
if (y < 0 || !mantleChunk.exists(section)) {
return null;
}
Matter matter = mantleChunk.get(section);
if (matter == null || !matter.hasSlice(type)) {
return null;
}
return matter.<T>getSlice(type).get(x & 15, y & 15, z & 15);
}
private void processColumnFromMask(
Hunk<PlatformBlockState> output,
MantleChunk<Matter> mc,
@@ -607,8 +429,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
zone.ceiling = buf;
} else {
if (zone.isValid(getEngine())) {
processZone(output, mc, mantle, zone, rx, rz, worldX, worldZ, resolverState,
caveBiomeCache, customBiomeCache);
processZone(output, mc, mantle, zone, rx, rz, worldX, worldZ, resolverState, caveBiomeCache, customBiomeCache);
}
zone = new CaveZone();
zone.setFloor(y);
@@ -620,14 +441,12 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
}
if (zone.isValid(getEngine())) {
processZone(output, mc, mantle, zone, rx, rz, worldX, worldZ, resolverState,
caveBiomeCache, customBiomeCache);
processZone(output, mc, mantle, zone, rx, rz, worldX, worldZ, resolverState, caveBiomeCache, customBiomeCache);
}
}
private void processBoundaryColumnFromMask(
Hunk<PlatformBlockState> output,
MantleChunk<Matter> mantleChunk,
CarveColumnMask boundaryMask,
CarveWallBuffer walls,
int columnIndex,
@@ -654,21 +473,18 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
if (y == zoneCeiling + 1) {
zoneCeiling = y;
} else {
paintBoundaryZone(output, mantleChunk, walls, rx, rz, worldX, worldZ, zoneFloor, zoneCeiling,
resolverState, caveBiomeCache, customBiomeCache);
paintBoundaryZone(output, walls, rx, rz, worldX, worldZ, zoneFloor, zoneCeiling, resolverState, caveBiomeCache, customBiomeCache);
zoneFloor = y;
zoneCeiling = y;
}
y = boundaryMask.nextSetBit(y + 1);
}
paintBoundaryZone(output, mantleChunk, walls, rx, rz, worldX, worldZ, zoneFloor, zoneCeiling,
resolverState, caveBiomeCache, customBiomeCache);
paintBoundaryZone(output, walls, rx, rz, worldX, worldZ, zoneFloor, zoneCeiling, resolverState, caveBiomeCache, customBiomeCache);
}
private void paintBoundaryZone(
Hunk<PlatformBlockState> output,
MantleChunk<Matter> mantleChunk,
CarveWallBuffer walls,
int rx,
int rz,
@@ -682,12 +498,10 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
) {
IrisBiome floorBiome = resolveCaveBoundaryBiome(
walls.get(rx, zoneFloor, rz), worldX, zoneFloor, worldZ,
resolverState, caveBiomeCache, customBiomeCache,
walls.isRiverBoundary(rx, zoneFloor, rz));
resolverState, caveBiomeCache, customBiomeCache);
IrisBiome ceilingBiome = resolveCaveBoundaryBiome(
walls.get(rx, zoneCeiling, rz), worldX, zoneCeiling, worldZ,
resolverState, caveBiomeCache, customBiomeCache,
walls.isRiverBoundary(rx, zoneCeiling, rz));
resolverState, caveBiomeCache, customBiomeCache);
if (floorBiome == null && ceilingBiome == null) {
return;
}
@@ -703,18 +517,9 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
if (floorY < 0) {
break;
}
RiverCaveHydrology hydrology = dataIfPresent(
mantleChunk, rx, floorY, rz, RiverCaveHydrology.class);
if (hydrology != null
&& hydrology.protectsPlacement()
&& hydrology.action() != RiverCaveAction.SEAL_GUARD) {
continue;
}
PlatformBlockState existing = output.getRaw(rx, floorY, rz);
PlatformBlockState layer = floorLayers.get(i);
if (!B.isSolid(existing)
|| !canReplaceRiverGuard(hydrology, layer, false)
|| !canReplaceCaveFloorLayer(output, rx, floorY, rz, layer)) {
if (!B.isSolid(existing) || !canReplaceCaveFloorLayer(output, rx, floorY, rz, layer)) {
continue;
}
if (B.isOre(existing)) {
@@ -734,21 +539,11 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
if (ceilingY >= worldMaxY) {
break;
}
RiverCaveHydrology hydrology = dataIfPresent(
mantleChunk, rx, ceilingY, rz, RiverCaveHydrology.class);
if (hydrology != null
&& hydrology.protectsPlacement()
&& hydrology.action() != RiverCaveAction.SEAL_GUARD) {
continue;
}
PlatformBlockState existing = output.getRaw(rx, ceilingY, rz);
if (!B.isSolid(existing)) {
continue;
}
PlatformBlockState layer = ceilingLayers.get(i);
if (!canReplaceRiverGuard(hydrology, layer, true)) {
continue;
}
if (B.isOre(existing)) {
output.setRaw(rx, ceilingY, rz, B.toDeepSlateOre(existing, layer));
continue;
@@ -770,11 +565,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
return (h & 15L) == 0L;
}
private void processZone(Hunk<PlatformBlockState> output, MantleChunk<Matter> mc, Mantle<Matter> mantle,
CaveZone zone, int rx, int rz, int xx, int zz,
IrisDimensionCarvingResolver.State resolverState,
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache,
Map<String, IrisBiome> customBiomeCache) {
private void processZone(Hunk<PlatformBlockState> output, MantleChunk<Matter> mc, Mantle<Matter> mantle, CaveZone zone, int rx, int rz, int xx, int zz, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache, Map<String, IrisBiome> customBiomeCache) {
int maxY = output.getHeight();
if (zone.ceiling + 1 < maxY && B.isDecorant(output.getRaw(rx, zone.ceiling + 1, rz))) {
@@ -799,7 +590,6 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
IrisBiome floorBiome = resolveCaveBoundaryBiome(mc, rx, zone.floor, rz, xx, zz, resolverState, caveBiomeCache, customBiomeCache);
IrisBiome ceilingBiome = resolveCaveBoundaryBiome(mc, rx, zone.ceiling, rz, xx, zz, resolverState, caveBiomeCache, customBiomeCache);
if (floorBiome == null && ceilingBiome == null) {
normalizeCaveZoneWaterlogging(output, mc, zone, rx, rz, xx, zz);
return;
}
@@ -810,17 +600,9 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
break;
}
int y = zone.floor - i - 1;
RiverCaveHydrology hydrology = dataIfPresent(mc, rx, y, rz, RiverCaveHydrology.class);
if (hydrology != null
&& hydrology.protectsPlacement()
&& hydrology.action() != RiverCaveAction.SEAL_GUARD) {
continue;
}
PlatformBlockState block = floorBlocks.get(i);
PlatformBlockState existing = output.getRaw(rx, y, rz);
if (!B.isSolid(existing)
|| !canReplaceRiverGuard(hydrology, block, false)
|| !canReplaceCaveFloorLayer(output, rx, y, rz, block)) {
if (!B.isSolid(existing) || !canReplaceCaveFloorLayer(output, rx, y, rz, block)) {
continue;
}
if (B.isOre(existing)) {
@@ -838,15 +620,9 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
if (cy >= maxY) {
break;
}
RiverCaveHydrology hydrology = dataIfPresent(mc, rx, cy, rz, RiverCaveHydrology.class);
if (hydrology != null
&& hydrology.protectsPlacement()
&& hydrology.action() != RiverCaveAction.SEAL_GUARD) {
continue;
}
PlatformBlockState block = ceilingBlocks.get(i);
PlatformBlockState existing = output.getRaw(rx, cy, rz);
if (!B.isSolid(existing) || !canReplaceRiverGuard(hydrology, block, true)) {
if (!B.isSolid(existing)) {
continue;
}
if (B.isOre(existing)) {
@@ -870,133 +646,33 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
if (ceilingDecorators.length > 0 && zone.getCeiling() + 1 < maxY && B.isSolid(output.getRaw(rx, zone.getCeiling() + 1, rz))) {
decorant.getCeilingDecorator().decorate(rx, rz, xx, xx, xx, zz, zz, zz, output, ceilingBiome, InferredType.CAVE, zone.getCeiling(), zone.airThickness());
}
normalizeCaveZoneWaterlogging(output, mc, zone, rx, rz, xx, zz);
}
private void normalizeCaveZoneWaterlogging(
Hunk<PlatformBlockState> output,
MantleChunk<Matter> mantleChunk,
CaveZone zone,
int localX,
int localZ,
int worldX,
int worldZ
) {
int minimumY = Math.max(0, zone.floor - 1);
int maximumY = Math.min(output.getHeight() - 1, zone.ceiling + 1);
for (int y = minimumY; y <= maximumY; y++) {
RiverCaveHydrology hydrology = dataIfPresent(
mantleChunk, localX, y, localZ, RiverCaveHydrology.class);
if (hydrology == null) {
continue;
}
MatterCavern baseline = dataIfPresent(
mantleChunk, localX, y, localZ, MatterCavern.class);
PlatformBlockState current = output.getRaw(localX, y, localZ);
PlatformBlockState columnFluid = getComplex().resolveRiverCaveFluid(
hydrology.fluidKind(),
worldX,
worldZ
);
PlatformBlockState normalized = normalizeHydrologyWaterlogging(
current,
baseline,
hydrology,
columnFluid
);
if (normalized != current) {
output.setRaw(localX, y, localZ, normalized);
}
}
}
IrisBiome resolveCaveBoundaryBiome(MantleChunk<Matter> mantleChunk, int x, int y, int z, int worldX, int worldZ, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache, Map<String, IrisBiome> customBiomeCache) {
MatterCavern cavern = composedCavernAt(mantleChunk, x, y, z);
RiverCaveHydrology hydrology = dataIfPresent(
mantleChunk, x, y, z, RiverCaveHydrology.class);
MatterCavern cavern = dataIfPresent(mantleChunk, x, y, z, MatterCavern.class);
return resolveCaveBoundaryBiome(
cavern, worldX, y, worldZ, resolverState, caveBiomeCache, customBiomeCache,
hydrology != null && !hydrology.floodedBiomeKey().isEmpty());
cavern, worldX, y, worldZ, resolverState, caveBiomeCache, customBiomeCache);
}
private static <T> T dataIfPresent(MantleChunk<Matter> mantleChunk, int x, int y, int z, Class<T> type) {
int section = y >> 4;
if (y < 0 || !mantleChunk.exists(section)) {
return null;
}
Matter matter = mantleChunk.get(section);
if (matter == null || !matter.hasSlice(type)) {
return null;
}
return matter.<T>getSlice(type).get(x & 15, y & 15, z & 15);
}
IrisBiome resolveCaveBoundaryBiome(MatterCavern cavern, int worldX, int y, int worldZ, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache, Map<String, IrisBiome> customBiomeCache) {
return resolveCaveBoundaryBiome(
cavern, worldX, y, worldZ, resolverState, caveBiomeCache, customBiomeCache, false);
}
private IrisBiome resolveCaveBoundaryBiome(MatterCavern cavern, int worldX, int y, int worldZ, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache, Map<String, IrisBiome> customBiomeCache, boolean riverBoundary) {
if (cavern != null && !cavern.getCustomBiome().isEmpty()) {
if (riverBoundary && selectsParentRiverBiome(
getEngine().getSeedManager().getCarve(),
worldX,
worldZ,
riverParentBiomeInheritance())) {
IrisBiome parent = resolveRiverParentBiome(
caveBiomeCache, worldX, y, worldZ, resolverState);
if (parent != null) {
return parent;
}
}
return resolveCustomBiome(customBiomeCache, cavern.getCustomBiome());
}
return resolveCaveBiome(caveBiomeCache, worldX, y, worldZ, resolverState);
}
static boolean selectsParentRiverBiome(long seed, int worldX, int worldZ, double inheritance) {
if (inheritance <= 0D) {
return false;
}
if (inheritance >= 1D) {
return true;
}
int cellX = Math.floorDiv(worldX, RIVER_BIOME_INHERITANCE_CELL_SIZE);
int cellZ = Math.floorDiv(worldZ, RIVER_BIOME_INHERITANCE_CELL_SIZE);
long hash = (seed + RIVER_BIOME_INHERITANCE_SALT) ^ BlockPosition.toLong(cellX, 0, cellZ);
hash = (hash ^ (hash >>> 30)) * 0xBF58476D1CE4E5B9L;
hash = (hash ^ (hash >>> 27)) * 0x94D049BB133111EBL;
hash ^= hash >>> 31;
double roll = (hash >>> 11) * 0x1.0p-53;
return roll < inheritance;
}
static boolean canReplaceRiverGuard(
RiverCaveHydrology hydrology,
PlatformBlockState layer,
boolean ceiling
) {
if (hydrology == null || hydrology.action() != RiverCaveAction.SEAL_GUARD) {
return true;
}
return layer != null
&& B.isSolid(layer)
&& !B.isFluid(layer)
&& (!ceiling || !isGravityAffected(layer));
}
private double riverParentBiomeInheritance() {
if (getDimension().getRivers() == null || getDimension().getRivers().getCaves() == null) {
return 0D;
}
IrisRiverCaves caves = getDimension().getRivers().getCaves();
return Math.max(0D, Math.min(1D, caves.getParentBiomeInheritance()));
}
private IrisBiome resolveRiverParentBiome(
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache,
int worldX,
int y,
int worldZ,
IrisDimensionCarvingResolver.State resolverState
) {
IrisBiome parent = resolveCaveBiome(caveBiomeCache, worldX, y, worldZ, resolverState);
if (parent != null && parent == getEngine().getSurfaceBiome(worldX, worldZ)) {
IrisBiome natural = getComplex().getNaturalTrueBiomeStream().get(worldX, worldZ);
return natural == null ? parent : natural;
}
return parent;
}
static boolean canReplaceCaveFloorLayer(Hunk<PlatformBlockState> output, int x, int y, int z, PlatformBlockState layer) {
return !isGravityAffected(layer) || y > 0 && B.isSolid(output.getRaw(x, y - 1, z));
}
@@ -140,7 +140,6 @@ final class CarveWallBuffer {
private int[] keys;
private MatterCavern[] values;
private boolean[] riverBoundaries;
private int mask;
private int resizeAt;
private int size;
@@ -155,12 +154,11 @@ final class CarveWallBuffer {
keys = new int[capacity];
Arrays.fill(keys, EMPTY_KEY);
values = new MatterCavern[capacity];
riverBoundaries = new boolean[capacity];
mask = capacity - 1;
resizeAt = Math.max(1, (int) (capacity * LOAD_FACTOR));
}
void put(int x, int y, int z, MatterCavern value, boolean riverBoundary) {
void put(int x, int y, int z, MatterCavern value) {
int key = pack(x, y, z);
int index = mix(key) & mask;
@@ -169,7 +167,6 @@ final class CarveWallBuffer {
if (existingKey == EMPTY_KEY) {
keys[index] = key;
values[index] = value;
riverBoundaries[index] = riverBoundary;
size++;
if (size >= resizeAt) {
resize();
@@ -179,7 +176,6 @@ final class CarveWallBuffer {
if (existingKey == key) {
values[index] = value;
riverBoundaries[index] = riverBoundaries[index] || riverBoundary;
return;
}
@@ -202,21 +198,6 @@ final class CarveWallBuffer {
}
}
boolean isRiverBoundary(int x, int y, int z) {
int key = pack(x, y, z);
int index = mix(key) & mask;
while (true) {
int existingKey = keys[index];
if (existingKey == EMPTY_KEY) {
return false;
}
if (existingKey == key) {
return riverBoundaries[index];
}
index = (index + 1) & mask;
}
}
void forEach(Consumer consumer) {
for (int index = 0; index < keys.length; index++) {
int key = keys[index];
@@ -226,7 +207,7 @@ final class CarveWallBuffer {
MatterCavern cavern = values[index];
if (cavern != null) {
consumer.accept(unpackX(key), unpackY(key), unpackZ(key), cavern, riverBoundaries[index]);
consumer.accept(unpackX(key), unpackY(key), unpackZ(key), cavern);
}
}
}
@@ -234,19 +215,16 @@ final class CarveWallBuffer {
void clear() {
Arrays.fill(keys, EMPTY_KEY);
Arrays.fill(values, null);
Arrays.fill(riverBoundaries, false);
size = 0;
}
private void resize() {
int[] oldKeys = keys;
MatterCavern[] oldValues = values;
boolean[] oldRiverBoundaries = riverBoundaries;
int nextCapacity = oldKeys.length << 1;
keys = new int[nextCapacity];
Arrays.fill(keys, EMPTY_KEY);
values = new MatterCavern[nextCapacity];
riverBoundaries = new boolean[nextCapacity];
mask = nextCapacity - 1;
resizeAt = Math.max(1, (int) (nextCapacity * LOAD_FACTOR));
size = 0;
@@ -255,12 +233,12 @@ final class CarveWallBuffer {
int key = oldKeys[index];
MatterCavern value = oldValues[index];
if (key != EMPTY_KEY && value != null) {
reinsert(key, value, oldRiverBoundaries[index]);
reinsert(key, value);
}
}
}
private void reinsert(int key, MatterCavern value, boolean riverBoundary) {
private void reinsert(int key, MatterCavern value) {
int index = mix(key) & mask;
while (keys[index] != EMPTY_KEY) {
index = (index + 1) & mask;
@@ -268,7 +246,6 @@ final class CarveWallBuffer {
keys[index] = key;
values[index] = value;
riverBoundaries[index] = riverBoundary;
size++;
}
@@ -295,6 +272,6 @@ final class CarveWallBuffer {
@FunctionalInterface
interface Consumer {
void accept(int x, int y, int z, MatterCavern cavern, boolean riverBoundary);
void accept(int x, int y, int z, MatterCavern cavern);
}
}
@@ -57,9 +57,10 @@ public class IrisPostModifier extends EngineAssignedModifier<PlatformBlockState>
IrisDimension dimension = getDimension();
boolean walls = dimension.isPostProcessingWalls();
boolean slabs = dimension.isPostProcessingSlabs();
int fluidHeight = dimension.getFluidHeight();
for (int i = 0; i < width; i++) {
for (int j = 0; j < depth; j++) {
post(i, j, sync, i + x, j + z, context, heights, planeWidth, walls, slabs);
post(i, j, sync, i + x, j + z, context, heights, planeWidth, walls, slabs, fluidHeight);
}
}
@@ -89,7 +90,7 @@ public class IrisPostModifier extends EngineAssignedModifier<PlatformBlockState>
return heights;
}
private void post(int currentPostX, int currentPostZ, Hunk<PlatformBlockState> currentData, int x, int z, ChunkContext context, int[] heights, int planeWidth, boolean walls, boolean slabs) {
private void post(int currentPostX, int currentPostZ, Hunk<PlatformBlockState> currentData, int x, int z, ChunkContext context, int[] heights, int planeWidth, boolean walls, boolean slabs, int fluidHeight) {
// x/z are world coordinates, the hunk is indexed relative to this chunk origin.
int originX = x - currentPostX;
int originZ = z - currentPostZ;
@@ -99,7 +100,6 @@ public class IrisPostModifier extends EngineAssignedModifier<PlatformBlockState>
int hb = heights[center + planeWidth];
int hc = heights[center - 1];
int hd = heights[center - planeWidth];
int fluidHeight = (int) Math.round(getComplex().getRiverWaterSurfaceStream().get(x, z));
// Floating Nibs
int g = 0;
@@ -50,17 +50,6 @@ public interface IObjectPlacer {
int getFluidHeight();
default int getFluidHeight(int x, int z) {
Engine engine = getEngine();
if (engine == null || engine.getComplex() == null) {
return getFluidHeight();
}
int coordinateShift = getFluidHeight() - engine.getDimension().getFluidHeight();
return coordinateShift + (int) Math.round(
engine.getComplex().getRiverWaterSurfaceStream().get(x, z)
);
}
boolean isDebugSmartBore();
void setTile(int xx, int yy, int zz, TileData tile);
@@ -130,8 +130,6 @@ public class IrisBiome extends IrisRegistrant implements IRare {
private int lockLayersMax = 7;
@Desc("Profile-driven 3D cave configuration")
private IrisCaveProfile caveProfile = new IrisCaveProfile();
@Desc("Biome-level river routing, shape, cave-entry, and biome-pool overrides. Omit to inherit region and dimension settings.")
private IrisRiverOverride riverOverride = null;
@MinNumber(1)
@MaxNumber(512)
@Desc("The rarity of this biome (integer)")
@@ -36,7 +36,7 @@ final class IrisBiomeColorRenderer {
static Color getColor(IrisBiome biome, Engine engine, RenderType type) {
switch (type) {
case BIOME, HEIGHT, CAVE_LAND, REGION, BIOME_SEA, BIOME_LAND, RIVER -> {
case BIOME, HEIGHT, CAVE_LAND, REGION, BIOME_SEA, BIOME_LAND -> {
return biome.getCacheColor().aquire(() -> {
if (biome.getColor() == null) {
RandomColor randomColor = new RandomColor(biome.getName().hashCode());
@@ -49,6 +49,9 @@ import lombok.experimental.Accessors;
@Desc("Creates ore & other block deposits underground")
@Data
public class IrisDepositGenerator {
private static final long FNV_OFFSET_BASIS = 0xcbf29ce484222325L;
private static final long FNV_PRIME = 0x100000001b3L;
private final transient ConcurrentMap<ClumpCacheKey, KList<IrisObject>> objects = new ConcurrentHashMap<>();
private final transient AtomicCache<KList<PlatformBlockState>> blockData = new AtomicCache<>();
private final transient AtomicCache<Boolean> ore = new AtomicCache<>();
@@ -135,7 +138,7 @@ public class IrisDepositGenerator {
ClumpCacheKey cacheKey = new ClumpCacheKey(engine.getSeedManager().getDeposit(), minSize, maxSize);
KList<IrisObject> objects = this.objects.computeIfAbsent(cacheKey, key -> {
RNG rngv = new RNG(key.depositSeed() + hashCode());
RNG rngv = new RNG(key.depositSeed() + stableClumpSalt(rdata));
KList<IrisObject> objectsf = new KList<>();
for (int i = 0; i < varience; i++) {
@@ -163,7 +166,7 @@ public class IrisDepositGenerator {
engine.getSeedManager().getDeposit(), scaledMinSize, scaledMaxSize);
KList<IrisObject> objects = scaledObjects.computeIfAbsent(cacheKey, key -> {
long sizeSeed = ((long) key.minSize() << 32) ^ (key.maxSize() & 0xffffffffL);
RNG rngv = new RNG(key.depositSeed() + hashCode() + sizeSeed);
RNG rngv = new RNG(key.depositSeed() + stableClumpSalt(rdata) + sizeSeed);
KList<IrisObject> generated = new KList<>();
for (int i = 0; i < varience; i++) {
@@ -184,6 +187,64 @@ public class IrisDepositGenerator {
return Math.max(0, Math.min(8192, (int) Math.round(size * multiplier)));
}
long stableClumpSalt(IrisData rdata) {
long hash = FNV_OFFSET_BASIS;
hash = mix(hash, minHeight);
hash = mix(hash, maxHeight);
hash = mixEnum(hash, heightDistribution);
hash = mixEnum(hash, placementScope);
hash = mix(hash, surfaceClearance);
hash = mix(hash, minSize);
hash = mix(hash, maxSize);
hash = mixEnum(hash, shape);
hash = mix(hash, maxPerChunk);
hash = mix(hash, minPerChunk);
hash = mix(hash, Double.doubleToLongBits(spawnChance));
hash = mix(hash, Double.doubleToLongBits(perClumpSpawnChance));
hash = mix(hash, Double.doubleToLongBits(discardChanceOnAirExposure));
KList<PlatformBlockState> resolvedPalette = getBlockData(rdata);
hash = mix(hash, resolvedPalette.size());
for (PlatformBlockState block : resolvedPalette) {
hash = mixString(hash, block == null ? null : block.key());
}
hash = mix(hash, varience);
hash = mixStrings(hash, replaceableBlocks);
hash = mixEnum(hash, biomeScope);
hash = mixStrings(hash, includedBiomes);
hash = mixStrings(hash, excludedBiomes);
return mix(hash, replaceBedrock ? 1L : 0L);
}
private static long mix(long hash, long value) {
return (hash ^ value) * FNV_PRIME;
}
private static long mixEnum(long hash, Enum<?> value) {
return mixString(hash, value == null ? null : value.name());
}
private static long mixString(long hash, String value) {
if (value == null) {
return mix(hash, -1L);
}
long mixed = mix(hash, value.length());
for (int i = 0; i < value.length(); i++) {
mixed = mix(mixed, value.charAt(i));
}
return mixed;
}
private static long mixStrings(long hash, KList<String> values) {
if (values == null) {
return mix(hash, -1L);
}
long mixed = mix(hash, values.size());
for (String value : values) {
mixed = mixString(mixed, value);
}
return mixed;
}
private IrisObject generateConfiguredClumpObject(RNG rng, IrisData rdata, int clumpMinSize, int clumpMaxSize) {
int size = rng.i(clumpMinSize, clumpMaxSize + 1);
return switch (shape) {
@@ -165,8 +165,6 @@ public class IrisDimension extends IrisRegistrant {
private KList<IrisDimensionCarvingEntry> carving = new KList<>();
@Desc("Profile-driven 3D cave configuration")
private IrisCaveProfile caveProfile = new IrisCaveProfile();
@Desc("Connected surface rivers and contained river cave-water generation.")
private IrisRiverNetwork rivers = new IrisRiverNetwork();
@Desc("Refuse to place surface objects and trees over carved surface openings.")
private boolean requireObjectSurfaceSupport = true;
@MinNumber(0)
@@ -515,17 +513,11 @@ public class IrisDimension extends IrisRegistrant {
}
Deque<String> pending = new ArrayDeque<>();
IrisRiverNetwork riverNetwork = getRivers();
boolean riversEnabled = riverNetwork != null && riverNetwork.isEnabled();
if (riversEnabled && riverNetwork.getBiomes() != null) {
addReachableBiomeKeys(pending, riverNetwork.getBiomes().getAllBiomeIds());
}
for (IrisRegion region : getAllRegions(g)) {
if (region == null) {
continue;
}
addReachableBiomeKeys(pending,
riversEnabled ? region.getAllBiomeIds() : region.getNaturalBiomeIds());
addReachableBiomeKeys(pending, region.getAllBiomeIds());
}
for (IrisImageMapBinding binding : getImageMaps()) {
if (binding == null || binding.getApplication() != IrisImageMapApplication.BIOME) {
@@ -571,10 +563,6 @@ public class IrisDimension extends IrisRegistrant {
biomes.put(loadKey, biome);
addReachableBiomeKeys(pending, biome.getChildren());
addReachableBiomeKey(pending, biome.getCarvingBiome());
if (riversEnabled && biome.getRiverOverride() != null) {
addReachableBiomeKeys(pending, biome.getRiverOverride().getAllBiomeIds());
}
KList<IrisFloatingChildBiomes> floatingChildren = biome.getFloatingChildBiomes();
if (floatingChildren == null) {
continue;
@@ -28,9 +28,6 @@ public enum IrisEngineStreamType {
@Desc("Represents the given slope at the x, z coordinates")
SLOPE((f) -> f.getComplex().getSlopeStream()),
@Desc("Represents terrain height before river incision and river biome replacement.")
NATURAL_HEIGHT((f) -> f.getComplex().getNaturalHeightStream()),
@Desc("Represents the base generator height at the given position. This includes only the biome generators / interpolation and noise features but does not include carving, caves.")
HEIGHT((f) -> f.getComplex().getHeightStream()),
@@ -44,19 +41,7 @@ public enum IrisEngineStreamType {
REGION_STYLE((f) -> f.getComplex().getRegionStyleStream()),
@Desc("Represents the identity of regions. Each region has a unique number (very large numbers)")
REGION_IDENTITY((f) -> f.getComplex().getRegionIdentityStream()),
@Desc("Represents block distance from the nearest active river centerline.")
RIVER_DISTANCE((f) -> f.getComplex().getRiverDistanceStream()),
@Desc("Represents the merged upstream flow carried by the active river reach.")
RIVER_FLOW((f) -> f.getComplex().getRiverFlowStream()),
@Desc("Represents the normalized river terrain-incision weight.")
RIVER_CARVE_WEIGHT((f) -> f.getComplex().getRiverCarveWeightStream()),
@Desc("Represents the solved river water-surface height.")
RIVER_WATER_SURFACE((f) -> f.getComplex().getRiverWaterSurfaceStream());
REGION_IDENTITY((f) -> f.getComplex().getRegionIdentityStream());
private final Function<Engine, ProceduralStream<Double>> getter;
@@ -52,9 +52,7 @@ public class IrisInterpolator {
@Override
public int hashCode() {
// Bit-identical to Objects.hash(horizontalScale, function) without the Object[] + Double boxing.
// The exact value is load bearing: it decides HashMap bucket order for the generator maps in
// IrisComplex, and that order fixes the floating point summation order of interpolated heights.
// Bit-identical to Objects.hash(horizontalScale, function) without the Object[] or Double boxing.
int result = 31 + Double.hashCode(horizontalScale);
return (31 * result) + (function == null ? 0 : function.hashCode());
}
@@ -20,21 +20,27 @@ package art.arcane.iris.engine.object;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.data.cache.LazyBoundedCache;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.annotations.ArrayType;
import art.arcane.iris.engine.object.annotations.Desc;
import art.arcane.iris.engine.object.annotations.MinNumber;
import art.arcane.iris.engine.object.annotations.Required;
import art.arcane.iris.engine.object.annotations.Snippet;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.iris.util.project.noise.CNG;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import art.arcane.iris.spi.PlatformBlockState;
import lombok.Setter;
import lombok.experimental.Accessors;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
@Snippet("palette")
@Accessors(chain = true)
@@ -43,8 +49,17 @@ import java.util.Optional;
@Desc("A palette of materials")
@Data
public class IrisMaterialPalette {
private static final int LAYER_GENERATOR_CACHE_SIZE = 32;
private static final int LAYER_GENERATOR_SALT = -23_498_896;
private final transient AtomicCache<KList<PlatformBlockState>> blockData = new AtomicCache<>();
private final transient AtomicCache<CNG> layerGenerator = new AtomicCache<>();
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private final transient LazyBoundedCache<LayerGeneratorKey, CNG> layerGenerators =
new LazyBoundedCache<>(LAYER_GENERATOR_CACHE_SIZE);
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private final transient AtomicReference<CachedLayerGenerator> recentLayerGenerator = new AtomicReference<>();
private final transient AtomicCache<CNG> heightGenerator = new AtomicCache<>();
@Desc("The style of noise")
private IrisGeneratorStyle style = NoiseStyle.STATIC.style();
@@ -82,11 +97,21 @@ public class IrisMaterialPalette {
}
public CNG getLayerGenerator(RNG rng, IrisData rdata) {
return layerGenerator.aquire(() ->
{
RNG rngx = rng.nextParallelRNG(-23498896 + getBlockData(rdata).size());
return style.create(rngx, rdata);
});
Engine engine = rdata == null ? null : rdata.getEngine();
int generatorSignature = LAYER_GENERATOR_SALT + getBlockData(rdata).size();
long generatorSeed = rng.getSeed() + generatorSignature;
CachedLayerGenerator recent = recentLayerGenerator.get();
if (recent != null && recent.key.matches(rdata, engine, generatorSeed)) {
return recent.generator;
}
LayerGeneratorKey key = new LayerGeneratorKey(rdata, engine, generatorSeed);
CNG generator = layerGenerators.computeIfAbsent(key,
ignored -> style.create(new RNG(generatorSeed), rdata, engine));
if (generator != null) {
recentLayerGenerator.set(new CachedLayerGenerator(key, generator));
}
return generator;
}
public IrisMaterialPalette qclear() {
@@ -127,4 +152,49 @@ public class IrisMaterialPalette {
palette.clear();
return this;
}
private static final class LayerGeneratorKey {
private final IrisData data;
private final Engine engine;
private final long seed;
private LayerGeneratorKey(IrisData data, Engine engine, long seed) {
this.data = data;
this.engine = engine;
this.seed = seed;
}
private boolean matches(IrisData data, Engine engine, long seed) {
return this.data == data && this.engine == engine && this.seed == seed;
}
@Override
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (!(object instanceof LayerGeneratorKey other)) {
return false;
}
return data == other.data && engine == other.engine && seed == other.seed;
}
@Override
public int hashCode() {
int result = System.identityHashCode(data);
result = 31 * result + System.identityHashCode(engine);
result = 31 * result + Long.hashCode(seed);
return result;
}
}
private static final class CachedLayerGenerator {
private final LayerGeneratorKey key;
private final CNG generator;
private CachedLayerGenerator(LayerGeneratorKey key, CNG generator) {
this.key = key;
this.generator = generator;
}
}
}
@@ -19,7 +19,8 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.data.cache.LazyBoundedCache;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.annotations.ArrayType;
import art.arcane.iris.engine.object.annotations.Desc;
import art.arcane.iris.engine.object.annotations.MaxNumber;
@@ -30,11 +31,16 @@ import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.iris.util.project.interpolation.IrisInterpolation;
import art.arcane.iris.util.project.noise.CNG;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.Accessors;
import java.util.concurrent.atomic.AtomicReference;
@Snippet("generator")
@Accessors(chain = true)
@NoArgsConstructor
@@ -42,7 +48,16 @@ import lombok.experimental.Accessors;
@Desc("A noise generator")
@Data
public class IrisNoiseGenerator {
private final transient AtomicCache<CNG> generator = new AtomicCache<>();
private static final int GENERATOR_CACHE_SIZE = 32;
private static final long GENERATOR_SEED_SALT = 33_955_677L;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private final transient LazyBoundedCache<GeneratorKey, CNG> generators =
new LazyBoundedCache<>(GENERATOR_CACHE_SIZE);
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private final transient AtomicReference<CachedGenerator> recentGenerator = new AtomicReference<>();
@MinNumber(0.0001)
@Desc("The coordinate input zoom")
private double zoom = 1;
@@ -87,7 +102,20 @@ public class IrisNoiseGenerator {
}
protected CNG getGenerator(long superSeed, IrisData data) {
return generator.aquire(() -> style.create(new RNG(superSeed + 33955677 - seed), data).oct(octaves));
Engine engine = data == null ? null : data.getEngine();
long generatorSeed = superSeed + GENERATOR_SEED_SALT - seed;
CachedGenerator recent = recentGenerator.get();
if (recent != null && recent.key.matches(data, engine, generatorSeed)) {
return recent.generator;
}
GeneratorKey key = new GeneratorKey(data, engine, generatorSeed);
CNG generator = generators.computeIfAbsent(key,
ignored -> style.createNoCache(new RNG(generatorSeed), data).oct(octaves));
if (generator != null) {
recentGenerator.set(new CachedGenerator(key, generator));
}
return generator;
}
public double getMax() {
@@ -136,4 +164,49 @@ public class IrisNoiseGenerator {
return g;
}
private static final class GeneratorKey {
private final IrisData data;
private final Engine engine;
private final long seed;
private GeneratorKey(IrisData data, Engine engine, long seed) {
this.data = data;
this.engine = engine;
this.seed = seed;
}
private boolean matches(IrisData data, Engine engine, long seed) {
return this.data == data && this.engine == engine && this.seed == seed;
}
@Override
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (!(object instanceof GeneratorKey other)) {
return false;
}
return data == other.data && engine == other.engine && seed == other.seed;
}
@Override
public int hashCode() {
int result = System.identityHashCode(data);
result = 31 * result + System.identityHashCode(engine);
result = 31 * result + Long.hashCode(seed);
return result;
}
}
private static final class CachedGenerator {
private final GeneratorKey key;
private final CNG generator;
private CachedGenerator(GeneratorKey key, CNG generator) {
this.key = key;
this.generator = generator;
}
}
}
@@ -332,8 +332,7 @@ final class IrisObjectPlacementRunner {
return -1;
}
if (!config.isForcePlace() && !rawStructurePiece && config.isUnderwater()
&& y + rty + ty >= placer.getFluidHeight(x, z)) {
if (!config.isForcePlace() && !rawStructurePiece && config.isUnderwater() && y + rty + ty >= placer.getFluidHeight()) {
return -1;
}
@@ -118,8 +118,6 @@ public class IrisRegion extends IrisRegistrant implements IRare {
private double caveBiomeZoom = 1;
@Desc("Profile-driven 3D cave configuration")
private IrisCaveProfile caveProfile = new IrisCaveProfile();
@Desc("Region-level river routing, shape, cave-entry, and biome-pool overrides. Omit to inherit dimension settings.")
private IrisRiverOverride riverOverride = null;
@RegistryListResource(IrisBiome.class)
@Required
@ArrayType(min = 1, type = String.class)
@@ -279,33 +277,18 @@ public class IrisRegion extends IrisRegistrant implements IRare {
}
public KSet<String> getAllBiomeIds() {
KSet<String> names = getNaturalBiomeIds();
if (riverOverride != null) {
names.addAll(riverOverride.getAllBiomeIds());
}
return names;
}
public KSet<String> getNaturalBiomeIds() {
KSet<String> names = new KSet<>();
names.addAll(landBiomes);
names.addAll(caveBiomes);
names.addAll(seaBiomes);
names.addAll(shoreBiomes);
return names;
}
public KList<IrisBiome> getAllBiomes(DataProvider g) {
return resolveBiomes(g, getAllBiomeIds());
}
public KList<IrisBiome> getNaturalBiomes(DataProvider g) {
return resolveBiomes(g, getNaturalBiomeIds());
}
private KList<IrisBiome> resolveBiomes(DataProvider g, KSet<String> biomeIds) {
KMap<String, IrisBiome> b = new KMap<>();
KSet<String> names = biomeIds.copy();
KSet<String> names = getAllBiomeIds();
while (!names.isEmpty()) {
for (String i : new KList<>(names)) {
@@ -1,61 +0,0 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.object.annotations.ArrayType;
import art.arcane.iris.engine.object.annotations.Desc;
import art.arcane.iris.engine.object.annotations.RegistryListResource;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KSet;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Accessors(chain = true)
@NoArgsConstructor
@Desc("Biome pools used by dimension-level river sections and contained river caves.")
@Data
public class IrisRiverBiomes {
@Desc("Noise used to select a biome inside the active river-section pool.")
private IrisGeneratorStyle selectionStyle = new IrisGeneratorStyle(NoiseStyle.CELLULAR_IRIS_DOUBLE)
.zoomed(512D);
@RegistryListResource(IrisBiome.class)
@ArrayType(type = String.class)
@Desc("Biome pool for wet river channels.")
private KList<String> channel = new KList<>();
@RegistryListResource(IrisBiome.class)
@ArrayType(type = String.class)
@Desc("Biome pool for river banks outside the wet channel.")
private KList<String> bank = new KList<>();
@RegistryListResource(IrisBiome.class)
@ArrayType(type = String.class)
@Desc("Biome pool for river reaches meeting natural sea.")
private KList<String> mouth = new KList<>();
@RegistryListResource(IrisBiome.class)
@ArrayType(type = String.class)
@Desc("Biome pool for dry river channels and terminal tapers.")
private KList<String> dry = new KList<>();
@RegistryListResource(IrisBiome.class)
@ArrayType(type = String.class)
@Desc("Cave biome pool for accepted contained river cave bodies.")
private KList<String> floodedCave = new KList<>();
public KSet<String> getAllBiomeIds() {
KSet<String> biomeIds = new KSet<>();
addAll(biomeIds, channel);
addAll(biomeIds, bank);
addAll(biomeIds, mouth);
addAll(biomeIds, dry);
addAll(biomeIds, floodedCave);
return biomeIds;
}
private static void addAll(KSet<String> destination, KList<String> source) {
if (source != null) {
destination.addAll(source);
}
}
}
@@ -1,12 +0,0 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.object.annotations.Desc;
@Desc("Selects the fallback when a requested river cave connection cannot be proven safe.")
public enum IrisRiverCaveFallback {
@Desc("Keep the rejected connection sealed.")
SEALED,
@Desc("Try a bounded generated grotto instead of the rejected existing cave.")
GENERATE_GROTTO
}
@@ -1,21 +0,0 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.object.annotations.Desc;
@Desc("Selects the contained cave-water behavior available to river entry events.")
public enum IrisRiverCaveMode {
@Desc("Keep the surface reservoir sealed from caves.")
SEALED,
@Desc("Flood only an existing cave component whose complete fluid-reachable boundary is proven closed.")
FLOOD_CLOSED_COMPONENT,
@Desc("Generate a bounded grotto with a guaranteed solid shell.")
GENERATE_GROTTO,
@Desc("Use a proven closed cave component when available, otherwise generate a bounded grotto.")
GROTTO_OR_CLOSED_COMPONENT,
@Desc("Generate a controlled falling column into a proven contained pool.")
WATERFALL_POOL
}
@@ -1,103 +0,0 @@
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 bounded and transactionally validated river-to-cave connections.")
@Data
public class IrisRiverCaves {
@Desc("The contained cave-water behavior available to river entry events.")
private IrisRiverCaveMode mode = IrisRiverCaveMode.SEALED;
@Desc("Selects cave-entry stations at stable river-reach anchors.")
private IrisRiverNoiseChance entry = new IrisRiverNoiseChance()
.setChance(0.12D)
.setStyle(new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(1024D))
.setInfluence(0.4D);
@MinNumber(16)
@MaxNumber(4096)
@Desc("The minimum distance in blocks between cave-entry candidates.")
private int minimumSpacing = 128;
@MinNumber(0)
@MaxNumber(16)
@Desc("The maximum entry-noise-eligible cave anchors accepted on one ordinary reach. A forced sinkhole terminal uses its reach exclusively and requires this value above zero.")
private int maximumPerReach = 1;
@MinNumber(1)
@MaxNumber(256)
@Desc("The maximum vertical distance searched while boring from river bed to cave.")
private int maxBoreDepth = 48;
@MinNumber(1)
@MaxNumber(16)
@Desc("The radius in blocks of a generated river-to-cave throat.")
private int throatRadius = 2;
@MinNumber(-64)
@MaxNumber(64)
@Desc("The offset applied to river water height when filling an accepted cave body.")
private int waterLevelOffset = 0;
@MinNumber(0)
@MaxNumber(64)
@Desc("The minimum dry headroom retained above water in generated grottos.")
private int dryHeadroom = 4;
@MinNumber(2)
@MaxNumber(128)
@Desc("The horizontal radius in blocks of a generated sealed grotto.")
private int grottoHorizontalRadius = 12;
@MinNumber(2)
@MaxNumber(128)
@Desc("The vertical radius in blocks of a generated sealed grotto.")
private int grottoVerticalRadius = 7;
@Desc("Noise shaping the boundary of generated sealed grottos.")
private IrisGeneratorStyle grottoShapeStyle = new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(24D);
@Desc("Noise warping the coordinate field used for generated sealed grottos.")
private IrisGeneratorStyle grottoWarpStyle = new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(48D);
@MinNumber(0)
@MaxNumber(32)
@Desc("The maximum coordinate warp applied to generated sealed grottos in blocks.")
private double grottoWarpStrength = 2D;
@MinNumber(0)
@MaxNumber(1)
@Desc("The fraction of river-cave boundary columns that inherit the naturally resolved parent cave or surface biome instead of a flooded-cave override.")
private double parentBiomeInheritance = 0.5D;
@MinNumber(4)
@MaxNumber(256)
@Desc("The horizontal proof radius for an existing closed cave component.")
private int maxFloodRadius = 48;
@MinNumber(4)
@MaxNumber(256)
@Desc("The vertical proof depth for an existing closed cave component.")
private int maxFloodDepth = 32;
@MinNumber(64)
@MaxNumber(1048576)
@Desc("The greatest cave-component volume that may be fully proven and flooded.")
private int maxFloodVolume = 8192;
@Desc("The behavior used when a requested cave connection cannot be proven safe.")
private IrisRiverCaveFallback fallback = IrisRiverCaveFallback.SEALED;
@Desc("The policy for fluid already present in a candidate contained cave body.")
private IrisRiverExistingFluidPolicy existingFluidPolicy = IrisRiverExistingFluidPolicy.REJECT;
@Desc("Sparse, independently filled cave-floor pools generated at deep river-network anchors.")
private IrisRiverDeepPools deepPools = new IrisRiverDeepPools();
}
@@ -1,92 +0,0 @@
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 sparse, river-anchored fluid pools attached to deep cave floors.")
@Data
public class IrisRiverDeepPools {
@Desc("Enables independently configured deep cave pools along eligible wet river reaches.")
private boolean enabled = false;
@Desc("Selects complete wet river reaches that may host deep pools.")
private IrisRiverNoiseChance reach = new IrisRiverNoiseChance()
.setChance(1D / 3D)
.setStyle(new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(4096D))
.setInfluence(0.08D);
@MinNumber(16)
@MaxNumber(4096)
@Desc("The minimum distance in blocks between deep-pool candidates.")
private int minimumSpacing = 768;
@MinNumber(0)
@MaxNumber(16)
@Desc("The maximum accepted deep pools on one river reach.")
private int maximumPerReach = 1;
@MinNumber(-2048)
@MaxNumber(2048)
@Desc("The lowest absolute world Y considered for the pool fluid surface.")
private int minimumFluidY = -224;
@MinNumber(-2048)
@MaxNumber(2048)
@Desc("The highest absolute world Y considered for the pool fluid surface.")
private int maximumFluidY = -104;
@MinNumber(0)
@MaxNumber(256)
@Desc("The horizontal distance searched from a river anchor for a contained cave floor.")
private int searchRadius = 16;
@MinNumber(1)
@MaxNumber(64)
@Desc("The number of deterministic nearby columns tested for a contained cave floor.")
private int searchAttempts = 12;
@MinNumber(2)
@MaxNumber(128)
@Desc("The horizontal radius of the generated deep-pool chamber.")
private int horizontalRadius = 18;
@MinNumber(2)
@MaxNumber(64)
@Desc("The vertical radius of the generated deep-pool chamber.")
private int verticalRadius = 8;
@MinNumber(1)
@MaxNumber(63)
@Desc("The dry chamber height retained above the deep-pool fluid surface.")
private int dryHeadroom = 4;
@Desc("Noise shaping the deep-pool chamber boundary.")
private IrisGeneratorStyle shapeStyle = new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(12D);
@MinNumber(0)
@MaxNumber(0.75)
@Desc("The proportional noise displacement applied to the deep-pool chamber boundary.")
private double shapeVariation = 0.5D;
@Desc("Noise warping the deep-pool chamber coordinate field.")
private IrisGeneratorStyle warpStyle = new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(24D);
@MinNumber(0)
@MaxNumber(64)
@Desc("The maximum coordinate warp applied to the deep-pool chamber in blocks.")
private double warpStrength = 6D;
@MinNumber(64)
@MaxNumber(1048576)
@Desc("The greatest generated deep-pool chamber volume that may be transactionally published.")
private int maximumVolume = 32768;
@Desc("The fluid palette used only by accepted deep pools.")
private IrisMaterialPalette fluidPalette = new IrisMaterialPalette().qclear().qadd("lava");
}
@@ -1,15 +0,0 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.object.annotations.Desc;
@Desc("Controls how river cave hydrology treats fluid already present in a candidate cave body.")
public enum IrisRiverExistingFluidPolicy {
@Desc("Reject a candidate containing any existing fluid.")
REJECT,
@Desc("Accept only fluid compatible with the river fluid palette.")
ALLOW_SAME,
@Desc("Replace contained existing fluid with the river fluid palette.")
REPLACE
}
@@ -1,30 +0,0 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.object.annotations.Desc;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Accessors(chain = true)
@NoArgsConstructor
@Desc("Dimension-owned configuration for connected surface rivers and contained river cave water.")
@Data
public class IrisRiverNetwork {
@Desc("Enable the river network for this dimension.")
private boolean enabled = false;
@Desc("Dimension-owned connected graph and source-selection settings.")
private IrisRiverTopology topology = new IrisRiverTopology();
@Desc("Channel geometry, terrain incision, meanders, and terminal behavior.")
private IrisRiverTerrain terrain = new IrisRiverTerrain();
@Desc("River water-surface settings.")
private IrisRiverWater water = new IrisRiverWater();
@Desc("Dimension-level biome pools for river sections and contained river caves.")
private IrisRiverBiomes biomes = new IrisRiverBiomes();
@Desc("Bounded river-to-cave connection settings.")
private IrisRiverCaves caves = new IrisRiverCaves();
}
@@ -1,27 +0,0 @@
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("A deterministic graph-event chance modulated by configurable noise.")
@Data
public class IrisRiverNoiseChance {
@MinNumber(0)
@MaxNumber(1)
@Desc("The base probability before noise modulation.")
private double chance = 1D;
@Desc("The noise sampled once at the stable graph-event anchor.")
private IrisGeneratorStyle style = new IrisGeneratorStyle(NoiseStyle.FLAT);
@MinNumber(0)
@MaxNumber(1)
@Desc("The maximum centered noise contribution added to the base probability.")
private double influence = 0D;
}
@@ -1,103 +0,0 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.object.annotations.ArrayType;
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 art.arcane.iris.engine.object.annotations.RegistryListResource;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KSet;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Accessors(chain = true)
@NoArgsConstructor
@Desc("Nullable river settings overridden by a region or natural biome without changing graph identity.")
@Data
public class IrisRiverOverride {
@Desc("Whether new river sources may begin in this area. Existing trunks are unaffected.")
private Boolean allowSources = null;
@Desc("How downstream routing treats this area.")
private IrisRiverRoutingPolicy routingPolicy = null;
@MinNumber(0)
@MaxNumber(64)
@Desc("Multiplier applied to downstream routing cost.")
private Double routingCostMultiplier = null;
@MinNumber(0.0001)
@MaxNumber(16)
@Desc("Multiplier applied to wet channel width.")
private Double widthMultiplier = null;
@MinNumber(0)
@MaxNumber(16)
@Desc("Multiplier applied to river bank width.")
private Double bankWidthMultiplier = null;
@MinNumber(0.0001)
@MaxNumber(16)
@Desc("Multiplier applied to river-bed depth.")
private Double depthMultiplier = null;
@MinNumber(0)
@MaxNumber(16)
@Desc("Multiplier applied to maximum terrain incision.")
private Double maxIncisionMultiplier = null;
@MinNumber(0)
@MaxNumber(16)
@Desc("Multiplier applied to reach continuation probability.")
private Double continuationChanceMultiplier = null;
@MinNumber(0)
@MaxNumber(16)
@Desc("Multiplier applied to cave-entry probability.")
private Double caveEntryMultiplier = null;
@Desc("Optional terminal behavior override for failed routes in this area.")
private IrisRiverTerminalMode terminalMode = null;
@RegistryListResource(IrisBiome.class)
@ArrayType(type = String.class)
@Desc("Optional replacement biome pool for wet river channels. Empty explicitly disables this pool.")
private KList<String> channelBiomes = null;
@RegistryListResource(IrisBiome.class)
@ArrayType(type = String.class)
@Desc("Optional replacement biome pool for river banks. Empty explicitly disables this pool.")
private KList<String> bankBiomes = null;
@RegistryListResource(IrisBiome.class)
@ArrayType(type = String.class)
@Desc("Optional replacement biome pool for river mouths. Empty explicitly disables this pool.")
private KList<String> mouthBiomes = null;
@RegistryListResource(IrisBiome.class)
@ArrayType(type = String.class)
@Desc("Optional replacement biome pool for dry river channels. Empty explicitly disables this pool.")
private KList<String> dryBiomes = null;
@RegistryListResource(IrisBiome.class)
@ArrayType(type = String.class)
@Desc("Optional replacement cave biome pool for accepted contained river cave bodies. Empty explicitly disables this pool.")
private KList<String> floodedCaveBiomes = null;
public KSet<String> getAllBiomeIds() {
KSet<String> biomeIds = new KSet<>();
addAll(biomeIds, channelBiomes);
addAll(biomeIds, bankBiomes);
addAll(biomeIds, mouthBiomes);
addAll(biomeIds, dryBiomes);
addAll(biomeIds, floodedCaveBiomes);
return biomeIds;
}
private static void addAll(KSet<String> destination, KList<String> source) {
if (source != null) {
destination.addAll(source);
}
}
}
@@ -1,15 +0,0 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.object.annotations.Desc;
@Desc("Controls how river routing treats a region or biome.")
public enum IrisRiverRoutingPolicy {
@Desc("Allow normal river routing through this area.")
ALLOW,
@Desc("Increase the routing cost while still permitting established river trunks.")
AVOID,
@Desc("Forbid river reaches from crossing this area.")
BLOCK
}
@@ -1,15 +0,0 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.object.annotations.Desc;
@Desc("Selects how a river route ends when it cannot continue to an outlet.")
public enum IrisRiverTerminalMode {
@Desc("Suppress the failed route instead of generating it.")
SUPPRESS,
@Desc("Continue as a dry channel that tapers back into natural terrain.")
DRY_CHANNEL,
@Desc("End in a contained underground grotto when cave hydrology accepts the connection.")
SINKHOLE_GROTTO
}
@@ -1,123 +0,0 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.object.annotations.ArrayType;
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 art.arcane.iris.engine.object.annotations.Required;
import art.arcane.volmlib.util.collection.KList;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Accessors(chain = true)
@NoArgsConstructor
@Desc("Controls river channel geometry, banks, incision, meanders, and terminal tapering.")
@Data
public class IrisRiverTerrain {
@Desc("The wet channel width in blocks before stream-order scaling.")
private IrisStyledRange channelWidth = range(8D, 20D, NoiseStyle.IRIS, 1024D);
@Desc("The bank width outside the wet channel in blocks.")
private IrisStyledRange bankWidth = range(5D, 18D, NoiseStyle.IRIS, 1024D);
@Desc("The wet-bed depth below the local water surface, or dry-channel depth below natural terrain, in blocks.")
private IrisStyledRange depth = range(2D, 7D, NoiseStyle.IRIS, 768D);
@MinNumber(0)
@MaxNumber(64)
@Desc("The radius added to every channel after worm, regional, local, and stream-order shaping.")
private double channelRadiusBonus = 0D;
@MinNumber(1)
@MaxNumber(2048)
@Desc("The final wet-channel width cap after region, biome, and stream-order scaling.")
private double maxChannelWidth = 10D;
@MinNumber(0)
@MaxNumber(2048)
@Desc("The final bank-width cap on each side after region and biome scaling.")
private double maxBankWidth = 4D;
@MinNumber(1)
@MaxNumber(512)
@Desc("The final river-depth cap after region, biome, and stream-order scaling.")
private double maxDepth = 10D;
@MinNumber(0)
@MaxNumber(8)
@Desc("Additional channel-width fraction applied for each merged upstream flow order.")
private double orderWidthFactor = 0.35D;
@MinNumber(0)
@MaxNumber(8)
@Desc("Additional river-bed depth fraction applied for each merged upstream flow order.")
private double orderDepthFactor = 0.2D;
@Desc("Selects whether a complete graph reach may incise terrain. A rejected reach follows terminal behavior.")
private IrisRiverNoiseChance incision = new IrisRiverNoiseChance();
@MinNumber(0)
@MaxNumber(512)
@Desc("The greatest permitted vertical incision below natural terrain.")
private int maxIncision = 48;
@MinNumber(0.125)
@MaxNumber(16)
@Desc("The exponent shaping the channel-to-bank cross-section transition.")
private double bankExponent = 2D;
@MinNumber(0)
@MaxNumber(16)
@Desc("The longitudinal transition length and maximum lateral and roof flare where a surface river enters or exits solid terrain.")
private double tunnelMouthBlend = 2D;
@Desc("Noise modulating the submerged floor of river tunnels.")
private IrisGeneratorStyle tunnelFloorStyle = new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(48D);
@Desc("The subterranean tunnel width multiplier relative to the surface river width.")
private IrisStyledRange tunnelWidthMultiplier = range(1D, 1D, NoiseStyle.FLAT, 1D);
@MinNumber(0)
@MaxNumber(8)
@Desc("The maximum vertical floor variation in river tunnels.")
private double tunnelFloorVariation = 2D;
@Desc("Noise modulating the dry roof of river tunnels.")
private IrisGeneratorStyle tunnelRoofStyle = new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(64D);
@MinNumber(0)
@MaxNumber(16)
@Desc("The maximum vertical roof variation in river tunnels.")
private double tunnelRoofVariation = 3D;
@Required
@ArrayType(min = 1, type = IrisRiverWorm.class)
@Desc("Weighted root Perlin-worm families with inherited child styles for trunks and tributaries.")
private KList<IrisRiverWorm> worms = new KList<IrisRiverWorm>();
@Desc("Modulates small river-bed height variation after the connected channel shape is solved.")
private IrisGeneratorStyle bedRoughnessStyle = new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(96D);
@MinNumber(0)
@MaxNumber(8)
@Desc("The maximum river-bed roughness in blocks.")
private double bedRoughness = 0.75D;
@Desc("The behavior used when a graph route cannot continue as a wet channel.")
private IrisRiverTerminalMode terminalMode = IrisRiverTerminalMode.DRY_CHANNEL;
@MinNumber(8)
@MaxNumber(1024)
@Desc("The distance in blocks over which a terminal channel returns to natural terrain.")
private int terminalTaper = 64;
@MinNumber(0)
@MaxNumber(1)
@Desc("The probability that a failed wet route continues as a tapered dry channel.")
private double dryContinuationChance = 1D;
private static IrisStyledRange range(double min, double max, NoiseStyle style, double zoom) {
return new IrisStyledRange(min, max, new IrisGeneratorStyle(style).zoomed(zoom));
}
}
@@ -1,112 +0,0 @@
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("Dimension-owned settings for the deterministic connected river graph.")
@Data
public class IrisRiverTopology {
@MinNumber(64)
@MaxNumber(4096)
@Desc("The routing-cell width in blocks. This controls graph identity and cannot be overridden by regions or biomes.")
private int cellSize = 512;
@MinNumber(1)
@MaxNumber(64)
@Desc("The number of routing cells grouped into one immutable river cache tile.")
private int tileCells = 4;
@MinNumber(0)
@MaxNumber(0.49)
@Desc("The fraction of a routing cell used to jitter its graph node away from the center.")
private double siteJitter = 0.35D;
@MinNumber(1)
@MaxNumber(256)
@Desc("The maximum number of directed graph reaches followed by one source route.")
private int maxRouteReaches = 16;
@MinNumber(0)
@MaxNumber(64)
@Desc("The minimum number of noise-weighted source nodes selected in each routing tile while source chance is above zero.")
private int minimumSourcesPerTile = 0;
@MinNumber(0)
@MaxNumber(7)
@Desc("The number of alternate downstream reaches inspected before declaring a sink.")
private int sinkSearchReaches = 4;
@MinNumber(8)
@MaxNumber(256)
@Desc("The spacing of deterministic drainage-basin sinks in routing cells. Larger values produce longer trunks and wider tributary trees.")
private int routingBasinCells = 64;
@MinNumber(8)
@MaxNumber(256)
@Desc("The wavelength in routing cells of the smooth domain warp applied to drainage distance.")
private int routingDeviationScaleCells = 24;
@MinNumber(0)
@MaxNumber(32)
@Desc("The maximum drainage-domain displacement in routing cells. Zero keeps straight radial basin gradients.")
private double routingDeviationStrengthCells = 0D;
@MinNumber(1)
@MaxNumber(64)
@Desc("The horizontal basin-distance span in routing cells per one block of terraced water rise.")
private double routingPlateauHeight = 8D;
@Desc("Selects complete river source routes at stable graph nodes.")
private IrisRiverNoiseChance source = new IrisRiverNoiseChance()
.setChance(0.05D)
.setStyle(new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(8192D))
.setInfluence(0.035D);
@Desc("Selects complete continuation reaches. A rejected reach terminates or suppresses its route rather than creating a gap.")
private IrisRiverNoiseChance continuation = new IrisRiverNoiseChance()
.setChance(0.99D)
.setStyle(new IrisGeneratorStyle(NoiseStyle.VASCULAR).zoomed(4096D))
.setInfluence(0.01D);
@Desc("Adds deterministic cost variation while choosing downstream graph neighbors.")
private IrisGeneratorStyle routingStyle = new IrisGeneratorStyle(NoiseStyle.VASCULAR).zoomed(8192D);
@MinNumber(0)
@MaxNumber(1024)
@Desc("The maximum routing-cost contribution from routingStyle.")
private double routingNoiseWeight = 24D;
@MinNumber(0)
@MaxNumber(1024)
@Desc("The penalty for choosing a downstream edge that does not follow the local routingStyle tangent.")
private double flowAlignmentWeight = 24D;
@MinNumber(0)
@MaxNumber(1024)
@Desc("The deterministic attraction toward shared downstream nodes. Larger values form stronger tributary trees and confluences.")
private double confluenceWeight = 0D;
@MinNumber(0)
@MaxNumber(16)
@Desc("The contribution of natural terrain height to downstream routing cost.")
private double terrainHeightWeight = 0.7D;
@MinNumber(0)
@MaxNumber(16)
@Desc("The contribution of natural terrain slope to downstream routing cost.")
private double terrainSlopeWeight = 0.35D;
@MinNumber(0)
@MaxNumber(16)
@Desc("The routing preference toward natural sea outlets.")
private double oceanAttraction = 1D;
@Desc("Require every wet source route to reach natural sea or a proven sea-reaching trunk.")
private boolean requireOcean = false;
}
@@ -1,40 +0,0 @@
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 river water-surface solver.")
@Data
public class IrisRiverWater {
@Desc("The strategy used to determine river water-surface height.")
private IrisRiverWaterMode mode = IrisRiverWaterMode.FIXED;
@MinNumber(-2048)
@MaxNumber(2048)
@Desc("The base river fluid surface in absolute world Y, independent of the dimension ocean height.")
private int fluidHeight = 63;
@Desc("The river fluid palette used by surface channels, contained tunnels, grottos, and waterfall throats.")
private IrisMaterialPalette fluidPalette = new IrisMaterialPalette().qclear().qadd("water");
@MinNumber(8)
@MaxNumber(4096)
@Desc("The target length of each flat terraced pool in blocks.")
private int poolLength = 96;
@MinNumber(0)
@MaxNumber(64)
@Desc("The greatest terraced river height permitted above fluidHeight.")
private int maximumPoolRise = 4;
@MinNumber(1)
@MaxNumber(32)
@Desc("The vertical height of controlled drops between terraced pools.")
private int dropHeight = 1;
}
@@ -1,12 +0,0 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.object.annotations.Desc;
@Desc("Selects how a river determines its surface fluid height.")
public enum IrisRiverWaterMode {
@Desc("Use the river water configuration's fixed fluid height for every wet reach.")
FIXED,
@Desc("Use flat pools connected by controlled vertical drops.")
TERRACED
}
@@ -1,138 +0,0 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.object.annotations.ArrayType;
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 art.arcane.iris.engine.object.annotations.Required;
import art.arcane.volmlib.util.collection.KList;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Accessors(chain = true)
@NoArgsConstructor
@Desc("One weighted Perlin-worm river shape and its channel proportions.")
@Data
public class IrisRiverWorm {
@Required
@Desc("Unique lowercase identifier for this root or child style.")
private String id = "river";
@Desc("Stable salt for this Perlin field pair.")
private long seed = 1L;
@MinNumber(0.000001)
@MaxNumber(1000000)
@Desc("Relative probability when selecting this root family or one child transition.")
private double weight = 1D;
@MinNumber(8)
@MaxNumber(16384)
@Desc("Primary gradient-Perlin wavelength in blocks.")
private double wavelength = 1024D;
@MinNumber(8)
@MaxNumber(16384)
@Desc("Secondary gradient-Perlin wavelength in blocks.")
private double detailWavelength = 256D;
@MinNumber(0)
@MaxNumber(1)
@Desc("Primary heading deviation as a fraction of 180 degrees.")
private double tortuosity = 0.5D;
@MinNumber(0)
@MaxNumber(1)
@Desc("Secondary heading deviation as a fraction of 180 degrees.")
private double detailTortuosity = 0.15D;
@MinNumber(0)
@MaxNumber(1024)
@Desc("Maximum endpoint-bridged displacement from the reach chord in blocks.")
private double maxOffset = 320D;
@MinNumber(1)
@MaxNumber(64)
@Desc("Number of deterministic Perlin-worm steps used to resolve the reach.")
private int segments = 48;
@MinNumber(0.125)
@MaxNumber(8)
@Desc("Channel-width multiplier for reaches selecting this worm.")
private double widthMultiplier = 1D;
@MinNumber(0.125)
@MaxNumber(8)
@Desc("Bank-width multiplier for reaches selecting this worm.")
private double bankMultiplier = 1D;
@MinNumber(0.125)
@MaxNumber(8)
@Desc("Depth multiplier for reaches selecting this worm.")
private double depthMultiplier = 1D;
@MinNumber(8)
@MaxNumber(16384)
@Desc("Primary world-space wavelength controlling longitudinal body swelling and pinching.")
private double bodyWavelength = 512D;
@MinNumber(8)
@MaxNumber(16384)
@Desc("Detail wavelength adding smaller changes to the longitudinal body profile.")
private double bodyDetailWavelength = 128D;
@MinNumber(0)
@MaxNumber(1)
@Desc("Share of the longitudinal body field supplied by bodyDetailWavelength; the remainder uses bodyWavelength.")
private double bodyDetailInfluence = 0.3D;
@MinNumber(0)
@MaxNumber(0.875)
@Desc("Maximum proportional channel-width variation along this style's body.")
private double widthVariation = 0D;
@MinNumber(0)
@MaxNumber(0.875)
@Desc("Maximum proportional bank or basin-width variation along this style's body.")
private double bankVariation = 0D;
@MinNumber(0)
@MaxNumber(0.875)
@Desc("Maximum proportional bed-depth variation along this style's body.")
private double depthVariation = 0D;
@MinNumber(0)
@MaxNumber(0.875)
@Desc("Maximum downward variation of tunnel roof clearance without exceeding the authored cave headroom.")
private double roofVariation = 0D;
@MinNumber(1)
@MaxNumber(8)
@Desc("Number of upstream children admitted before additional siblings decay probabilistically.")
private int branchCap = 4;
@MinNumber(0)
@MaxNumber(1)
@Desc("Multiplicative survival probability for every sibling beyond branchCap.")
private double branchDecay = 0.35D;
@MinNumber(0)
@MaxNumber(8)
@Desc("Multiplier applied to the dimension confluence attraction for this style.")
private double confluenceMultiplier = 1D;
@MinNumber(0)
@MaxNumber(1)
@Desc("Chance that an upstream continuation mutates from this style into one weighted child.")
private double childChance = 0D;
@MinNumber(0)
@MaxNumber(1)
@Desc("Additional child-mutation chance for each sibling slot beyond the primary branch.")
private double branchChildChance = 0D;
@ArrayType(type = IrisRiverWorm.class)
@Desc("Weighted descendant styles inherited by the complete upstream lineage after mutation.")
private KList<IrisRiverWorm> children = new KList<IrisRiverWorm>();
}
@@ -311,14 +311,6 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
}
private int resolveInitialSpawnY(World world, Location initialSpawn, int minY, int maxY) {
Engine activeEngine = engine;
if (activeEngine != null && activeEngine.getComplex() != null && activeEngine.getComplex().getHeightStream() != null) {
int generatedY = activeEngine.getMinHeight()
+ activeEngine.getComplex().getHeightStream().get(initialSpawn.getX(), initialSpawn.getZ()).intValue()
+ 1;
return Math.max(minY, Math.min(maxY, generatedY));
}
return Math.max(minY, Math.min(maxY, world.getHighestBlockYAt(initialSpawn) + 1));
}
@@ -1,27 +0,0 @@
package art.arcane.iris.engine.river;
import java.util.Objects;
public record RiverAnchor(
RiverEdgeId reachId,
int index,
long stableId,
double samplingSpacing,
long samplingSalt,
double x,
double z,
double alongReach,
RiverRouteState state,
int flow,
int order
) {
public RiverAnchor {
Objects.requireNonNull(reachId);
Objects.requireNonNull(state);
if (index < 0 || !Double.isFinite(samplingSpacing) || samplingSpacing <= 0D
|| !Double.isFinite(x) || !Double.isFinite(z)
|| !Double.isFinite(alongReach) || alongReach < 0.0 || alongReach > 1.0) {
throw new IllegalArgumentException("River anchor index and coordinates must be valid");
}
}
}
@@ -1,191 +0,0 @@
package art.arcane.iris.engine.river;
import java.util.Arrays;
public final class RiverBodyProfile {
private final double[] positions;
private final double[] widths;
private final double[] bankWidths;
private final double[] depths;
private final double[] roofScales;
private final double maximumWidth;
private final double maximumBankWidth;
private final double maximumDepth;
public RiverBodyProfile(
double[] positions,
double[] widths,
double[] bankWidths,
double[] depths,
double[] roofScales
) {
if (positions == null || widths == null || bankWidths == null || depths == null || roofScales == null
|| positions.length < 2
|| positions.length != widths.length
|| positions.length != bankWidths.length
|| positions.length != depths.length
|| positions.length != roofScales.length) {
throw new IllegalArgumentException("River body profiles require matching dimension samples");
}
this.positions = positions.clone();
this.widths = widths.clone();
this.bankWidths = bankWidths.clone();
this.depths = depths.clone();
this.roofScales = roofScales.clone();
double resolvedMaximumWidth = 0D;
double resolvedMaximumBankWidth = 0D;
double resolvedMaximumDepth = 0D;
for (int index = 0; index < this.positions.length; index++) {
double position = this.positions[index];
if (!Double.isFinite(position) || position < 0D || position > 1D
|| index > 0 && position <= this.positions[index - 1]) {
throw new IllegalArgumentException("River body profile positions must increase from zero to one");
}
requirePositive(this.widths[index], "width");
requireNonNegative(this.bankWidths[index], "bank width");
requirePositive(this.depths[index], "depth");
requireUnitScale(this.roofScales[index], "roof scale");
resolvedMaximumWidth = StrictMath.max(resolvedMaximumWidth, this.widths[index]);
resolvedMaximumBankWidth = StrictMath.max(resolvedMaximumBankWidth, this.bankWidths[index]);
resolvedMaximumDepth = StrictMath.max(resolvedMaximumDepth, this.depths[index]);
}
if (this.positions[0] != 0D || this.positions[this.positions.length - 1] != 1D) {
throw new IllegalArgumentException("River body profile positions must include zero and one");
}
maximumWidth = resolvedMaximumWidth;
maximumBankWidth = resolvedMaximumBankWidth;
maximumDepth = resolvedMaximumDepth;
}
public static RiverBodyProfile constant(double width, double bankWidth, double depth) {
return new RiverBodyProfile(
new double[]{0D, 1D},
new double[]{width, width},
new double[]{bankWidth, bankWidth},
new double[]{depth, depth},
new double[]{1D, 1D}
);
}
public double width(double alongReach) {
return sample(widths, alongReach);
}
public double bankWidth(double alongReach) {
return sample(bankWidths, alongReach);
}
public double depth(double alongReach) {
return sample(depths, alongReach);
}
public double roofScale(double alongReach) {
return sample(roofScales, alongReach);
}
public double maximumWidth() {
return maximumWidth;
}
public double maximumBankWidth() {
return maximumBankWidth;
}
public double maximumDepth() {
return maximumDepth;
}
public int size() {
return positions.length;
}
public double position(int index) {
return positions[index];
}
public double widthAtIndex(int index) {
return widths[index];
}
public double bankWidthAtIndex(int index) {
return bankWidths[index];
}
public double depthAtIndex(int index) {
return depths[index];
}
public double roofScaleAtIndex(int index) {
return roofScales[index];
}
public int intervalIndex(double alongReach) {
double position = StrictMath.max(0D, StrictMath.min(1D, alongReach));
int index = Arrays.binarySearch(positions, position);
if (index >= 0) {
return StrictMath.min(index, positions.length - 2);
}
return StrictMath.max(0, StrictMath.min(-index - 2, positions.length - 2));
}
@Override
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (!(object instanceof RiverBodyProfile profile)) {
return false;
}
return Arrays.equals(positions, profile.positions)
&& Arrays.equals(widths, profile.widths)
&& Arrays.equals(bankWidths, profile.bankWidths)
&& Arrays.equals(depths, profile.depths)
&& Arrays.equals(roofScales, profile.roofScales);
}
@Override
public int hashCode() {
int hash = Arrays.hashCode(positions);
hash = 31 * hash + Arrays.hashCode(widths);
hash = 31 * hash + Arrays.hashCode(bankWidths);
hash = 31 * hash + Arrays.hashCode(depths);
return 31 * hash + Arrays.hashCode(roofScales);
}
private double sample(double[] values, double alongReach) {
double position = StrictMath.max(0D, StrictMath.min(1D, alongReach));
int index = Arrays.binarySearch(positions, position);
if (index >= 0) {
return values[index];
}
int upper = -index - 1;
if (upper <= 0) {
return values[0];
}
if (upper >= positions.length) {
return values[values.length - 1];
}
int lower = upper - 1;
double range = positions[upper] - positions[lower];
double interpolation = range <= 0D ? 0D : (position - positions[lower]) / range;
return values[lower] + (values[upper] - values[lower]) * interpolation;
}
private static void requirePositive(double value, String name) {
if (!Double.isFinite(value) || value <= 0D) {
throw new IllegalArgumentException("River body profile " + name + " must be finite and positive");
}
}
private static void requireNonNegative(double value, String name) {
if (!Double.isFinite(value) || value < 0D) {
throw new IllegalArgumentException("River body profile " + name + " must be finite and non-negative");
}
}
private static void requireUnitScale(double value, String name) {
if (!Double.isFinite(value) || value <= 0D || value > 1D) {
throw new IllegalArgumentException("River body profile " + name + " must be greater than zero and at most one");
}
}
}
@@ -1,34 +0,0 @@
package art.arcane.iris.engine.river;
import java.util.Objects;
public record RiverEdgeId(RiverNodeId first, RiverNodeId second) implements Comparable<RiverEdgeId> {
public RiverEdgeId {
Objects.requireNonNull(first);
Objects.requireNonNull(second);
if (first.compareTo(second) >= 0) {
throw new IllegalArgumentException("River edge endpoints must be distinct and canonical");
}
}
public static RiverEdgeId of(RiverNodeId first, RiverNodeId second) {
Objects.requireNonNull(first);
Objects.requireNonNull(second);
if (first.equals(second)) {
throw new IllegalArgumentException("River edge endpoints must be distinct");
}
return first.compareTo(second) < 0
? new RiverEdgeId(first, second)
: new RiverEdgeId(second, first);
}
public long stableId() {
return RiverNetwork.mix(first.stableId() ^ Long.rotateLeft(second.stableId(), 29));
}
@Override
public int compareTo(RiverEdgeId other) {
int firstComparison = first.compareTo(other.first);
return firstComparison != 0 ? firstComparison : second.compareTo(other.second);
}
}
File diff suppressed because it is too large Load Diff
@@ -1,488 +0,0 @@
package art.arcane.iris.engine.river;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public record RiverNetworkOptions(
long seed,
int cellSize,
int tileCells,
double siteJitter,
int maxRouteReaches,
int minimumSourcesPerTile,
int downstreamCandidateLimit,
int routingBasinCells,
int routingDeviationScaleCells,
double routingDeviationStrengthCells,
double routingPlateauHeight,
double hydraulicBaseHeight,
boolean requireOcean,
double sourceChance,
double reachChance,
double dryChannelChance,
double terrainHeightWeight,
double routingNoiseWeight,
double flowAlignmentWeight,
double confluenceWeight,
double oceanAttraction,
double channelWidth,
double bankWidth,
double depth,
double channelRadiusBonus,
double maxChannelWidth,
double maxBankWidth,
double maxDepth,
double orderWidthFactor,
double orderDepthFactor,
double maximumReachRadius,
List<RiverWorm> worms
) {
public RiverNetworkOptions {
requireRange(cellSize, 8, 4096, "cellSize");
requireRange(tileCells, 1, 64, "tileCells");
requireRange(maxRouteReaches, 1, 256, "maxRouteReaches");
requireRange(minimumSourcesPerTile, 0, tileCells * tileCells, "minimumSourcesPerTile");
requireRange(downstreamCandidateLimit, 1, 8, "downstreamCandidateLimit");
requireRange(routingBasinCells, 8, 256, "routingBasinCells");
requireRange(routingDeviationScaleCells, 8, 256, "routingDeviationScaleCells");
requireRange(routingDeviationStrengthCells, 0D, 32D, "routingDeviationStrengthCells");
requirePositive(routingPlateauHeight, "routingPlateauHeight");
requireFinite(hydraulicBaseHeight, "hydraulicBaseHeight");
requireProbability(siteJitter, "siteJitter");
requireProbability(sourceChance, "sourceChance");
requireProbability(reachChance, "reachChance");
requireProbability(dryChannelChance, "dryChannelChance");
requireFiniteNonNegative(terrainHeightWeight, "terrainHeightWeight");
requireFiniteNonNegative(routingNoiseWeight, "routingNoiseWeight");
requireFiniteNonNegative(flowAlignmentWeight, "flowAlignmentWeight");
requireFiniteNonNegative(confluenceWeight, "confluenceWeight");
requireFiniteNonNegative(oceanAttraction, "oceanAttraction");
requirePositive(channelWidth, "channelWidth");
requireFiniteNonNegative(bankWidth, "bankWidth");
requirePositive(depth, "depth");
requireFiniteNonNegative(channelRadiusBonus, "channelRadiusBonus");
requirePositive(maxChannelWidth, "maxChannelWidth");
requireFiniteNonNegative(maxBankWidth, "maxBankWidth");
requirePositive(maxDepth, "maxDepth");
requireFiniteNonNegative(orderWidthFactor, "orderWidthFactor");
requireFiniteNonNegative(orderDepthFactor, "orderDepthFactor");
requireFiniteNonNegative(maximumReachRadius, "maximumReachRadius");
if (worms == null || worms.isEmpty()) {
throw new IllegalArgumentException("worms must contain at least one profile");
}
if (worms.size() > 16) {
throw new IllegalArgumentException("worms must contain at most 16 root profiles");
}
Set<String> ids = new HashSet<String>();
Set<Long> seeds = new HashSet<Long>();
int wormCount = validateWormTree(worms, 1, ids, seeds);
if (wormCount > 128) {
throw new IllegalArgumentException("worm hierarchy must contain at most 128 profiles");
}
worms = List.copyOf(worms);
RiverTopologyComplexity.requireSafe(
cellSize,
tileCells,
siteJitter,
maxRouteReaches,
maximumReachRadius,
maximumWormOffset(worms),
maximumWormSegments(worms)
);
}
public static Builder builder(long seed) {
return new Builder(seed);
}
public double maximumWormOffset() {
return maximumWormOffset(worms);
}
public int maximumWormSegments() {
return maximumWormSegments(worms);
}
private static double maximumWormOffset(List<RiverWorm> worms) {
double maximum = 0D;
for (RiverWorm worm : worms) {
if (worm == null) {
throw new IllegalArgumentException("worms must not contain null profiles");
}
maximum = StrictMath.max(maximum, worm.maxOffset());
maximum = StrictMath.max(maximum, maximumWormOffset(worm.children()));
}
return maximum;
}
private static int maximumWormSegments(List<RiverWorm> worms) {
int maximum = 1;
for (RiverWorm worm : worms) {
if (worm == null) {
throw new IllegalArgumentException("worms must not contain null profiles");
}
maximum = StrictMath.max(maximum, worm.segments());
maximum = StrictMath.max(maximum, maximumWormSegments(worm.children()));
}
return maximum;
}
private static int validateWormTree(
List<RiverWorm> worms,
int depth,
Set<String> ids,
Set<Long> seeds
) {
if (depth > 4) {
throw new IllegalArgumentException("worm hierarchy must be at most 4 profiles deep");
}
int count = 0;
for (RiverWorm worm : worms) {
if (worm == null) {
throw new IllegalArgumentException("worm hierarchy must not contain null profiles");
}
if (!ids.add(worm.id())) {
throw new IllegalArgumentException("worm ids must be unique: " + worm.id());
}
if (!seeds.add(worm.seed())) {
throw new IllegalArgumentException("worm seeds must be unique: " + worm.seed());
}
count++;
if (!worm.children().isEmpty()) {
count += validateWormTree(worm.children(), depth + 1, ids, seeds);
}
}
return count;
}
private static void requireRange(int value, int minimum, int maximum, String name) {
if (value < minimum || value > maximum) {
throw new IllegalArgumentException(name + " must be between " + minimum + " and " + maximum);
}
}
private static void requireRange(double value, double minimum, double maximum, String name) {
if (!Double.isFinite(value) || value < minimum || value > maximum) {
throw new IllegalArgumentException(name + " must be between " + minimum + " and " + maximum);
}
}
private static void requireProbability(double value, String name) {
if (!Double.isFinite(value) || value < 0.0 || value > 1.0) {
throw new IllegalArgumentException(name + " must be finite and between 0 and 1");
}
}
private static void requireFiniteNonNegative(double value, String name) {
if (!Double.isFinite(value) || value < 0.0) {
throw new IllegalArgumentException(name + " must be finite and non-negative");
}
}
private static void requirePositive(double value, String name) {
if (!Double.isFinite(value) || value <= 0.0) {
throw new IllegalArgumentException(name + " must be finite and positive");
}
}
private static void requireFinite(double value, String name) {
if (!Double.isFinite(value)) {
throw new IllegalArgumentException(name + " must be finite");
}
}
public static final class Builder {
private final long seed;
private int cellSize;
private int tileCells;
private double siteJitter;
private int maxRouteReaches;
private int minimumSourcesPerTile;
private int downstreamCandidateLimit;
private int routingBasinCells;
private int routingDeviationScaleCells;
private double routingDeviationStrengthCells;
private double routingPlateauHeight;
private double hydraulicBaseHeight;
private boolean requireOcean;
private double sourceChance;
private double reachChance;
private double dryChannelChance;
private double terrainHeightWeight;
private double routingNoiseWeight;
private double flowAlignmentWeight;
private double confluenceWeight;
private double oceanAttraction;
private double channelWidth;
private double bankWidth;
private double depth;
private double channelRadiusBonus;
private double maxChannelWidth;
private double maxBankWidth;
private double maxDepth;
private double orderWidthFactor;
private double orderDepthFactor;
private double maximumReachRadius;
private List<RiverWorm> worms;
private Builder(long seed) {
this.seed = seed;
cellSize = 512;
tileCells = 4;
siteJitter = 0.35;
maxRouteReaches = 16;
minimumSourcesPerTile = 0;
downstreamCandidateLimit = 4;
routingBasinCells = 64;
routingDeviationScaleCells = 24;
routingDeviationStrengthCells = 0D;
routingPlateauHeight = 8.0;
hydraulicBaseHeight = 64D;
requireOcean = false;
sourceChance = 0.12;
reachChance = 0.98;
dryChannelChance = 0.35;
terrainHeightWeight = 1.0;
routingNoiseWeight = 24.0;
flowAlignmentWeight = 0D;
confluenceWeight = 0D;
oceanAttraction = 64.0;
channelWidth = 10.0;
bankWidth = 8.0;
depth = 4.0;
maxChannelWidth = 10D;
maxBankWidth = 8D;
maxDepth = 10D;
orderWidthFactor = 0.35;
orderDepthFactor = 0.2;
maximumReachRadius = Double.NaN;
worms = List.of(new RiverWorm(
"default",
1L,
1D,
1024D,
256D,
0.5D,
0.15D,
40D,
8,
1D,
1D,
1D,
512D,
128D,
0.3D,
0D,
0D,
0D,
0D,
4,
0.35D,
1D,
0D,
0D,
List.of()
));
}
public Builder cellSize(int value) {
cellSize = value;
return this;
}
public Builder tileCells(int value) {
tileCells = value;
return this;
}
public Builder siteJitter(double value) {
siteJitter = value;
return this;
}
public Builder maxRouteReaches(int value) {
maxRouteReaches = value;
return this;
}
public Builder minimumSourcesPerTile(int value) {
minimumSourcesPerTile = value;
return this;
}
public Builder downstreamCandidateLimit(int value) {
downstreamCandidateLimit = value;
return this;
}
public Builder routingBasinCells(int value) {
routingBasinCells = value;
return this;
}
public Builder routingDeviationScaleCells(int value) {
routingDeviationScaleCells = value;
return this;
}
public Builder routingDeviationStrengthCells(double value) {
routingDeviationStrengthCells = value;
return this;
}
public Builder routingPlateauHeight(double value) {
routingPlateauHeight = value;
return this;
}
public Builder hydraulicBaseHeight(double value) {
hydraulicBaseHeight = value;
return this;
}
public Builder requireOcean(boolean value) {
requireOcean = value;
return this;
}
public Builder sourceChance(double value) {
sourceChance = value;
return this;
}
public Builder reachChance(double value) {
reachChance = value;
return this;
}
public Builder dryChannelChance(double value) {
dryChannelChance = value;
return this;
}
public Builder terrainHeightWeight(double value) {
terrainHeightWeight = value;
return this;
}
public Builder routingNoiseWeight(double value) {
routingNoiseWeight = value;
return this;
}
public Builder flowAlignmentWeight(double value) {
flowAlignmentWeight = value;
return this;
}
public Builder confluenceWeight(double value) {
confluenceWeight = value;
return this;
}
public Builder oceanAttraction(double value) {
oceanAttraction = value;
return this;
}
public Builder channelWidth(double value) {
channelWidth = value;
return this;
}
public Builder bankWidth(double value) {
bankWidth = value;
return this;
}
public Builder depth(double value) {
depth = value;
return this;
}
public Builder channelRadiusBonus(double value) {
channelRadiusBonus = value;
return this;
}
public Builder maxChannelWidth(double value) {
maxChannelWidth = value;
return this;
}
public Builder maxBankWidth(double value) {
maxBankWidth = value;
return this;
}
public Builder maxDepth(double value) {
maxDepth = value;
return this;
}
public Builder orderWidthFactor(double value) {
orderWidthFactor = value;
return this;
}
public Builder orderDepthFactor(double value) {
orderDepthFactor = value;
return this;
}
public Builder maximumReachRadius(double value) {
maximumReachRadius = value;
return this;
}
public Builder worms(List<RiverWorm> value) {
worms = value;
return this;
}
public RiverNetworkOptions build() {
double resolvedMaximumReachRadius = Double.isFinite(maximumReachRadius)
? maximumReachRadius
: defaultMaximumReachRadius();
return new RiverNetworkOptions(
seed,
cellSize,
tileCells,
siteJitter,
maxRouteReaches,
minimumSourcesPerTile,
downstreamCandidateLimit,
routingBasinCells,
routingDeviationScaleCells,
routingDeviationStrengthCells,
routingPlateauHeight,
hydraulicBaseHeight,
requireOcean,
sourceChance,
reachChance,
dryChannelChance,
terrainHeightWeight,
routingNoiseWeight,
flowAlignmentWeight,
confluenceWeight,
oceanAttraction,
channelWidth,
bankWidth,
depth,
channelRadiusBonus,
maxChannelWidth,
maxBankWidth,
maxDepth,
orderWidthFactor,
orderDepthFactor,
resolvedMaximumReachRadius,
worms
);
}
private double defaultMaximumReachRadius() {
return maxChannelWidth * 0.5D + maxBankWidth;
}
}
}
@@ -1,24 +0,0 @@
package art.arcane.iris.engine.river;
import java.util.Objects;
public record RiverNode(
RiverNodeId id,
double x,
double z,
double naturalHeight,
double hydraulicHeight,
double rank,
double routingScore,
boolean ocean,
boolean riverAllowed
) {
public RiverNode {
Objects.requireNonNull(id);
if (!Double.isFinite(x) || !Double.isFinite(z) || !Double.isFinite(naturalHeight)
|| !Double.isFinite(hydraulicHeight)
|| !Double.isFinite(rank) || !Double.isFinite(routingScore)) {
throw new IllegalArgumentException("River node coordinates, height, and rank must be finite");
}
}
}
@@ -1,13 +0,0 @@
package art.arcane.iris.engine.river;
public record RiverNodeId(long cellX, long cellZ) implements Comparable<RiverNodeId> {
public long stableId() {
return RiverNetwork.mix(cellX * 0x9E3779B97F4A7C15L ^ Long.rotateLeft(cellZ * 0xC2B2AE3D27D4EB4FL, 31));
}
@Override
public int compareTo(RiverNodeId other) {
int xComparison = Long.compare(cellX, other.cellX);
return xComparison != 0 ? xComparison : Long.compare(cellZ, other.cellZ);
}
}
@@ -1,48 +0,0 @@
package art.arcane.iris.engine.river;
public final class RiverPolyline {
private final double[] x;
private final double[] z;
private final double[] cumulativeLength;
private final double length;
public RiverPolyline(double[] x, double[] z) {
if (x.length != z.length || x.length < 2) {
throw new IllegalArgumentException("River polyline requires matching coordinate arrays and at least two points");
}
this.x = x.clone();
this.z = z.clone();
cumulativeLength = new double[x.length];
double measuredLength = 0.0;
for (int i = 0; i < x.length; i++) {
if (!Double.isFinite(x[i]) || !Double.isFinite(z[i])) {
throw new IllegalArgumentException("River polyline coordinates must be finite");
}
if (i > 0) {
measuredLength += StrictMath.hypot(x[i] - x[i - 1], z[i] - z[i - 1]);
cumulativeLength[i] = measuredLength;
}
}
length = measuredLength;
}
public int size() {
return x.length;
}
public double x(int index) {
return x[index];
}
public double z(int index) {
return z[index];
}
public double cumulativeLength(int index) {
return cumulativeLength[index];
}
public double length() {
return length;
}
}
@@ -1,59 +0,0 @@
package art.arcane.iris.engine.river;
import java.util.Objects;
public record RiverReach(
RiverEdgeId id,
RiverNode from,
RiverNode to,
RiverRouteState state,
int flow,
int order,
double width,
double bankWidth,
double depth,
RiverBodyProfile bodyProfile,
boolean mouth,
boolean terminal,
RiverPolyline polyline
) {
public RiverReach {
Objects.requireNonNull(id);
Objects.requireNonNull(from);
Objects.requireNonNull(to);
Objects.requireNonNull(state);
Objects.requireNonNull(bodyProfile);
Objects.requireNonNull(polyline);
if (state == RiverRouteState.SUPPRESSED) {
throw new IllegalArgumentException("Suppressed routes cannot produce reaches");
}
if (flow < 1 || order < 1) {
throw new IllegalArgumentException("River reach flow and order must be positive");
}
if (!Double.isFinite(width) || width <= 0.0 || !Double.isFinite(bankWidth) || bankWidth < 0.0
|| !Double.isFinite(depth) || depth <= 0.0) {
throw new IllegalArgumentException("River reach dimensions must be finite and valid");
}
if (Double.compare(width, bodyProfile.maximumWidth()) != 0
|| Double.compare(bankWidth, bodyProfile.maximumBankWidth()) != 0
|| Double.compare(depth, bodyProfile.maximumDepth()) != 0) {
throw new IllegalArgumentException("River reach dimensions must equal their body-profile maxima");
}
}
public double widthAt(double alongReach) {
return bodyProfile.width(alongReach);
}
public double bankWidthAt(double alongReach) {
return bodyProfile.bankWidth(alongReach);
}
public double depthAt(double alongReach) {
return bodyProfile.depth(alongReach);
}
public double roofScaleAt(double alongReach) {
return bodyProfile.roofScale(alongReach);
}
}
@@ -1,18 +0,0 @@
package art.arcane.iris.engine.river;
import java.util.List;
import java.util.Objects;
public record RiverRoute(
RiverNodeId source,
RiverRouteState state,
List<RiverEdgeId> edges,
boolean oceanConnected,
boolean terminal
) {
public RiverRoute {
Objects.requireNonNull(source);
Objects.requireNonNull(state);
edges = List.copyOf(edges);
}
}
@@ -1,7 +0,0 @@
package art.arcane.iris.engine.river;
public enum RiverRouteState {
WET,
DRY,
SUPPRESSED
}
@@ -1,105 +0,0 @@
package art.arcane.iris.engine.river;
import java.util.Objects;
import java.util.function.Supplier;
public final class RiverRoutingContext {
private final RiverEdgeId edgeId;
private final RiverNode from;
private final RiverNode to;
private final Supplier<RiverPolyline> polylineSupplier;
private volatile RiverPolyline polyline;
public RiverRoutingContext(RiverEdgeId edgeId, RiverNode from, RiverNode to, RiverPolyline polyline) {
this(edgeId, from, to, () -> polyline, Objects.requireNonNull(polyline));
}
static RiverRoutingContext lazy(
RiverEdgeId edgeId,
RiverNode from,
RiverNode to,
Supplier<RiverPolyline> polylineSupplier
) {
return new RiverRoutingContext(edgeId, from, to, polylineSupplier, null);
}
private RiverRoutingContext(
RiverEdgeId edgeId,
RiverNode from,
RiverNode to,
Supplier<RiverPolyline> polylineSupplier,
RiverPolyline polyline
) {
this.edgeId = Objects.requireNonNull(edgeId);
this.from = Objects.requireNonNull(from);
this.to = Objects.requireNonNull(to);
this.polylineSupplier = Objects.requireNonNull(polylineSupplier);
this.polyline = polyline;
}
public RiverEdgeId edgeId() {
return edgeId;
}
public RiverNode from() {
return from;
}
public RiverNode to() {
return to;
}
public RiverPolyline polyline() {
RiverPolyline resolved = polyline;
if (resolved != null) {
return resolved;
}
synchronized (this) {
if (polyline == null) {
polyline = Objects.requireNonNull(polylineSupplier.get());
}
return polyline;
}
}
public int midpointX() {
return (int) StrictMath.max(
Integer.MIN_VALUE,
StrictMath.min(Integer.MAX_VALUE, StrictMath.round((from.x() + to.x()) * 0.5))
);
}
public int midpointZ() {
return (int) StrictMath.max(
Integer.MIN_VALUE,
StrictMath.min(Integer.MAX_VALUE, StrictMath.round((from.z() + to.z()) * 0.5))
);
}
@Override
public boolean equals(Object candidate) {
if (this == candidate) {
return true;
}
if (!(candidate instanceof RiverRoutingContext context)) {
return false;
}
return edgeId.equals(context.edgeId)
&& from.equals(context.from)
&& to.equals(context.to)
&& polyline().equals(context.polyline());
}
@Override
public int hashCode() {
return Objects.hash(edgeId, from, to, polyline());
}
@Override
public String toString() {
return "RiverRoutingContext[edgeId=" + edgeId
+ ", from=" + from
+ ", to=" + to
+ ", polyline=" + polyline() + "]";
}
}
@@ -1,37 +0,0 @@
package art.arcane.iris.engine.river;
public record RiverSample(
boolean present,
RiverRouteState state,
RiverSection section,
double distance,
double alongReach,
double carveWeight,
int flow,
int order,
double width,
double bankWidth,
double depth,
boolean terminal,
RiverEdgeId reachId
) {
private static final RiverSample NONE = new RiverSample(
false,
RiverRouteState.SUPPRESSED,
RiverSection.NONE,
Double.POSITIVE_INFINITY,
0.0,
0.0,
0,
0,
0.0,
0.0,
0.0,
false,
null
);
public static RiverSample none() {
return NONE;
}
}
@@ -1,10 +0,0 @@
package art.arcane.iris.engine.river;
public enum RiverSection {
NONE,
CHANNEL,
MOUTH,
BANK,
DRY_CHANNEL,
DRY_BANK
}
@@ -1,8 +0,0 @@
package art.arcane.iris.engine.river;
public enum RiverTerminalPolicy {
INHERIT,
WET,
DRY,
SUPPRESS
}
@@ -1,9 +0,0 @@
package art.arcane.iris.engine.river;
public record RiverTerrainNodeSample(
double naturalHeight,
boolean ocean,
boolean riverAllowed,
double routingCost
) {
}
@@ -1,99 +0,0 @@
package art.arcane.iris.engine.river;
public interface RiverTerrainSampler {
double naturalHeight(int blockX, int blockZ);
boolean isOcean(int blockX, int blockZ);
default RiverTerrainNodeSample sampleNode(int blockX, int blockZ) {
return new RiverTerrainNodeSample(
naturalHeight(blockX, blockZ),
isOcean(blockX, blockZ),
allowsRiver(blockX, blockZ),
routingCost(blockX, blockZ)
);
}
default RiverTerrainSourceSample sampleSource(int blockX, int blockZ) {
return new RiverTerrainSourceSample(
sourceChanceMultiplier(blockX, blockZ),
allowsRiver(blockX, blockZ),
isOcean(blockX, blockZ)
);
}
default double routingCost(int blockX, int blockZ) {
return 0.0;
}
default double sourceChanceMultiplier(int blockX, int blockZ) {
return 1.0;
}
default double maximumSourceChanceMultiplier() {
return Double.POSITIVE_INFINITY;
}
default double reachChanceMultiplier(int blockX, int blockZ) {
return 1.0;
}
default boolean allowsRiver(int blockX, int blockZ) {
return true;
}
default boolean allowsReach(RiverRoutingContext context) {
return true;
}
default double reachRoutingCost(RiverRoutingContext context) {
return 0.0;
}
default double flowNoise(double x, double z) {
return Double.NaN;
}
default double channelWidth(RiverRoutingContext context, double fallback) {
return fallback;
}
default double channelWidth(
RiverRoutingContext context,
double x,
double z,
double fallback
) {
return channelWidth(context, fallback);
}
default double bankWidth(RiverRoutingContext context, double fallback) {
return fallback;
}
default double bankWidth(
RiverRoutingContext context,
double x,
double z,
double fallback
) {
return bankWidth(context, fallback);
}
default double depth(RiverRoutingContext context, double fallback) {
return fallback;
}
default double depth(
RiverRoutingContext context,
double x,
double z,
double fallback
) {
return depth(context, fallback);
}
default RiverTerminalPolicy terminalPolicy(int blockX, int blockZ) {
return RiverTerminalPolicy.INHERIT;
}
}
@@ -1,8 +0,0 @@
package art.arcane.iris.engine.river;
public record RiverTerrainSourceSample(
double chanceMultiplier,
boolean riverAllowed,
boolean ocean
) {
}
@@ -1,717 +0,0 @@
package art.arcane.iris.engine.river;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
public final class RiverTile {
private static final int BUCKET_SIZE = 64;
private final int tileX;
private final int tileZ;
private final int minimumX;
private final int minimumZ;
private final int maximumX;
private final int maximumZ;
private final List<RiverReach> reaches;
private final Map<RiverEdgeId, RiverReach> reachesById;
private final Map<Long, List<RiverReach>> spatialIndex;
public RiverTile(
int tileX,
int tileZ,
int minimumX,
int minimumZ,
int maximumX,
int maximumZ,
List<RiverReach> reaches
) {
if (minimumX >= maximumX || minimumZ >= maximumZ) {
throw new IllegalArgumentException("River tile bounds must have positive area");
}
this.tileX = tileX;
this.tileZ = tileZ;
this.minimumX = minimumX;
this.minimumZ = minimumZ;
this.maximumX = maximumX;
this.maximumZ = maximumZ;
this.reaches = List.copyOf(reaches);
reachesById = indexById(this.reaches);
spatialIndex = createSpatialIndex(this.reaches);
}
public int tileX() {
return tileX;
}
public int tileZ() {
return tileZ;
}
public int minimumX() {
return minimumX;
}
public int minimumZ() {
return minimumZ;
}
public int maximumX() {
return maximumX;
}
public int maximumZ() {
return maximumZ;
}
public List<RiverReach> reaches() {
return reaches;
}
public RiverReach reach(RiverEdgeId id) {
return reachesById.get(Objects.requireNonNull(id));
}
public List<RiverAnchor> candidateAnchors(double spacing, long salt) {
return candidateAnchors(minimumX, minimumZ, maximumX, maximumZ, spacing, salt);
}
public List<RiverAnchor> candidateAnchors(
double queryMinimumX,
double queryMinimumZ,
double queryMaximumX,
double queryMaximumZ,
double spacing,
long salt
) {
if (!Double.isFinite(spacing) || spacing <= 0.0) {
throw new IllegalArgumentException("River anchor spacing must be finite and positive");
}
if (!Double.isFinite(queryMinimumX) || !Double.isFinite(queryMinimumZ)
|| !Double.isFinite(queryMaximumX) || !Double.isFinite(queryMaximumZ)
|| queryMinimumX >= queryMaximumX || queryMinimumZ >= queryMaximumZ) {
throw new IllegalArgumentException("River anchor query bounds must be finite and have positive area");
}
ArrayList<RiverAnchor> anchors = new ArrayList<>();
for (RiverReach reach : indexedReaches(queryMinimumX, queryMinimumZ, queryMaximumX, queryMaximumZ)) {
addAnchors(
reach,
spacing,
salt,
queryMinimumX,
queryMinimumZ,
queryMaximumX,
queryMaximumZ,
anchors
);
}
return List.copyOf(anchors);
}
public int sampleCandidateCount(double x, double z) {
return indexedReaches(x, z).size();
}
public RiverSample sample(double x, double z) {
return sampleExpanded(x, z, 0D);
}
public RiverSample sampleExpanded(double x, double z, double additionalRadius) {
if (!Double.isFinite(additionalRadius) || additionalRadius < 0D) {
throw new IllegalArgumentException("Additional river sample radius must be finite and non-negative");
}
RiverReach nearestReach = null;
double nearestDistanceSquared = Double.POSITIVE_INFINITY;
double nearestAlongReach = 0.0;
List<RiverReach> candidates = additionalRadius == 0D
? indexedReaches(x, z)
: indexedReaches(
x - additionalRadius,
z - additionalRadius,
x + additionalRadius,
z + additionalRadius
);
for (RiverReach reach : candidates) {
ClosestPoint closest = closestCoveringPoint(reach, x, z, additionalRadius);
if (closest == null) {
continue;
}
if (closest.distanceSquared() < nearestDistanceSquared
|| (closest.distanceSquared() == nearestDistanceSquared
&& nearestReach != null
&& reach.id().compareTo(nearestReach.id()) < 0)) {
nearestReach = reach;
nearestDistanceSquared = closest.distanceSquared();
nearestAlongReach = closest.alongReach();
}
}
if (nearestReach == null) {
return RiverSample.none();
}
return createSample(nearestReach, nearestDistanceSquared, nearestAlongReach);
}
public RiverSample sampleFootprint(
double queryMinimumX,
double queryMinimumZ,
double queryMaximumX,
double queryMaximumZ
) {
if (!Double.isFinite(queryMinimumX) || !Double.isFinite(queryMinimumZ)
|| !Double.isFinite(queryMaximumX) || !Double.isFinite(queryMaximumZ)
|| queryMinimumX > queryMaximumX || queryMinimumZ > queryMaximumZ) {
throw new IllegalArgumentException("River footprint bounds must be finite and ordered");
}
RiverReach nearestReach = null;
double nearestDistanceSquared = Double.POSITIVE_INFINITY;
double nearestAlongReach = 0.0;
for (RiverReach reach : indexedReachesInclusive(
queryMinimumX,
queryMinimumZ,
queryMaximumX,
queryMaximumZ
)) {
ClosestPoint closest = closestCoveringPoint(
reach,
queryMinimumX,
queryMinimumZ,
queryMaximumX,
queryMaximumZ
);
if (closest == null) {
continue;
}
if (closest.distanceSquared() < nearestDistanceSquared
|| (closest.distanceSquared() == nearestDistanceSquared
&& nearestReach != null
&& reach.id().compareTo(nearestReach.id()) < 0)) {
nearestReach = reach;
nearestDistanceSquared = closest.distanceSquared();
nearestAlongReach = closest.alongReach();
}
}
if (nearestReach == null) {
return RiverSample.none();
}
return createSample(nearestReach, nearestDistanceSquared, nearestAlongReach);
}
private static RiverSample createSample(
RiverReach nearestReach,
double nearestDistanceSquared,
double nearestAlongReach
) {
double distance = StrictMath.sqrt(nearestDistanceSquared);
double localWidth = nearestReach.widthAt(nearestAlongReach);
double localBankWidth = nearestReach.bankWidthAt(nearestAlongReach);
double localDepth = nearestReach.depthAt(nearestAlongReach);
double channelRadius = localWidth * 0.5;
RiverSection section = section(nearestReach, distance, channelRadius);
double carveWeight = carveWeight(distance, channelRadius, localBankWidth);
return new RiverSample(
true,
nearestReach.state(),
section,
distance,
nearestAlongReach,
carveWeight,
nearestReach.flow(),
nearestReach.order(),
localWidth,
localBankWidth,
localDepth,
nearestReach.terminal(),
nearestReach.id()
);
}
private static RiverSection section(RiverReach reach, double distance, double channelRadius) {
if (distance <= channelRadius) {
if (reach.state() == RiverRouteState.DRY) {
return RiverSection.DRY_CHANNEL;
}
return reach.mouth() ? RiverSection.MOUTH : RiverSection.CHANNEL;
}
return reach.state() == RiverRouteState.DRY ? RiverSection.DRY_BANK : RiverSection.BANK;
}
private void addAnchors(
RiverReach reach,
double spacing,
long salt,
double queryMinimumX,
double queryMinimumZ,
double queryMaximumX,
double queryMaximumZ,
List<RiverAnchor> anchors
) {
double length = reach.polyline().length();
double firstDistance = unit(RiverNetwork.mix(reach.id().stableId() ^ salt)) * spacing;
int index = 0;
for (double distance = firstDistance; distance < length; distance += spacing) {
Position position = positionAt(reach.polyline(), distance);
if (position.x() >= minimumX && position.x() < maximumX
&& position.z() >= minimumZ && position.z() < maximumZ
&& position.x() >= queryMinimumX && position.x() < queryMaximumX
&& position.z() >= queryMinimumZ && position.z() < queryMaximumZ) {
long stableId = RiverNetwork.mix(
reach.id().stableId() ^ salt ^ (long) index * 0x9E3779B97F4A7C15L
);
anchors.add(new RiverAnchor(
reach.id(),
index,
stableId,
spacing,
salt,
position.x(),
position.z(),
position.alongReach(),
reach.state(),
reach.flow(),
reach.order()
));
}
index++;
}
}
private static Position positionAt(RiverPolyline polyline, double targetDistance) {
double traversed = 0.0;
for (int point = 0; point < polyline.size() - 1; point++) {
double startX = polyline.x(point);
double startZ = polyline.z(point);
double deltaX = polyline.x(point + 1) - startX;
double deltaZ = polyline.z(point + 1) - startZ;
double segmentLength = StrictMath.hypot(deltaX, deltaZ);
if (targetDistance <= traversed + segmentLength || point == polyline.size() - 2) {
double t = segmentLength == 0.0 ? 0.0 : (targetDistance - traversed) / segmentLength;
t = StrictMath.max(0.0, StrictMath.min(1.0, t));
double alongReach = polyline.length() == 0.0 ? 0.0 : targetDistance / polyline.length();
return new Position(startX + deltaX * t, startZ + deltaZ * t, alongReach);
}
traversed += segmentLength;
}
return new Position(
polyline.x(polyline.size() - 1),
polyline.z(polyline.size() - 1),
1.0
);
}
private static double unit(long hash) {
return (hash >>> 11) * 0x1.0p-53;
}
private static double carveWeight(double distance, double channelRadius, double bankWidth) {
if (distance <= channelRadius || bankWidth == 0.0) {
return 1.0;
}
double t = StrictMath.min(1.0, (distance - channelRadius) / bankWidth);
double smooth = t * t * (3.0 - 2.0 * t);
return 1.0 - smooth;
}
private static ClosestPoint closestCoveringPoint(
RiverReach reach,
double x,
double z,
double additionalRadius
) {
RiverPolyline polyline = reach.polyline();
RiverBodyProfile bodyProfile = reach.bodyProfile();
double polylineLength = polyline.length();
if (polylineLength == 0D) {
double distanceSquared = squared(x - polyline.x(0)) + squared(z - polyline.z(0));
double radius = reach.widthAt(0D) * 0.5D + reach.bankWidthAt(0D) + additionalRadius;
return distanceSquared <= radius * radius ? new ClosestPoint(distanceSquared, 0D) : null;
}
double nearest = Double.POSITIVE_INFINITY;
double nearestAlong = 0.0;
int pointLimit = polyline.size() - 1;
int profileLimit = bodyProfile.size() - 1;
for (int point = 0; point < pointLimit; point++) {
double segmentStartAlong = polyline.cumulativeLength(point) / polylineLength;
double segmentEndAlong = polyline.cumulativeLength(point + 1) / polylineLength;
double segmentAlongSpan = segmentEndAlong - segmentStartAlong;
if (segmentAlongSpan == 0D) {
continue;
}
double startX = polyline.x(point);
double startZ = polyline.z(point);
double deltaX = polyline.x(point + 1) - startX;
double deltaZ = polyline.z(point + 1) - startZ;
int firstProfileIndex = bodyProfile.intervalIndex(segmentStartAlong);
for (int profileIndex = firstProfileIndex;
profileIndex < profileLimit
&& bodyProfile.position(profileIndex) <= segmentEndAlong;
profileIndex++) {
double profileStart = bodyProfile.position(profileIndex);
double profileEnd = bodyProfile.position(profileIndex + 1);
double overlapStart = StrictMath.max(segmentStartAlong, profileStart);
double overlapEnd = StrictMath.min(segmentEndAlong, profileEnd);
if (overlapStart > overlapEnd) {
continue;
}
double intervalStart = (overlapStart - segmentStartAlong) / segmentAlongSpan;
double intervalEnd = (overlapEnd - segmentStartAlong) / segmentAlongSpan;
double profileSpan = profileEnd - profileStart;
double profileWidth = bodyProfile.widthAtIndex(profileIndex);
double profileBankWidth = bodyProfile.bankWidthAtIndex(profileIndex);
double widthSlope = (bodyProfile.widthAtIndex(profileIndex + 1)
- profileWidth) / profileSpan;
double bankSlope = (bodyProfile.bankWidthAtIndex(profileIndex + 1)
- profileBankWidth) / profileSpan;
double radiusBase = (profileWidth
+ widthSlope * (segmentStartAlong - profileStart)) * 0.5D
+ profileBankWidth
+ bankSlope * (segmentStartAlong - profileStart)
+ additionalRadius;
double radiusSlope = (widthSlope * 0.5D + bankSlope) * segmentAlongSpan;
ClosestPoint candidate = coveringPoint(
intervalStart,
intervalEnd,
deltaX,
startX - x,
deltaZ,
startZ - z,
radiusSlope,
radiusBase,
segmentStartAlong,
segmentAlongSpan
);
if (candidate != null && candidate.distanceSquared() < nearest) {
nearest = candidate.distanceSquared();
nearestAlong = candidate.alongReach();
}
}
}
return Double.isFinite(nearest) ? new ClosestPoint(nearest, nearestAlong) : null;
}
private static ClosestPoint closestCoveringPoint(
RiverReach reach,
double minimumX,
double minimumZ,
double maximumX,
double maximumZ
) {
RiverPolyline polyline = reach.polyline();
RiverBodyProfile bodyProfile = reach.bodyProfile();
double polylineLength = polyline.length();
if (polylineLength == 0D) {
double distanceSquared = pointRectangleDistanceSquared(
polyline.x(0),
polyline.z(0),
minimumX,
minimumZ,
maximumX,
maximumZ
);
double radius = reach.widthAt(0D) * 0.5D + reach.bankWidthAt(0D);
return distanceSquared <= radius * radius ? new ClosestPoint(distanceSquared, 0D) : null;
}
double nearest = Double.POSITIVE_INFINITY;
double nearestAlong = 0.0;
int pointLimit = polyline.size() - 1;
int profileLimit = bodyProfile.size() - 1;
for (int point = 0; point < pointLimit; point++) {
double segmentStartAlong = polyline.cumulativeLength(point) / polylineLength;
double segmentEndAlong = polyline.cumulativeLength(point + 1) / polylineLength;
double segmentAlongSpan = segmentEndAlong - segmentStartAlong;
if (segmentAlongSpan == 0D) {
continue;
}
double startX = polyline.x(point);
double startZ = polyline.z(point);
double deltaX = polyline.x(point + 1) - startX;
double deltaZ = polyline.z(point + 1) - startZ;
int firstProfileIndex = bodyProfile.intervalIndex(segmentStartAlong);
for (int profileIndex = firstProfileIndex;
profileIndex < profileLimit
&& bodyProfile.position(profileIndex) <= segmentEndAlong;
profileIndex++) {
double profileStart = bodyProfile.position(profileIndex);
double profileEnd = bodyProfile.position(profileIndex + 1);
double overlapStart = StrictMath.max(segmentStartAlong, profileStart);
double overlapEnd = StrictMath.min(segmentEndAlong, profileEnd);
if (overlapStart > overlapEnd) {
continue;
}
double intervalStart = (overlapStart - segmentStartAlong) / segmentAlongSpan;
double intervalEnd = (overlapEnd - segmentStartAlong) / segmentAlongSpan;
double profileSpan = profileEnd - profileStart;
double profileWidth = bodyProfile.widthAtIndex(profileIndex);
double profileBankWidth = bodyProfile.bankWidthAtIndex(profileIndex);
double widthSlope = (bodyProfile.widthAtIndex(profileIndex + 1)
- profileWidth) / profileSpan;
double bankSlope = (bodyProfile.bankWidthAtIndex(profileIndex + 1)
- profileBankWidth) / profileSpan;
double radiusBase = (profileWidth
+ widthSlope * (segmentStartAlong - profileStart)) * 0.5D
+ profileBankWidth
+ bankSlope * (segmentStartAlong - profileStart);
double radiusSlope = (widthSlope * 0.5D + bankSlope) * segmentAlongSpan;
double cursor = intervalStart;
do {
double next = intervalEnd;
next = nextCrossing(startX, deltaX, minimumX, cursor, next);
next = nextCrossing(startX, deltaX, maximumX, cursor, next);
next = nextCrossing(startZ, deltaZ, minimumZ, cursor, next);
next = nextCrossing(startZ, deltaZ, maximumZ, cursor, next);
double middle = (cursor + next) * 0.5D;
double middleX = startX + deltaX * middle;
double middleZ = startZ + deltaZ * middle;
double distanceSlopeX = middleX < minimumX || middleX > maximumX ? deltaX : 0D;
double distanceBaseX = middleX < minimumX
? startX - minimumX
: middleX > maximumX ? startX - maximumX : 0D;
double distanceSlopeZ = middleZ < minimumZ || middleZ > maximumZ ? deltaZ : 0D;
double distanceBaseZ = middleZ < minimumZ
? startZ - minimumZ
: middleZ > maximumZ ? startZ - maximumZ : 0D;
ClosestPoint candidate = coveringPoint(
cursor,
next,
distanceSlopeX,
distanceBaseX,
distanceSlopeZ,
distanceBaseZ,
radiusSlope,
radiusBase,
segmentStartAlong,
segmentAlongSpan
);
if (candidate != null && candidate.distanceSquared() < nearest) {
nearest = candidate.distanceSquared();
nearestAlong = candidate.alongReach();
}
cursor = next;
} while (cursor < intervalEnd);
}
}
return Double.isFinite(nearest) ? new ClosestPoint(nearest, nearestAlong) : null;
}
private static ClosestPoint coveringPoint(
double intervalStart,
double intervalEnd,
double distanceSlopeX,
double distanceBaseX,
double distanceSlopeZ,
double distanceBaseZ,
double radiusSlope,
double radiusBase,
double segmentStartAlong,
double segmentAlongSpan
) {
double distanceQuadratic = squared(distanceSlopeX) + squared(distanceSlopeZ);
double distanceLinear = 2D * (distanceSlopeX * distanceBaseX + distanceSlopeZ * distanceBaseZ);
double distanceConstant = squared(distanceBaseX) + squared(distanceBaseZ);
double coverageQuadratic = distanceQuadratic - squared(radiusSlope);
double coverageLinear = distanceLinear - 2D * radiusSlope * radiusBase;
double coverageConstant = distanceConstant - squared(radiusBase);
double distancePosition = distanceQuadratic == 0D
? intervalStart
: clamp(-distanceLinear / (2D * distanceQuadratic), intervalStart, intervalEnd);
if (quadraticValue(coverageQuadratic, coverageLinear, coverageConstant, distancePosition) <= 0D) {
return new ClosestPoint(
StrictMath.max(0D, quadraticValue(
distanceQuadratic,
distanceLinear,
distanceConstant,
distancePosition
)),
segmentStartAlong + segmentAlongSpan * distancePosition
);
}
double coveragePosition = intervalStart;
double minimumCoverage = quadraticValue(
coverageQuadratic,
coverageLinear,
coverageConstant,
coveragePosition
);
double endCoverage = quadraticValue(coverageQuadratic, coverageLinear, coverageConstant, intervalEnd);
if (endCoverage < minimumCoverage) {
minimumCoverage = endCoverage;
coveragePosition = intervalEnd;
}
if (coverageQuadratic > 0D) {
double vertex = clamp(-coverageLinear / (2D * coverageQuadratic), intervalStart, intervalEnd);
double vertexCoverage = quadraticValue(coverageQuadratic, coverageLinear, coverageConstant, vertex);
if (vertexCoverage < minimumCoverage) {
minimumCoverage = vertexCoverage;
coveragePosition = vertex;
}
}
if (minimumCoverage > 0D) {
return null;
}
double uncovered = distancePosition;
double covered = coveragePosition;
for (int iteration = 0; iteration < 40; iteration++) {
double middle = (uncovered + covered) * 0.5D;
if (quadraticValue(coverageQuadratic, coverageLinear, coverageConstant, middle) <= 0D) {
covered = middle;
} else {
uncovered = middle;
}
}
return new ClosestPoint(
StrictMath.max(0D, quadraticValue(
distanceQuadratic,
distanceLinear,
distanceConstant,
covered
)),
segmentStartAlong + segmentAlongSpan * covered
);
}
private static double nextCrossing(
double start,
double delta,
double boundary,
double cursor,
double currentNext
) {
if (delta == 0D) {
return currentNext;
}
double crossing = (boundary - start) / delta;
return crossing > cursor && crossing < currentNext ? crossing : currentNext;
}
private static double quadraticValue(double quadratic, double linear, double constant, double value) {
return (quadratic * value + linear) * value + constant;
}
private static double clamp(double value, double minimum, double maximum) {
return StrictMath.max(minimum, StrictMath.min(maximum, value));
}
private static double pointRectangleDistanceSquared(
double x,
double z,
double minimumX,
double minimumZ,
double maximumX,
double maximumZ
) {
double deltaX = x < minimumX ? minimumX - x : StrictMath.max(0.0, x - maximumX);
double deltaZ = z < minimumZ ? minimumZ - z : StrictMath.max(0.0, z - maximumZ);
return squared(deltaX) + squared(deltaZ);
}
private static double squared(double value) {
return value * value;
}
private static Map<Long, List<RiverReach>> createSpatialIndex(List<RiverReach> reaches) {
HashMap<Long, Set<RiverReach>> mutable = new HashMap<>();
for (RiverReach reach : reaches) {
double radius = reach.width() * 0.5 + reach.bankWidth();
RiverPolyline polyline = reach.polyline();
for (int point = 0; point < polyline.size() - 1; point++) {
int minimumBucketX = bucket(StrictMath.min(polyline.x(point), polyline.x(point + 1)) - radius);
int maximumBucketX = bucket(StrictMath.max(polyline.x(point), polyline.x(point + 1)) + radius);
int minimumBucketZ = bucket(StrictMath.min(polyline.z(point), polyline.z(point + 1)) - radius);
int maximumBucketZ = bucket(StrictMath.max(polyline.z(point), polyline.z(point + 1)) + radius);
for (int bucketX = minimumBucketX; bucketX <= maximumBucketX; bucketX++) {
for (int bucketZ = minimumBucketZ; bucketZ <= maximumBucketZ; bucketZ++) {
mutable.computeIfAbsent(bucketKey(bucketX, bucketZ), ignored -> new LinkedHashSet<>()).add(reach);
}
}
}
}
HashMap<Long, List<RiverReach>> immutable = new HashMap<>(mutable.size());
for (Map.Entry<Long, Set<RiverReach>> entry : mutable.entrySet()) {
immutable.put(entry.getKey(), List.copyOf(entry.getValue()));
}
return Map.copyOf(immutable);
}
private static Map<RiverEdgeId, RiverReach> indexById(List<RiverReach> reaches) {
HashMap<RiverEdgeId, RiverReach> indexed = new HashMap<>(reaches.size());
for (RiverReach reach : reaches) {
RiverReach previous = indexed.put(reach.id(), reach);
if (previous != null) {
throw new IllegalArgumentException("River tile cannot contain duplicate reach IDs");
}
}
return Map.copyOf(indexed);
}
private List<RiverReach> indexedReaches(double x, double z) {
return spatialIndex.getOrDefault(bucketKey(bucket(x), bucket(z)), List.of());
}
private List<RiverReach> indexedReaches(
double queryMinimumX,
double queryMinimumZ,
double queryMaximumX,
double queryMaximumZ
) {
LinkedHashSet<RiverReach> indexed = new LinkedHashSet<>();
int minimumBucketX = bucket(queryMinimumX);
int maximumBucketX = bucket(StrictMath.nextDown(queryMaximumX));
int minimumBucketZ = bucket(queryMinimumZ);
int maximumBucketZ = bucket(StrictMath.nextDown(queryMaximumZ));
if (minimumBucketX == maximumBucketX && minimumBucketZ == maximumBucketZ) {
return spatialIndex.getOrDefault(
bucketKey(minimumBucketX, minimumBucketZ),
List.of()
);
}
for (int bucketX = minimumBucketX; bucketX <= maximumBucketX; bucketX++) {
for (int bucketZ = minimumBucketZ; bucketZ <= maximumBucketZ; bucketZ++) {
indexed.addAll(spatialIndex.getOrDefault(bucketKey(bucketX, bucketZ), List.of()));
}
}
return List.copyOf(indexed);
}
private List<RiverReach> indexedReachesInclusive(
double queryMinimumX,
double queryMinimumZ,
double queryMaximumX,
double queryMaximumZ
) {
int minimumBucketX = bucket(queryMinimumX);
int maximumBucketX = bucket(queryMaximumX);
int minimumBucketZ = bucket(queryMinimumZ);
int maximumBucketZ = bucket(queryMaximumZ);
if (minimumBucketX == maximumBucketX && minimumBucketZ == maximumBucketZ) {
return indexedReaches(queryMinimumX, queryMinimumZ);
}
LinkedHashSet<RiverReach> indexed = new LinkedHashSet<>();
for (int bucketX = minimumBucketX; bucketX <= maximumBucketX; bucketX++) {
for (int bucketZ = minimumBucketZ; bucketZ <= maximumBucketZ; bucketZ++) {
indexed.addAll(spatialIndex.getOrDefault(bucketKey(bucketX, bucketZ), List.of()));
}
}
return List.copyOf(indexed);
}
private static int bucket(double coordinate) {
return (int) StrictMath.floor(coordinate / BUCKET_SIZE);
}
private static long bucketKey(int bucketX, int bucketZ) {
return ((long) bucketX << 32) ^ (bucketZ & 0xFFFFFFFFL);
}
private record Position(double x, double z, double alongReach) {
}
private record ClosestPoint(double distanceSquared, double alongReach) {
}
}
@@ -1,200 +0,0 @@
package art.arcane.iris.engine.river;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
public final class RiverTileCache implements AutoCloseable {
private final Object lock;
private final int maxCompletedEntries;
private final Map<TileKey, Entry> entries;
private final LinkedHashMap<TileKey, Entry> completedEntries;
private TileBuilder builder;
private boolean closed;
public RiverTileCache(int maxCompletedEntries, TileBuilder builder) {
if (maxCompletedEntries < 1) {
throw new IllegalArgumentException("River tile cache capacity must be positive");
}
this.maxCompletedEntries = maxCompletedEntries;
this.builder = Objects.requireNonNull(builder);
lock = new Object();
entries = new HashMap<>(maxCompletedEntries);
completedEntries = new LinkedHashMap<>(maxCompletedEntries, 0.75f, true);
}
public RiverTile get(int tileX, int tileZ) {
TileKey key = new TileKey(tileX, tileZ);
Entry entry;
TileBuilder activeBuilder;
boolean build;
synchronized (lock) {
requireOpen();
entry = entries.get(key);
if (entry == null) {
entry = new Entry();
entries.put(key, entry);
activeBuilder = builder;
build = true;
} else {
if (entry.completed) {
completedEntries.get(key);
}
activeBuilder = null;
build = false;
}
}
if (build) {
build(key, entry, activeBuilder);
}
return await(entry.future, key);
}
public int completedSize() {
synchronized (lock) {
return completedEntries.size();
}
}
public boolean isClosed() {
synchronized (lock) {
return closed;
}
}
public void clear() {
List<CompletableFuture<RiverTile>> invalidated;
synchronized (lock) {
requireOpen();
invalidated = clearLocked();
}
invalidate(invalidated, "River tile cache was cleared");
}
@Override
public void close() {
List<CompletableFuture<RiverTile>> invalidated;
synchronized (lock) {
if (closed) {
return;
}
closed = true;
builder = null;
invalidated = clearLocked();
}
invalidate(invalidated, "River tile cache was closed");
}
private void build(TileKey key, Entry entry, TileBuilder activeBuilder) {
try {
RiverTile tile = Objects.requireNonNull(
activeBuilder.build(key.tileX(), key.tileZ()),
"River tile builder returned null"
);
if (tile.tileX() != key.tileX() || tile.tileZ() != key.tileZ()) {
throw new IllegalStateException(
"River tile builder returned " + tile.tileX() + "," + tile.tileZ()
+ " for " + key.tileX() + "," + key.tileZ()
);
}
publishCompleted(key, entry, tile);
} catch (Throwable failure) {
removeFailed(key, entry);
entry.future.completeExceptionally(failure);
}
}
private void publishCompleted(TileKey key, Entry entry, RiverTile tile) {
synchronized (lock) {
if (closed || entries.get(key) != entry) {
entry.future.completeExceptionally(new IllegalStateException(
closed ? "River tile cache was closed" : "River tile cache entry was cleared"
));
return;
}
entry.completed = true;
completedEntries.put(key, entry);
while (completedEntries.size() > maxCompletedEntries) {
Map.Entry<TileKey, Entry> eldest = completedEntries.entrySet().iterator().next();
completedEntries.remove(eldest.getKey());
entries.remove(eldest.getKey(), eldest.getValue());
}
entry.future.complete(tile);
}
}
private void removeFailed(TileKey key, Entry entry) {
synchronized (lock) {
entries.remove(key, entry);
completedEntries.remove(key, entry);
}
}
private List<CompletableFuture<RiverTile>> clearLocked() {
ArrayList<CompletableFuture<RiverTile>> invalidated = new ArrayList<>(entries.size());
for (Entry entry : entries.values()) {
if (!entry.future.isDone()) {
invalidated.add(entry.future);
}
}
entries.clear();
completedEntries.clear();
return invalidated;
}
private void requireOpen() {
if (closed) {
throw new IllegalStateException("River tile cache is closed");
}
}
private static RiverTile await(CompletableFuture<RiverTile> future, TileKey key) {
try {
return future.join();
} catch (CompletionException failure) {
Throwable cause = failure.getCause();
if (cause instanceof InterruptedException) {
Thread.currentThread().interrupt();
}
if (cause instanceof RuntimeException runtimeException) {
throw runtimeException;
}
if (cause instanceof Error error) {
throw error;
}
throw new IllegalStateException(
"Failed to build river tile " + key.tileX() + "," + key.tileZ(),
cause
);
}
}
private static void invalidate(List<CompletableFuture<RiverTile>> futures, String message) {
for (CompletableFuture<RiverTile> future : futures) {
future.completeExceptionally(new IllegalStateException(message));
}
}
@FunctionalInterface
public interface TileBuilder {
RiverTile build(int tileX, int tileZ) throws Exception;
}
private record TileKey(int tileX, int tileZ) {
}
private static final class Entry {
private final CompletableFuture<RiverTile> future;
private boolean completed;
private Entry() {
future = new CompletableFuture<>();
}
}
}
@@ -1,211 +0,0 @@
package art.arcane.iris.engine.river;
import java.util.ArrayList;
import java.util.List;
public final class RiverTopologyComplexity {
public static final long MAXIMUM_SOURCE_WINDOW_CELLS = 65_536L;
public static final long MAXIMUM_ROUTE_SCAN_STEPS = 65_536L;
public static final long MAXIMUM_BUCKET_WRITES_PER_REACH = 1_048_576L;
public static final long MAXIMUM_TUNNEL_SAMPLE_COLUMNS = 65_536L;
private static final int SPATIAL_BUCKET_SIZE = 64;
private RiverTopologyComplexity() {
}
public static Estimate estimate(
int cellSize,
int tileCells,
double siteJitter,
int maxRouteReaches,
double maximumReachRadius,
double maximumWormOffset,
int maximumWormSegments
) {
double maximumEdgeAxisDelta = cellSize * (1D + siteJitter);
long geometryPaddingCells = 1L + ceilToLong(
(maximumReachRadius + maximumWormOffset) / cellSize
);
long targetWindowAxis = saturatedAdd(tileCells, saturatedMultiply(2L, geometryPaddingCells));
long sourceWindowAxis = saturatedAdd(
targetWindowAxis,
saturatedMultiply(2L, maxRouteReaches)
);
long sourceWindowCells = saturatedMultiply(sourceWindowAxis, sourceWindowAxis);
long maximumRouteScanSteps = saturatedMultiply(sourceWindowCells, maxRouteReaches);
double maximumSegmentSpan = maximumEdgeAxisDelta
+ maximumWormOffset * 2D
+ maximumReachRadius * 2D;
long maximumSegmentBucketAxis = saturatedAdd(
ceilToLong(maximumSegmentSpan / SPATIAL_BUCKET_SIZE),
1L
);
long maximumSegmentBucketCount = saturatedMultiply(
maximumSegmentBucketAxis,
maximumSegmentBucketAxis
);
long maximumBucketWritesPerReach = saturatedMultiply(
maximumSegmentBucketCount,
maximumWormSegments
);
return new Estimate(
geometryPaddingCells,
sourceWindowAxis,
sourceWindowCells,
maximumRouteScanSteps,
maximumSegmentBucketAxis,
maximumBucketWritesPerReach
);
}
public static void requireSafe(
int cellSize,
int tileCells,
double siteJitter,
int maxRouteReaches,
double maximumReachRadius,
double maximumWormOffset,
int maximumWormSegments
) {
Estimate estimate = estimate(
cellSize,
tileCells,
siteJitter,
maxRouteReaches,
maximumReachRadius,
maximumWormOffset,
maximumWormSegments
);
List<String> violations = estimate.violations();
if (!violations.isEmpty()) {
throw new IllegalArgumentException(String.join(" ", violations));
}
}
public static int tunnelHalo(
double maximumChannelWidth,
double maximumTunnelWidthMultiplier,
double tunnelMouthBlend
) {
if (!Double.isFinite(maximumChannelWidth) || maximumChannelWidth <= 0D
|| !Double.isFinite(maximumTunnelWidthMultiplier) || maximumTunnelWidthMultiplier < 1D
|| !Double.isFinite(tunnelMouthBlend) || tunnelMouthBlend < 0D) {
throw new IllegalArgumentException("River tunnel dimensions must be finite and valid");
}
return Math.max(
1,
(int) StrictMath.ceil(
maximumChannelWidth * 0.5D * maximumTunnelWidthMultiplier + tunnelMouthBlend
) + 1
);
}
public static long tunnelSampleColumns(
double maximumChannelWidth,
double maximumTunnelWidthMultiplier,
double tunnelMouthBlend
) {
long axis = 16L + 2L * tunnelHalo(
maximumChannelWidth,
maximumTunnelWidthMultiplier,
tunnelMouthBlend
);
return saturatedMultiply(axis, axis);
}
public static String tunnelPlanViolation(
double maximumChannelWidth,
double maximumTunnelWidthMultiplier,
double tunnelMouthBlend
) {
long columns = tunnelSampleColumns(
maximumChannelWidth,
maximumTunnelWidthMultiplier,
tunnelMouthBlend
);
if (columns <= MAXIMUM_TUNNEL_SAMPLE_COLUMNS) {
return null;
}
return "River tunnel planning may sample " + columns
+ " columns per generated chunk, above the safe limit of " + MAXIMUM_TUNNEL_SAMPLE_COLUMNS
+ "; reduce maxChannelWidth, tunnelWidthMultiplier.max, or tunnelMouthBlend.";
}
public static void requireSafeTunnelPlan(
double maximumChannelWidth,
double maximumTunnelWidthMultiplier,
double tunnelMouthBlend
) {
String violation = tunnelPlanViolation(
maximumChannelWidth,
maximumTunnelWidthMultiplier,
tunnelMouthBlend
);
if (violation != null) {
throw new IllegalArgumentException(violation);
}
}
private static long ceilToLong(double value) {
if (!Double.isFinite(value) || value >= Long.MAX_VALUE) {
return Long.MAX_VALUE;
}
if (value <= 0D) {
return 0L;
}
return (long) StrictMath.ceil(value);
}
private static long saturatedAdd(long first, long second) {
if (first > Long.MAX_VALUE - second) {
return Long.MAX_VALUE;
}
return first + second;
}
private static long saturatedMultiply(long first, long second) {
if (first == 0L || second == 0L) {
return 0L;
}
if (first > Long.MAX_VALUE / second) {
return Long.MAX_VALUE;
}
return first * second;
}
public record Estimate(
long geometryPaddingCells,
long sourceWindowAxis,
long sourceWindowCells,
long maximumRouteScanSteps,
long maximumSegmentBucketAxis,
long maximumBucketWritesPerReach
) {
public boolean safe() {
return violations().isEmpty();
}
public List<String> violations() {
ArrayList<String> violations = new ArrayList<>(3);
if (sourceWindowCells > MAXIMUM_SOURCE_WINDOW_CELLS) {
violations.add("River topology source window requires " + sourceWindowCells
+ " cells (" + sourceWindowAxis + " per axis), above the safe limit of "
+ MAXIMUM_SOURCE_WINDOW_CELLS
+ "; increase cellSize or reduce tileCells, maxRouteReaches, channel width, or bank width.");
}
if (maximumRouteScanSteps > MAXIMUM_ROUTE_SCAN_STEPS) {
violations.add("River topology route scan permits " + maximumRouteScanSteps
+ " source-to-reach steps, above the safe limit of " + MAXIMUM_ROUTE_SCAN_STEPS
+ "; reduce maxRouteReaches, tileCells, channel width, or bank width.");
}
if (maximumBucketWritesPerReach > MAXIMUM_BUCKET_WRITES_PER_REACH) {
violations.add("River topology spatial index may require " + maximumBucketWritesPerReach
+ " bucket writes for one reach (" + maximumSegmentBucketAxis
+ " buckets per segment axis), above the safe limit of "
+ MAXIMUM_BUCKET_WRITES_PER_REACH
+ "; reduce channel width, bank width, orderWidthFactor, worm maxOffset, or worm segments.");
}
return List.copyOf(violations);
}
}
}
@@ -1,81 +0,0 @@
package art.arcane.iris.engine.river;
import java.util.List;
public record RiverWorm(
String id,
long seed,
double weight,
double wavelength,
double detailWavelength,
double tortuosity,
double detailTortuosity,
double maxOffset,
int segments,
double widthMultiplier,
double bankMultiplier,
double depthMultiplier,
double bodyWavelength,
double bodyDetailWavelength,
double bodyDetailInfluence,
double widthVariation,
double bankVariation,
double depthVariation,
double roofVariation,
int branchCap,
double branchDecay,
double confluenceMultiplier,
double childChance,
double branchChildChance,
List<RiverWorm> children
) {
public RiverWorm {
if (id == null || !id.matches("[a-z0-9][a-z0-9_-]{0,63}")) {
throw new IllegalArgumentException("id must use 1 to 64 lowercase letters, digits, underscores, or hyphens");
}
requireRange(weight, 0.000001D, 1000000D, "weight");
requireRange(wavelength, 8D, 16384D, "wavelength");
requireRange(detailWavelength, 8D, 16384D, "detailWavelength");
requireRange(tortuosity, 0D, 1D, "tortuosity");
requireRange(detailTortuosity, 0D, 1D, "detailTortuosity");
requireRange(maxOffset, 0D, 1024D, "maxOffset");
if (segments < 1 || segments > 64) {
throw new IllegalArgumentException("segments must be between 1 and 64");
}
requireRange(widthMultiplier, 0.125D, 8D, "widthMultiplier");
requireRange(bankMultiplier, 0.125D, 8D, "bankMultiplier");
requireRange(depthMultiplier, 0.125D, 8D, "depthMultiplier");
requireRange(bodyWavelength, 8D, 16384D, "bodyWavelength");
requireRange(bodyDetailWavelength, 8D, 16384D, "bodyDetailWavelength");
requireRange(bodyDetailInfluence, 0D, 1D, "bodyDetailInfluence");
requireRange(widthVariation, 0D, 0.875D, "widthVariation");
requireRange(bankVariation, 0D, 0.875D, "bankVariation");
requireRange(depthVariation, 0D, 0.875D, "depthVariation");
requireRange(roofVariation, 0D, 0.875D, "roofVariation");
if (branchCap < 1 || branchCap > 8) {
throw new IllegalArgumentException("branchCap must be between 1 and 8");
}
requireRange(branchDecay, 0D, 1D, "branchDecay");
requireRange(confluenceMultiplier, 0D, 8D, "confluenceMultiplier");
requireRange(childChance, 0D, 1D, "childChance");
requireRange(branchChildChance, 0D, 1D, "branchChildChance");
if (children == null) {
throw new IllegalArgumentException("children must not be null");
}
if (children.size() > 16) {
throw new IllegalArgumentException("children must contain at most 16 profiles");
}
for (RiverWorm child : children) {
if (child == null) {
throw new IllegalArgumentException("children must not contain null profiles");
}
}
children = List.copyOf(children);
}
private static void requireRange(double value, double minimum, double maximum, String name) {
if (!Double.isFinite(value) || value < minimum || value > maximum) {
throw new IllegalArgumentException(name + " must be between " + minimum + " and " + maximum);
}
}
}
@@ -1,7 +0,0 @@
package art.arcane.iris.engine.river.cave;
public record CavePosition(int x, int y, int z) {
public CavePosition offset(int dx, int dy, int dz) {
return new CavePosition(x + dx, y + dy, z + dz);
}
}

Some files were not shown because too many files have changed in this diff Show More