Patching Cleanup

This commit is contained in:
Brian Neumann-Fopiano
2026-09-11 00:38:08 -04:00
parent bf12f30d26
commit 29a31a69e0
44 changed files with 1340 additions and 970 deletions
@@ -2,6 +2,7 @@ package art.arcane.iris.core.nms.v26_2_R1;
import art.arcane.iris.engine.DimensionStackContext;
import art.arcane.iris.engine.DimensionStackLayout;
import art.arcane.iris.nativegen.NativeGenerationWriteGuard;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.NativeFeatureGenerationPolicy;
import art.arcane.iris.engine.object.IrisBiome;
@@ -44,7 +45,6 @@ import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.IntBinaryOperator;
import java.util.function.Predicate;
/**
* Bukkit twin of the modded imported-feature stage. Same control, same semantics, same seeds: with
@@ -299,7 +299,7 @@ final class ImportedFeatureStage {
surfaceFirstFreeY,
floorFirstFreeY,
stackContext != null,
importedFeatureProtection(staticObjects, stackContext, staticMinY));
NativeGenerationWriteGuard.protectedPositions(staticObjects, stackContext, staticMinY));
try {
for (int stepIndex = 0; stepIndex < steps.size(); stepIndex++) {
@@ -317,30 +317,6 @@ final class ImportedFeatureStage {
}
}
private static Predicate<BlockPos> importedFeatureProtection(
IrisStaticObjectLayer staticObjects,
DimensionStackContext stackContext,
int minimumY
) {
Map<Long, DimensionStackLayout> layouts = new ConcurrentHashMap<>();
return position -> {
if (!staticObjects.isEmpty() && staticObjects.contains(
position.getX(), position.getY() - minimumY, position.getZ())) {
return true;
}
if (stackContext == null) {
return false;
}
long columnKey = ((long) position.getX() << 32)
^ (position.getZ() & 0xFFFFFFFFL);
DimensionStackLayout layout = layouts.computeIfAbsent(
columnKey,
ignored -> stackContext.sample(position.getX(), position.getZ())
);
return layout.isHostFeatureProtectedY(position.getY() - minimumY);
};
}
private void placeStep(WorldGenLevel level, FeatureTable table, FeatureSorter.StepFeatureData stepData,
Registry<PlacedFeature> featureRegistry, Set<Holder<Biome>> chunkBiomes,
ChunkGenerator owner, WorldgenRandom random, long decorationSeed,
@@ -7,7 +7,6 @@ import java.util.Optional;
import art.arcane.iris.nativegen.NativeGenerationWriteGuard;
import java.util.function.LongPredicate;
import art.arcane.iris.engine.DimensionStackContext;
import art.arcane.iris.engine.DimensionStackLayout;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineLifecycleTasks;
import art.arcane.iris.engine.framework.GenerationSessionException;
@@ -1125,7 +1124,8 @@ public class IrisChunkGenerator extends CustomChunkGenerator implements LongPred
chunkPos.x(), chunkPos.z(), error);
}
IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData());
Predicate<BlockPos> protectedPosition = nativeStructureProtection(staticObjects);
Predicate<BlockPos> protectedPosition = NativeGenerationWriteGuard.protectedPositions(
staticObjects, engine.getDimensionStackContext(), engine.getMinHeight());
WorldGenLevel boundedWorld = NativeStructureWorldgenAccess.create(
world, chunkPos, hostWorldgenSurfaceHeight(), hostWorldgenFloorHeight(),
engine.getDimensionStackContext() != null,
@@ -1216,8 +1216,9 @@ public class IrisChunkGenerator extends CustomChunkGenerator implements LongPred
hostWorldgenSurfaceHeight(),
hostWorldgenFloorHeight(),
engine.getDimensionStackContext() != null,
nativeStructureProtection(
engine.getDimension().getStaticObjectLayer(engine.getData())));
NativeGenerationWriteGuard.protectedPositions(
engine.getDimension().getStaticObjectLayer(engine.getData()),
engine.getDimensionStackContext(), engine.getMinHeight()));
world.setCurrentlyGenerating(() -> "Iris native structure " + structureId);
try {
NativeStructurePostProcessor.place(boundedWorld, structureManager, this, random, area, chunkPos,
@@ -1228,28 +1229,6 @@ public class IrisChunkGenerator extends CustomChunkGenerator implements LongPred
}
}
private Predicate<BlockPos> nativeStructureProtection(IrisStaticObjectLayer staticObjects) {
DimensionStackContext stackContext = engine.getDimensionStackContext();
int minimumY = engine.getMinHeight();
Map<Long, DimensionStackLayout> layouts = new ConcurrentHashMap<>();
return position -> {
if (!staticObjects.isEmpty() && staticObjects.contains(
position.getX(), position.getY() - minimumY, position.getZ())) {
return true;
}
if (stackContext == null) {
return false;
}
long columnKey = ((long) position.getX() << 32)
^ (position.getZ() & 0xFFFFFFFFL);
DimensionStackLayout layout = layouts.computeIfAbsent(
columnKey,
ignored -> stackContext.sample(position.getX(), position.getZ())
);
return layout.isHostFeatureProtectedY(position.getY() - minimumY);
};
}
private List<List<Structure>> structuresByStep(Registry<Structure> registry) {
int runtimeId = engine.getCacheID();
StructureStepCache cached = structureStepCache;
@@ -1,13 +1,16 @@
package art.arcane.iris.nativegen;
import art.arcane.iris.engine.DimensionStackContext;
import art.arcane.iris.engine.DimensionStackLayout;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.history.GenerationActivation;
import art.arcane.iris.engine.history.GenerationHistory;
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
import java.util.Optional;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisStaticObjectLayer;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.WorldGenLevel;
@@ -16,10 +19,17 @@ import net.minecraft.world.level.chunk.status.ChunkStatus;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.Optional;
import java.util.function.Predicate;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
public final class NativeGenerationWriteGuardTest {
@@ -29,6 +39,42 @@ public final class NativeGenerationWriteGuardTest {
Bootstrap.bootStrap();
}
@Test
public void staticObjectsProtectRelativeHeightBeforeStackSampling() {
IrisStaticObjectLayer objects = mock(IrisStaticObjectLayer.class);
DimensionStackContext stack = mock(DimensionStackContext.class);
when(objects.contains(-3, 74, -7)).thenReturn(true);
Predicate<BlockPos> protectedPositions = NativeGenerationWriteGuard.protectedPositions(objects, stack, -64);
assertTrue(protectedPositions.test(new BlockPos(-3, 10, -7)));
verifyNoInteractions(stack);
assertFalse(NativeGenerationWriteGuard.protectedPositions(objects, null, -64)
.test(new BlockPos(-3, 11, -7)));
}
@Test
public void emptyStaticLayerSkipsContainsAndCachesEachSignedColumn() {
IrisStaticObjectLayer objects = mock(IrisStaticObjectLayer.class);
DimensionStackContext stack = mock(DimensionStackContext.class);
DimensionStackLayout negative = mock(DimensionStackLayout.class);
DimensionStackLayout positive = mock(DimensionStackLayout.class);
when(objects.isEmpty()).thenReturn(true);
when(stack.sample(-1, -1)).thenReturn(negative);
when(stack.sample(-1, 1)).thenReturn(positive);
when(negative.isHostFeatureProtectedY(64)).thenReturn(true);
when(positive.isHostFeatureProtectedY(65)).thenReturn(true);
Predicate<BlockPos> protectedPositions = NativeGenerationWriteGuard.protectedPositions(objects, stack, -64);
assertTrue(protectedPositions.test(new BlockPos(-1, 0, -1)));
assertFalse(protectedPositions.test(new BlockPos(-1, 1, -1)));
assertTrue(protectedPositions.test(new BlockPos(-1, 1, 1)));
verify(stack, times(1)).sample(-1, -1);
verify(stack, times(1)).sample(-1, 1);
verify(objects, never()).contains(anyInt(), anyInt(), anyInt());
}
@Test
public void preTerrainSavedStampDistinguishesEarlierNativeStarts() {
IrisEngine engine = mock(IrisEngine.class);
@@ -1,14 +1,22 @@
package art.arcane.iris.nativegen;
import art.arcane.iris.engine.DimensionStackContext;
import art.arcane.iris.engine.DimensionStackLayout;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.engine.object.IrisStaticObjectLayer;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.status.ChunkPyramid;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;
public final class NativeGenerationWriteGuard {
private NativeGenerationWriteGuard() {
}
@@ -43,4 +51,28 @@ public final class NativeGenerationWriteGuard {
&& !chunk.getPersistedStatus().isOrAfter(stage)
&& !engine.getComplex().allowsMantleChunkWrite(chunk.getPos().x(), chunk.getPos().z());
}
public static Predicate<BlockPos> protectedPositions(
IrisStaticObjectLayer staticObjects,
DimensionStackContext stackContext,
int minimumY
) {
Map<Long, DimensionStackLayout> layouts = new ConcurrentHashMap<>();
return position -> {
if (!staticObjects.isEmpty() && staticObjects.contains(
position.getX(), position.getY() - minimumY, position.getZ())) {
return true;
}
if (stackContext == null) {
return false;
}
long columnKey = ((long) position.getX() << 32)
^ (position.getZ() & 0xFFFFFFFFL);
DimensionStackLayout layout = layouts.computeIfAbsent(
columnKey,
ignored -> stackContext.sample(position.getX(), position.getZ())
);
return layout.isHostFeatureProtectedY(position.getY() - minimumY);
};
}
}
@@ -18,13 +18,13 @@
package art.arcane.iris.modded;
import art.arcane.iris.platform.BlockStateKey;
import art.arcane.iris.spi.PlatformBlockState;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.tags.BlockTags;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.Property;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
@@ -52,7 +52,7 @@ public final class ModdedBlockState implements PlatformBlockState {
this.state = state;
this.parsedProperties = parsedProperties;
this.key = key;
this.namespace = parseNamespace(key);
this.namespace = BlockStateKey.namespace(key);
this.deferredPlacementKey = deferredPlacementKey;
}
@@ -101,44 +101,6 @@ public final class ModdedBlockState implements PlatformBlockState {
return Objects.hash(state, deferredPlacementKey);
}
private static String parseNamespace(String key) {
String base = key;
int bracket = base.indexOf('[');
if (bracket >= 0) {
base = base.substring(0, bracket);
}
int colon = base.indexOf(':');
return colon >= 0 ? base.substring(0, colon) : "minecraft";
}
private static String mergeProperty(String key, String name, String value) {
int bracket = key.indexOf('[');
if (bracket < 0) {
return key + "[" + name + "=" + value + "]";
}
String base = key.substring(0, bracket);
String body = key.substring(bracket + 1, key.lastIndexOf(']'));
LinkedHashMap<String, String> properties = new LinkedHashMap<>();
for (String entry : body.split(",")) {
int equals = entry.indexOf('=');
if (equals < 0) {
continue;
}
properties.put(entry.substring(0, equals).trim(), entry.substring(equals + 1).trim());
}
properties.put(name, value);
StringBuilder merged = new StringBuilder(base).append('[');
boolean first = true;
for (Map.Entry<String, String> property : properties.entrySet()) {
if (!first) {
merged.append(',');
}
merged.append(property.getKey()).append('=').append(property.getValue());
first = false;
}
return merged.append(']').toString();
}
@Override
public String key() {
return key;
@@ -338,7 +300,7 @@ public final class ModdedBlockState implements PlatformBlockState {
@Override
public PlatformBlockState withProperty(String name, String value) {
String merged = mergeProperty(key, name, value);
String merged = BlockStateKey.withProperty(key, name, value);
ModdedBlockState resolved = ModdedBlockResolution.strictParse(merged);
return withHandle(resolved.handle(), resolved.parsedProperties());
}
@@ -20,6 +20,7 @@ package art.arcane.iris.modded;
import art.arcane.iris.engine.DimensionStackContext;
import art.arcane.iris.engine.DimensionStackLayout;
import art.arcane.iris.nativegen.NativeGenerationWriteGuard;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.GenerationSessionLease;
import art.arcane.iris.engine.framework.NativeFeatureGenerationPolicy;
@@ -62,7 +63,6 @@ import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.IntBinaryOperator;
import java.util.function.Predicate;
/**
* Native placed-feature passthrough for one Iris dimension, gated on {@code importedFeatures.enabled}.
@@ -353,7 +353,7 @@ final class ModdedImportedFeatureStage {
surfaceFirstFreeY,
floorFirstFreeY,
stackContext != null,
importedFeatureProtection(staticObjects, stackContext, staticMinY));
NativeGenerationWriteGuard.protectedPositions(staticObjects, stackContext, staticMinY));
try {
for (int stepIndex = 0; stepIndex < steps.size(); stepIndex++) {
@@ -401,30 +401,6 @@ final class ModdedImportedFeatureStage {
}
}
private static Predicate<BlockPos> importedFeatureProtection(
IrisStaticObjectLayer staticObjects,
DimensionStackContext stackContext,
int minimumY
) {
Map<Long, DimensionStackLayout> layouts = new ConcurrentHashMap<>();
return position -> {
if (!staticObjects.isEmpty() && staticObjects.contains(
position.getX(), position.getY() - minimumY, position.getZ())) {
return true;
}
if (stackContext == null) {
return false;
}
long columnKey = ((long) position.getX() << 32)
^ (position.getZ() & 0xFFFFFFFFL);
DimensionStackLayout layout = layouts.computeIfAbsent(
columnKey,
ignored -> stackContext.sample(position.getX(), position.getZ())
);
return layout.isHostFeatureProtectedY(position.getY() - minimumY);
};
}
void evictRuntime(int runtimeIdentity) {
featureTables.remove(runtimeIdentity);
inertGenerations.remove(runtimeIdentity);
@@ -23,7 +23,6 @@ import net.minecraft.world.level.chunk.status.ChunkStatus;
import art.arcane.iris.nativegen.NativeGenerationWriteGuard;
import art.arcane.iris.engine.DimensionStackContext;
import art.arcane.iris.engine.DimensionStackLayout;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
@@ -397,7 +396,8 @@ final class ModdedNativeStructureStage {
chunkPos.x(), chunkPos.z(), error);
}
IrisStaticObjectLayer staticObjects = current.getDimension().getStaticObjectLayer(current.getData());
Predicate<BlockPos> protectedPosition = nativeStructureProtection(current, staticObjects);
Predicate<BlockPos> protectedPosition = NativeGenerationWriteGuard.protectedPositions(
staticObjects, current.getDimensionStackContext(), current.getMinHeight());
WorldGenLevel boundedWorld = ModdedNativeStructureWorldgenAccess.create(
world, chunkPos, worldgenSurfaceHeight(current, world.getMinY()), worldgenFloorHeight(current, world.getMinY()),
current.getDimensionStackContext() != null,
@@ -480,9 +480,9 @@ final class ModdedNativeStructureStage {
worldgenSurfaceHeight(current, world.getMinY()),
worldgenFloorHeight(current, world.getMinY()),
current.getDimensionStackContext() != null,
nativeStructureProtection(
current,
current.getDimension().getStaticObjectLayer(current.getData())));
NativeGenerationWriteGuard.protectedPositions(
current.getDimension().getStaticObjectLayer(current.getData()),
current.getDimensionStackContext(), current.getMinHeight()));
world.setCurrentlyGenerating(() -> "Iris native structure " + structureId);
try {
NativeStructurePostProcessor.place(
@@ -494,31 +494,6 @@ final class ModdedNativeStructureStage {
}
}
private static Predicate<BlockPos> nativeStructureProtection(
Engine engine,
IrisStaticObjectLayer staticObjects
) {
DimensionStackContext stackContext = engine.getDimensionStackContext();
int minimumY = engine.getMinHeight();
Map<Long, DimensionStackLayout> layouts = new ConcurrentHashMap<>();
return position -> {
if (!staticObjects.isEmpty() && staticObjects.contains(
position.getX(), position.getY() - minimumY, position.getZ())) {
return true;
}
if (stackContext == null) {
return false;
}
long columnKey = ((long) position.getX() << 32)
^ (position.getZ() & 0xFFFFFFFFL);
DimensionStackLayout layout = layouts.computeIfAbsent(
columnKey,
ignored -> stackContext.sample(position.getX(), position.getZ())
);
return layout.isHostFeatureProtectedY(position.getY() - minimumY);
};
}
private List<List<Structure>> structuresByStep(Registry<Structure> registry) {
StructureStepCache cached = structureStepCache;
if (cached != null && cached.registry() == registry) {
@@ -3,6 +3,7 @@ package art.arcane.iris.modded;
import art.arcane.iris.engine.framework.Engine;
import java.nio.charset.StandardCharsets;
import java.util.HexFormat;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
@@ -91,13 +92,7 @@ public final class ModdedWorldgenIds {
}
private static String encode(String value) {
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
StringBuilder encoded = new StringBuilder(bytes.length * 2);
for (byte current : bytes) {
encoded.append(Character.forDigit((current >>> 4) & 0xF, 16));
encoded.append(Character.forDigit(current & 0xF, 16));
}
return encoded.toString();
return HexFormat.of().formatHex(value.getBytes(StandardCharsets.UTF_8));
}
private static String decode(String value) {
@@ -0,0 +1,30 @@
package art.arcane.iris.modded;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class ModdedWorldgenIdsTest {
@Test
public void scopedIdsRetainExactUtf8HexEncoding() {
assertEquals("irisworldgen:packs/5061636b2f412d5f/dimensions/c389e5b1b1/preset",
ModdedWorldgenIds.presetRef("Pack/A-_", "É山"));
assertEquals("irisworldgen:packs//dimensions//preset", ModdedWorldgenIds.presetRef("", ""));
assertEquals("irisworldgen:packs/3f/dimensions/f0a08080/dimension_type",
ModdedWorldgenIds.dimensionTypeRef("\uD800", "\uD840\uDC00"));
assertEquals(new ModdedWorldgenIds.ScopedDimension("Pack/A-_", "É山"),
ModdedWorldgenIds.scopedDimensionType(
ModdedWorldgenIds.dimensionTypeRef("Pack/A-_", "É山")).orElseThrow());
}
@Test
public void malformedIdentifiersRetainRejectionBehavior() {
assertTrue(ModdedWorldgenIds.scopedDimensionType(
"irisworldgen:packs/7/dimensions/61/dimension_type").isEmpty());
assertTrue(ModdedWorldgenIds.scopedDimensionType(
"irisworldgen:packs/gg/dimensions/61/dimension_type").isEmpty());
assertThrows(NullPointerException.class, () -> ModdedWorldgenIds.presetRef(null, "dimension"));
}
}
+5 -1
View File
@@ -1,3 +1,4 @@
import art.arcane.volmit.packaging.PackagingMode
import org.gradle.api.file.CopySpec
import org.gradle.api.file.DeleteSpec
import org.gradle.api.file.FileSystemOperations
@@ -169,6 +170,7 @@ nmsBindings.each { key, value ->
transitive = false
}
testRuntimeOnly(project(':core'))
testRuntimeOnly(rootProject.libs.caffeine)
}
}
}
@@ -790,7 +792,9 @@ pluginPackaging {
policyName = 'Iris Bukkit'
stripDirectories = true
stripLocalVariables = true
releaseCompression = providers.gradleProperty('compactRelease').map { it.toBoolean() }.getOrElse(false)
releaseCompression = providers.gradleProperty('compactRelease')
.map { String value -> value.toBoolean() }
.getOrElse(!PackagingMode.resolve(providers).development())
prunePrefixes = ['art/arcane/volmlib/']
keepPrefixes = ['art/arcane/volmlib/util/matter/slices/']
shrinkRules = [file('packaging/shrink.pro')]
@@ -53,22 +53,16 @@ public record CompatFinding(CompatRegistry registry,
/** {@code <key> (<registry>): <action> <subjectType> <subjectKey> at <detail>} */
public String line() {
StringBuilder sb = new StringBuilder(96);
sb.append(key).append(" (").append(registry.label()).append("): ").append(action.label());
if (!subjectType.isEmpty()) {
sb.append(' ').append(subjectType);
}
if (!subjectKey.isEmpty()) {
sb.append(' ').append(subjectKey);
}
if (!detail.isEmpty()) {
sb.append(" at ").append(detail);
}
return sb.toString();
sb.append(key).append(" (").append(registry.label()).append("): ");
return appendSubject(sb);
}
/** The part after the key, for lines already grouped under the key. */
public String subjectLine() {
StringBuilder sb = new StringBuilder(64);
return appendSubject(new StringBuilder(64));
}
private String appendSubject(StringBuilder sb) {
sb.append(action.label());
if (!subjectType.isEmpty()) {
sb.append(' ').append(subjectType);
@@ -22,7 +22,6 @@ import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.engine.image.IrisImageMapCompiler;
import art.arcane.iris.engine.object.IrisImage;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KSet;
import art.arcane.volmlib.util.data.KCache;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
@@ -143,7 +142,7 @@ public class ImageResourceLoader extends ResourceLoader<IrisImage> {
for (File file : listedFiles) {
if (file.isFile() && file.getName().endsWith(".png")) {
m.add(prefix + file.getName().replaceAll("\\Q.png\\E", ""));
m.add(prefix + file.getName().replace(".png", ""));
} else if (file.isDirectory()) {
getPNGFiles(file, prefix + file.getName() + "/", m, visitedDirectories);
}
@@ -164,8 +163,7 @@ public class ImageResourceLoader extends ResourceLoader<IrisImage> {
getPNGFiles(i, "", m, visitedDirectories);
}
KList<String> v = new KList<>(m);
possibleKeys = v.toArray(new String[0]);
possibleKeys = m.toArray(new String[0]);
return possibleKeys;
}
@@ -21,7 +21,6 @@ package art.arcane.iris.core.loader;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.engine.object.matter.IrisMatterObject;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KSet;
import art.arcane.volmlib.util.data.KCache;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
@@ -84,7 +83,7 @@ public class MatterObjectResourceLoader extends ResourceLoader<IrisMatterObject>
for (File file : listedFiles) {
if (file.isFile() && file.getName().endsWith(".mat")) {
m.add(prefix + file.getName().replaceAll("\\Q.mat\\E", ""));
m.add(prefix + file.getName().replace(".mat", ""));
} else if (file.isDirectory()) {
findMatFiles(file, prefix + file.getName() + "/", m, visitedDirectories);
}
@@ -104,8 +103,7 @@ public class MatterObjectResourceLoader extends ResourceLoader<IrisMatterObject>
findMatFiles(folder, "", m, visitedDirectories);
}
KList<String> v = new KList<>(m);
possibleKeys = v.toArray(new String[0]);
possibleKeys = m.toArray(new String[0]);
return possibleKeys;
}
@@ -397,12 +397,11 @@ public class ResourceLoader<T extends IrisRegistrant> implements MeteredCache {
HashSet<String> m = new HashSet<>();
for (File i : files) {
for (File j : matchAllFiles(i, (f) -> f.getName().endsWith(".json"))) {
m.add(i.toURI().relativize(j.toURI()).getPath().replaceAll("\\Q.json\\E", ""));
m.add(i.toURI().relativize(j.toURI()).getPath().replace(".json", ""));
}
}
KList<String> v = new KList<>(m);
possibleKeys = v.toArray(new String[0]);
possibleKeys = m.toArray(new String[0]);
return possibleKeys;
}
@@ -345,20 +345,7 @@ public final class StructureTransactionWriter {
if (owner != null) {
verifyRecoveryClaim(transactionRoot, token.transactionId(), owner);
}
Path journalPath = recoveryJournalPath(transactionRoot);
if (journalPath == null || !files.isRegularFile(journalPath)) {
throw new IOException("Missing prepared removal journal at " + transactionRoot);
}
StructureTransactionJournal journal;
try {
journal = StructureTransactionJournal.fromJson(readBoundedBytes(
journalPath,
MAX_STRUCTURE_STATE_BYTES,
"Prepared removal journal"
));
} catch (RuntimeException e) {
throw new IOException("Invalid prepared removal journal at " + journalPath, e);
}
StructureTransactionJournal journal = readPreparedRemovalJournal(transactionRoot);
if (!journal.transactionId().equals(token.transactionId())) {
throw new IOException("Prepared removal journal id does not match " + token.transactionId());
}
@@ -973,20 +960,7 @@ public final class StructureTransactionWriter {
}
private void verifyPreparedRemovalAuthority(Path transactionRoot, UUID transactionId) throws IOException {
Path journalPath = recoveryJournalPath(transactionRoot);
if (journalPath == null || !files.isRegularFile(journalPath)) {
throw new IOException("Missing prepared removal journal at " + transactionRoot);
}
StructureTransactionJournal journal;
try {
journal = StructureTransactionJournal.fromJson(readBoundedBytes(
journalPath,
MAX_STRUCTURE_STATE_BYTES,
"Prepared removal journal"
));
} catch (RuntimeException e) {
throw new IOException("Invalid prepared removal journal at " + journalPath, e);
}
StructureTransactionJournal journal = readPreparedRemovalJournal(transactionRoot);
if (!journal.transactionId().equals(transactionId)
|| journal.phase() != StructureTransactionJournal.Phase.PREPARED) {
throw new IOException("Prepared removal journal does not match its datapack coordinator");
@@ -1015,6 +989,22 @@ public final class StructureTransactionWriter {
}
}
private StructureTransactionJournal readPreparedRemovalJournal(Path transactionRoot) throws IOException {
Path journalPath = recoveryJournalPath(transactionRoot);
if (journalPath == null || !files.isRegularFile(journalPath)) {
throw new IOException("Missing prepared removal journal at " + transactionRoot);
}
try {
return StructureTransactionJournal.fromJson(readBoundedBytes(
journalPath,
MAX_STRUCTURE_STATE_BYTES,
"Prepared removal journal"
));
} catch (RuntimeException e) {
throw new IOException("Invalid prepared removal journal at " + journalPath, e);
}
}
private RecoveryClaim readRecoveryClaim(Path claimPath) throws IOException {
if (!files.isRegularFile(claimPath)) {
throw new IOException("Invalid external recovery claim " + claimPath);
@@ -1,6 +1,5 @@
package art.arcane.iris.engine.data.cache;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Objects;
import java.util.function.Function;
@@ -35,9 +34,7 @@ public final class LazyBoundedCache<K, V> {
}
entries.put(requiredKey, computed);
if (entries.size() > maximumSize) {
Iterator<K> iterator = entries.keySet().iterator();
iterator.next();
iterator.remove();
entries.sequencedKeySet().removeFirst();
}
return computed;
}
@@ -2692,15 +2692,7 @@ public final class GenerationSemanticIndex {
}
}
}
byte[] body = bodyBytes.toByteArray();
CRC32 checksum = new CRC32();
checksum.update(body);
ByteArrayOutputStream encodedBytes = new ByteArrayOutputStream(body.length + CHECKSUM_BYTES);
encodedBytes.write(body);
try (DataOutputStream output = new DataOutputStream(encodedBytes)) {
output.writeInt((int) checksum.getValue());
}
return encodedBytes.toByteArray();
return withChecksum(bodyBytes.toByteArray());
}
private record Summary(
@@ -283,26 +283,12 @@ final class HydrologyCaveCourseFilter {
|| !compatibleSharedCandidates(firstCandidate, secondCandidate, coursesById)) {
continue;
}
LinkedHashMap<CavePosition, HydrologyCaveAction> firstActions = null;
LinkedHashMap<CavePosition, HydrologyCaveAction> secondActions = null;
for (Map.Entry<CavePosition, HydrologyCaveAction> entry : first.actions().entrySet()) {
HydrologyCaveAction secondAction = second.actions().get(entry.getKey());
if (secondAction == null || secondAction == entry.getValue()) {
TerminalActions actions = alignTerminalActions(first.actions(), second.actions());
if (actions == null) {
continue;
}
HydrologyCaveAction sharedAction = sharedTerminalAction(entry.getValue(), secondAction);
if (firstActions == null) {
firstActions = new LinkedHashMap<>(first.actions());
secondActions = new LinkedHashMap<>(second.actions());
}
firstActions.put(entry.getKey(), sharedAction);
secondActions.put(entry.getKey(), sharedAction);
}
if (firstActions == null) {
continue;
}
aligned.set(firstIndex, withActions(first, firstActions));
aligned.set(secondIndex, withActions(second, secondActions));
aligned.set(firstIndex, withActions(first, actions.first()));
aligned.set(secondIndex, withActions(second, actions.second()));
}
}
return List.copyOf(aligned);
@@ -336,26 +322,12 @@ final class HydrologyCaveCourseFilter {
&& !sharesTerminalGrotto(firstCourse, secondCourse, first)) {
continue;
}
LinkedHashMap<CavePosition, HydrologyCaveAction> firstActions = null;
LinkedHashMap<CavePosition, HydrologyCaveAction> secondActions = null;
for (Map.Entry<CavePosition, HydrologyCaveAction> entry : first.actions().entrySet()) {
HydrologyCaveAction secondAction = second.actions().get(entry.getKey());
if (secondAction == null || secondAction == entry.getValue()) {
TerminalActions actions = alignTerminalActions(first.actions(), second.actions());
if (actions == null) {
continue;
}
HydrologyCaveAction sharedAction = sharedTerminalAction(entry.getValue(), secondAction);
if (firstActions == null) {
firstActions = new LinkedHashMap<>(first.actions());
secondActions = new LinkedHashMap<>(second.actions());
}
firstActions.put(entry.getKey(), sharedAction);
secondActions.put(entry.getKey(), sharedAction);
}
if (firstActions == null) {
continue;
}
HydrologyCaveCandidate alignedFirst = withActions(first, firstActions);
HydrologyCaveCandidate alignedSecond = withActions(second, secondActions);
HydrologyCaveCandidate alignedFirst = withActions(first, actions.first());
HydrologyCaveCandidate alignedSecond = withActions(second, actions.second());
replaceExposureCandidate(exposureValidatedCandidates, first, alignedFirst);
replaceExposureCandidate(exposureValidatedCandidates, second, alignedSecond);
candidates.set(firstIndex, alignedFirst);
@@ -364,6 +336,28 @@ final class HydrologyCaveCourseFilter {
}
}
private TerminalActions alignTerminalActions(
Map<CavePosition, HydrologyCaveAction> first,
Map<CavePosition, HydrologyCaveAction> second
) {
LinkedHashMap<CavePosition, HydrologyCaveAction> firstActions = null;
LinkedHashMap<CavePosition, HydrologyCaveAction> secondActions = null;
for (Map.Entry<CavePosition, HydrologyCaveAction> entry : first.entrySet()) {
HydrologyCaveAction secondAction = second.get(entry.getKey());
if (secondAction == null || secondAction == entry.getValue()) {
continue;
}
HydrologyCaveAction sharedAction = sharedTerminalAction(entry.getValue(), secondAction);
if (firstActions == null) {
firstActions = new LinkedHashMap<>(first);
secondActions = new LinkedHashMap<>(second);
}
firstActions.put(entry.getKey(), sharedAction);
secondActions.put(entry.getKey(), sharedAction);
}
return firstActions == null ? null : new TerminalActions(firstActions, secondActions);
}
HydrologyCaveAction sharedTerminalAction(
HydrologyCaveAction first,
HydrologyCaveAction second
@@ -1251,6 +1245,12 @@ final class HydrologyCaveCourseFilter {
return List.copyOf(openings);
}
private record TerminalActions(
LinkedHashMap<CavePosition, HydrologyCaveAction> first,
LinkedHashMap<CavePosition, HydrologyCaveAction> second
) {
}
private record GeneratedChannelView(CaveVoxelView terrain) implements CaveVoxelView {
@Override
public boolean isInWorld(CavePosition position) {
@@ -1,5 +1,7 @@
package art.arcane.iris.engine.hydrology;
import art.arcane.iris.engine.hydrology.surface.SurfaceNoise;
final class HydrologyChannelGeometry {
private final HydrologyFootprintCompiler compiler;
@@ -149,7 +151,7 @@ final class HydrologyChannelGeometry {
if (maximum == 0) {
return 0;
}
double sampled = organicNoise(seed, x, z, 12);
double sampled = SurfaceNoise.value(seed, x, z, 12);
return (int) StrictMath.round(sampled * maximum);
}
@@ -222,13 +224,13 @@ final class HydrologyChannelGeometry {
)) * StrictMath.PI * 2D;
double firstLobe = 0.5D + 0.5D * StrictMath.sin(angle * 3D + firstPhase);
double secondLobe = 0.5D + 0.5D * StrictMath.sin(angle * 5D + secondPhase);
double coherent = signedOrganicNoise(
double coherent = SurfaceNoise.signed(
segment.courseId(),
worldX,
worldZ,
channelShape.roughnessWavelength()
);
double detail = signedOrganicNoise(
double detail = SurfaceNoise.signed(
HydrologyHash.mix(segment.courseId(), ORGANIC_SHAPE_SECOND_PHASE_SALT),
worldX,
worldZ,
@@ -262,14 +264,14 @@ final class HydrologyChannelGeometry {
}
double signedCross = (deltaX * -flowZ + deltaZ * flowX) / flowLength;
HydrologyPlannerSettings.ChannelShape channelShape = compiler.settings.geometry().surface();
double thalweg = signedOrganicNoise(
double thalweg = SurfaceNoise.signed(
HydrologyHash.mix(segment.courseId(), ORGANIC_BED_VARIATION_SALT),
worldX,
worldZ,
channelShape.roughnessWavelength()
) * shape.channelRadius() * channelShape.wallRoughness();
long bankSeed = HydrologyHash.mix(segment.courseId(), ORGANIC_SHAPE_SECOND_PHASE_SALT);
double bankNoise = signedOrganicNoise(
double bankNoise = SurfaceNoise.signed(
bankSeed,
worldX,
worldZ,
@@ -297,10 +299,6 @@ final class HydrologyChannelGeometry {
return compiler.settings.geometry().surface();
}
double signedOrganicNoise(long seed, int x, int z, int scale) {
return organicNoise(seed, x, z, scale) * 2D - 1D;
}
double deepPoolDistance(
FootprintLayerShape shape,
HydraulicSegment segment,
@@ -331,32 +329,4 @@ final class HydrologyChannelGeometry {
);
return normalizedDistance * radius / radialScale;
}
double organicNoise(long seed, int x, int z, int scale) {
int cellX = Math.floorDiv(x, scale);
int cellZ = Math.floorDiv(z, scale);
double localX = Math.floorMod(x, scale) / (double) scale;
double localZ = Math.floorMod(z, scale) / (double) scale;
double smoothX = localX * localX * (3D - 2D * localX);
double smoothZ = localZ * localZ * (3D - 2D * localZ);
double top = interpolate(
organicCorner(seed, cellX, cellZ),
organicCorner(seed, cellX + 1, cellZ),
smoothX
);
double bottom = interpolate(
organicCorner(seed, cellX, cellZ + 1),
organicCorner(seed, cellX + 1, cellZ + 1),
smoothX
);
return interpolate(top, bottom, smoothZ);
}
double organicCorner(long seed, int cellX, int cellZ) {
return HydrologyHash.unit(HydrologyHash.mix(seed, cellX, cellZ));
}
double interpolate(double first, double second, double progress) {
return first + (second - first) * progress;
}
}
@@ -4,6 +4,7 @@ import art.arcane.iris.engine.hydrology.surface.SurfaceFootprint;
import art.arcane.iris.engine.hydrology.surface.SurfaceBounds;
import art.arcane.iris.engine.hydrology.surface.SurfaceFootprintCompiler;
import art.arcane.iris.engine.hydrology.surface.SurfaceLayerColumn;
import art.arcane.iris.engine.hydrology.surface.SurfaceNoise;
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
@@ -763,7 +764,7 @@ final class HydrologyFootprintCompiler {
(normalized - thalwegFraction) / (1D - thalwegFraction)
);
int maximumDepth = Math.max(1, shape.fluidHead() - shape.bed());
double roughness = channelGeometry.signedOrganicNoise(
double roughness = SurfaceNoise.signed(
HydrologyHash.mix(segment.id(), HydrologyChannelGeometry.ORGANIC_BED_VARIATION_SALT),
worldX,
worldZ,
@@ -802,7 +803,7 @@ final class HydrologyFootprintCompiler {
return shape.bed();
}
int lowerExtent = shape.fluidHead() - shape.bed();
double roughness = channelGeometry.signedOrganicNoise(
double roughness = SurfaceNoise.signed(
HydrologyHash.mix(segment.id(), HydrologyChannelGeometry.ORGANIC_BED_VARIATION_SALT),
worldX,
worldZ,
@@ -833,7 +834,7 @@ final class HydrologyFootprintCompiler {
int upperExtent = shape.ceiling() - shape.fluidHead();
double extent = upperExtent * scale;
if (channelShape.ceilingRoughness() > 0D) {
double roughness = channelGeometry.signedOrganicNoise(
double roughness = SurfaceNoise.signed(
HydrologyHash.mix(segment.id(), HydrologyChannelGeometry.ORGANIC_CEILING_VARIATION_SALT),
worldX,
worldZ,
@@ -1,8 +1,5 @@
package art.arcane.iris.engine.hydrology.runtime;
import art.arcane.iris.engine.hydrology.policy.SurfaceRiverPolicy;
import art.arcane.iris.engine.hydrology.HydrologyHash;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
@@ -21,64 +18,39 @@ import art.arcane.iris.engine.hydrology.HydrologyTerrainSample;
import art.arcane.iris.engine.hydrology.HydrologyTile;
import art.arcane.iris.engine.hydrology.HydrologyTileCache;
import art.arcane.iris.engine.hydrology.HydrologyTileKey;
import art.arcane.iris.engine.hydrology.cave.CavePosition;
import art.arcane.iris.engine.hydrology.cave.HydrologyCavePlan;
import art.arcane.iris.engine.hydrology.policy.EffectiveRiverPolicy;
import art.arcane.iris.engine.hydrology.policy.RiverConfinement;
import art.arcane.iris.engine.hydrology.policy.RiverPolicyResolver;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisCoastalRiverGrottoConfig;
import art.arcane.iris.engine.object.IrisDeepFluidConfig;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisHydrology;
import art.arcane.iris.engine.object.IrisInlandRiverGrottoConfig;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisRiverChannelShapeConfig;
import art.arcane.iris.engine.object.IrisRiverDropShapeConfig;
import art.arcane.iris.engine.object.IrisRiverGeometryConfig;
import art.arcane.iris.engine.object.IrisRiverGrottoConfig;
import art.arcane.iris.engine.object.IrisRiverHydrology;
import art.arcane.iris.engine.object.IrisRiverInlandOutlet;
import art.arcane.iris.engine.object.IrisRiverMeanderConfig;
import art.arcane.iris.engine.object.IrisRiverMouthConfig;
import art.arcane.iris.engine.object.IrisRiverPolicy;
import art.arcane.iris.engine.object.IrisRiverProfile;
import art.arcane.iris.engine.object.IrisRiverRoutingConfig;
import art.arcane.iris.engine.object.IrisRiverRoutingMode;
import art.arcane.iris.engine.object.IrisSeaCaveConfig;
import art.arcane.iris.engine.object.IrisStyledRange;
import art.arcane.iris.engine.object.IrisSurfacePoolConfig;
import art.arcane.iris.engine.object.IrisSurfaceRiverBankConfig;
import art.arcane.iris.engine.object.IrisSurfaceRiverChannelConfig;
import art.arcane.iris.engine.object.IrisSurfaceRiverConfig;
import art.arcane.iris.engine.object.IrisSurfaceRiverErosionConfig;
import art.arcane.iris.engine.object.IrisSurfaceRiverFlowConfig;
import art.arcane.iris.engine.object.IrisSurfaceRiverPondConfig;
import art.arcane.iris.engine.object.IrisSurfaceRiverShapeConfig;
import art.arcane.iris.engine.object.IrisSurfaceRiverSourceConfig;
import art.arcane.iris.engine.object.IrisUndergroundRiverConfig;
import art.arcane.iris.engine.object.IrisUndergroundRiverSourceConfig;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.util.common.data.DataProvider;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.iris.util.project.stream.ProceduralStream;
import art.arcane.volmlib.util.math.RNG;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.nio.file.Path;
import java.util.function.IntConsumer;
import java.util.function.Predicate;
@@ -90,7 +62,6 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
private static final int BIOME_PATCH_SCALE = 32;
private static final long SURFACE_WIDTH_SALT = 0x5355524657494454L;
private static final long SURFACE_DEPTH_SALT = 0x5355524644455054L;
private static final long SURFACE_BLEND_SALT = 0x53555246424c454eL;
private static final long UNDERGROUND_LEVEL_SALT = 0x554e4445524c564cL;
private static final long UNDERGROUND_WIDTH_SALT = 0x554e444552574944L;
private static final long UNDERGROUND_DEPTH_SALT = 0x554e444552444550L;
@@ -121,7 +92,7 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
this.defaultProfileKey = profileKeys.isEmpty() ? "default" : profileKeys.iterator().next();
this.terrainSampleLock = new Object();
this.terrainSamples = new LinkedHashMap<>(MAXIMUM_TERRAIN_SAMPLES, 0.75F, true);
this.settings = createSettings(context.dimension(), hydrology, context::data);
this.settings = IrisHydrologySettingsCompiler.compile(context.dimension(), hydrology, context::data);
HydrologyGeometrySampler geometrySampler = geometrySampler(context, hydrology);
IrisHydrologyRoutingTerrainSampler.Sources terrainSources = new IrisHydrologyRoutingTerrainSampler.Sources(
this::createTerrainBasis,
@@ -851,463 +822,10 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
return range.stream(new RNG(context.seed() ^ salt), context.data());
}
static HydrologyPlannerSettings createSettings(
IrisDimension dimension,
IrisHydrology hydrology,
DataProvider data
) {
IrisRiverHydrology rivers = hydrology.getRivers();
IrisRiverRoutingConfig routing = rivers.getRouting();
IrisSurfaceRiverConfig surface = rivers.getSurface();
IrisSurfaceRiverSourceConfig surfaceSources = surface.getSources();
IrisSurfaceRiverChannelConfig channel = surface.getChannel();
IrisSurfaceRiverBankConfig banks = surface.getBanks();
IrisSurfaceRiverErosionConfig erosion = surface.getErosion();
IrisSurfaceRiverFlowConfig flow = surface.getFlow();
IrisRiverMouthConfig mouths = surface.getMouths();
IrisUndergroundRiverConfig underground = rivers.getUnderground();
IrisUndergroundRiverSourceConfig undergroundSources = underground.getSources();
int routeNodes = maximumRouteNodes(routing);
int minimumWorldY = dimension.getMinHeight();
boolean riversEnabled = rivers.isEnabled();
boolean surfaceEnabled = riversEnabled && surface.isEnabled();
boolean undergroundEnabled = riversEnabled && underground.isEnabled();
HydrologyPlannerSettings.Routing plannerRouting = new HydrologyPlannerSettings.Routing(
routing.getTileSize(),
routing.getSampleSpacing(),
routeNodes,
routing.getMaximumRouteLength(),
routing.getMinimumSurfaceCourseLength(),
routing.getMinimumUndergroundCourseLength(),
routing.getValleyPreference(),
routing.getUphillPenalty(),
routing.getSlopePenalty(),
routing.getConfluenceAttraction(),
routing.getLengthPreference(),
routing.getTributaries(),
new HydrologyPlannerSettings.Regional(
routing.getRegional().isEnabled(),
routing.getRegional().getSampleSpacing(),
routing.getRegional().getMinimumLength(),
routing.getRegional().getMaximumTrunks(),
routing.getRegional().getMaximumCachedBasins(),
routing.getRegional().getMaximumCachedStations(),
routing.getRegional().isCoastalChannels(),
routing.getRegional().getCoastalChannelChance(),
routing.getRegional().getMaximumCoastalIncision()
)
);
HydrologyPlannerSettings.Source plannerSurfaceSources = new HydrologyPlannerSettings.Source(
surfaceEnabled,
surfaceSources.getDensity(),
surfaceSources.getMinimumElevation() - minimumWorldY,
surfaceSources.getMinimumPerTile(),
maximumSources(surfaceSources.getDensity(), surfaceSources.getMinimumPerTile()),
surfaceSources.getMinimumSpacing()
);
HydrologyPlannerSettings.Surface plannerSurface = new HydrologyPlannerSettings.Surface(
surfaceEnabled,
plannerSurfaceSources,
minimumInt(channel.getWidth()),
maximumInt(channel.getWidth()),
minimumInt(channel.getDepth()),
maximumInt(channel.getDepth()),
channel.getMaximumIncision(),
banks.getShoreWidth(),
new HydrologyPlannerSettings.Banks(
channel.getSink(),
banks.getBlendSlope(),
banks.getMinimumBlendWidth(),
banks.getMaximumBlendWidth(),
channel.getRoughness(),
channel.getRoughnessWavelength(),
flow.getCascadeRun(),
flow.getWaterfallMinimumDrop(),
mouths.getFlareRatio(),
new HydrologyPlannerSettings.Inlet(
mouths.getInletLength(),
mouths.getInletDepth(),
mouths.getMaximumIncision(),
mouths.getInletCourseFraction(),
mouths.getInletRampSlope()
),
channel.getSpringWidthRatio(),
channel.getSpringLength(),
banks.isExposeCutStrata(),
new HydrologyPlannerSettings.Erosion(
erosion.isEnabled(),
erosion.getSmoothingRadius(),
erosion.getThalwegFraction(),
erosion.getBlendCurve(),
erosion.getBedNoise(),
erosion.getStyle(),
erosion.getTerraceSteps(),
erosion.getCliffFraction(),
erosion.getBedProfile(),
banks.getShoreRise(),
banks.getBlendBaseWidth(),
new HydrologyPlannerSettings.Excavation(
banks.getExcavation().getMaximumDepth(),
banks.getExcavation().getMaximumWidth(),
banks.getExcavation().getMaximumVolumePerBlock()
)
),
new HydrologyPlannerSettings.Ponds(
pond(surface.getPonds().getSource()),
pond(surface.getPonds().getTerminal())
),
new HydrologyPlannerSettings.Channel(
channel.getSmoothingRadius(),
channel.getOutlineMinimumRatio(),
channel.getOutlineMaximumRatio(),
channel.getSpringExtraDepth()
),
new HydrologyPlannerSettings.Flow(
flow.getWaterfallThalwegFraction(),
flow.getPlungeBasinMinimumDrop(),
flow.getPlungeBasinLengthRatio(),
flow.getPlungeBasinDepth()
)
)
);
HydrologyPlannerSettings.Hydraulics plannerHydraulics =
new HydrologyPlannerSettings.Hydraulics(flow.getWaterfallMinimumDrop());
HydrologyPlannerSettings.Source plannerUndergroundSources = new HydrologyPlannerSettings.Source(
undergroundEnabled,
undergroundSources.getDensity(),
Integer.MIN_VALUE,
undergroundSources.getMinimumPerTile(),
maximumSources(undergroundSources.getDensity(), undergroundSources.getMinimumPerTile()),
undergroundSources.getMinimumSpacing()
);
HydrologyPlannerSettings.Underground plannerUnderground = new HydrologyPlannerSettings.Underground(
undergroundEnabled,
plannerUndergroundSources,
minimumInt(underground.getFluidLevel()) - minimumWorldY,
maximumInt(underground.getFluidLevel()) - minimumWorldY,
minimumInt(underground.getChannelWidth()),
maximumInt(underground.getChannelWidth()),
minimumInt(underground.getDepth()),
maximumInt(underground.getDepth()),
minimumInt(underground.getHeadroom()),
maximumInt(underground.getHeadroom()),
underground.isConnectToExistingCaves(),
underground.getTributaries(),
underground.getMinimumRockCover(),
underground.getMinimumFloorCover(),
underground.getWideningSources()
);
IrisRiverGrottoConfig grottos = rivers.getGrottos();
IrisCoastalRiverGrottoConfig coastal = grottos.getCoastal();
IrisInlandRiverGrottoConfig inland = grottos.getInland();
boolean inlandEnabled = inland.isEnabled()
&& routing.getInlandOutlets().contains(IrisRiverInlandOutlet.SINKHOLE_GROTTO);
HydrologyPlannerSettings.Outlets plannerOutlets = new HydrologyPlannerSettings.Outlets(
routing.isOceanOutlets(),
grotto(coastal.isEnabled(), coastal.getHorizontalRadius(), coastal.getVerticalRadius(),
coastal.getHeadroom(), coastal.getMaximumVolume()),
grotto(inlandEnabled, inland.getHorizontalRadius(), inland.getVerticalRadius(),
inland.getHeadroom(), inland.getMaximumVolume()),
inlandEnabled && inland.isConnectSurfaceRivers(),
coastal.getCliffMinimumHeight() == null
? Math.max(4, coastal.getVerticalRadius())
: coastal.getCliffMinimumHeight(),
underground.getMouthLevelingDistance(),
mouths.getMaximumOceanApron(),
routing.getMaximumOutletsPerTile(),
routing.getMaximumCoastalOutletsPerTile(),
coastal.getCliffSlopeFactor()
);
HydrologyPlannerSettings.Geometry plannerGeometry = geometry(
rivers.getGeometry(),
surfaceShape(rivers.getGeometry().getSurface(), channel)
);
return new HydrologyPlannerSettings(
dimension.getFluidHeight(),
plannerRouting,
plannerSurface,
plannerHydraulics,
plannerUnderground,
plannerOutlets,
plannerGeometry,
deepFluids(hydrology.getDeepFluids(), minimumWorldY, routing, plannerRouting.refinementSpacing()),
surfacePools(hydrology.getSurfacePools()),
widestShoreBiomeWidth(dimension, data, banks.getShoreWidth()),
seaCaves(coastal),
surfacePolicyBounds(dimension, data)
);
}
static HydrologyPlannerSettings.SurfacePolicyBounds surfacePolicyBounds(IrisDimension dimension, DataProvider data) {
TreeMap<String, SurfaceRiverPolicy> policies = new TreeMap<>();
addSurfacePolicy(policies, "dimension", dimension.getRiverPolicy());
for (IrisRegion region : dimension.getAllRegions(data)) {
addSurfacePolicy(policies, "region:" + region.getLoadKey(), region.getRiverPolicy());
}
for (IrisBiome biome : dimension.getAllBiomes(data)) {
addSurfacePolicy(policies, "biome:" + biome.getLoadKey(), biome.getRiverPolicy());
}
long fingerprint = 0L;
int maximumSpacing = 0;
for (SurfaceRiverPolicy policy : policies.values()) {
fingerprint = HydrologyHash.mix(fingerprint, policy.hashCode(), HydrologyHash.text(policy.areaKey()));
maximumSpacing = Math.max(maximumSpacing, policy.sourceSpacing(0));
}
return new HydrologyPlannerSettings.SurfacePolicyBounds(fingerprint, maximumSpacing);
}
private static void addSurfacePolicy(Map<String, SurfaceRiverPolicy> policies, String key, IrisRiverPolicy policy) {
if (policy == null) {
return;
}
SurfaceRiverPolicy surface = policy.surfacePolicy(key);
if (surface.configured()) {
policies.put(key, surface);
}
}
/**
* The widest shore band any policy of the dimension asks for, whether as a shore biome band or as a
* geometric shore width, never below the dimension's geometric shore width, so the publication
* envelope reaches every column a river can give shore content to.
*/
static double widestShoreBiomeWidth(IrisDimension dimension, DataProvider data, double shoreWidth) {
double widest = Math.max(0D, shoreWidth);
widest = Math.max(widest, policyShoreReach(dimension.getRiverPolicy()));
for (IrisRegion region : dimension.getAllRegions(data)) {
widest = Math.max(widest, policyShoreReach(region.getRiverPolicy()));
}
for (IrisBiome biome : dimension.getAllBiomes(data)) {
widest = Math.max(widest, policyShoreReach(biome.getRiverPolicy()));
}
return widest;
}
private static double policyShoreReach(IrisRiverPolicy policy) {
if (policy == null) {
return 0D;
}
double shoreBiomeWidth = policy.getShoreBiomeWidth() == null ? 0D : policy.getShoreBiomeWidth();
double shoreWidth = policy.getShoreWidth() == null ? 0D : policy.getShoreWidth();
return Math.max(shoreBiomeWidth, shoreWidth);
}
private static List<HydrologyPlannerSettings.SurfacePool> surfacePools(List<IrisSurfacePoolConfig> configurations) {
ArrayList<HydrologyPlannerSettings.SurfacePool> pools = new ArrayList<>();
for (IrisSurfacePoolConfig configuration : configurations) {
pools.add(new HydrologyPlannerSettings.SurfacePool(
configuration.getId(),
configuration.getDensity() > 0D,
configuration.getDensity(),
configuration.getSpacing(),
configuration.getMinimumRadius(),
configuration.getMaximumRadius(),
configuration.getDepth(),
Math.min(64, maximumSources(configuration.getDensity(), 0)),
configuration.getBiome()
));
}
return List.copyOf(pools);
}
private static HydrologyPlannerSettings.Geometry geometry(
IrisRiverGeometryConfig geometry,
HydrologyPlannerSettings.ChannelShape surfaceShape
) {
IrisRiverMeanderConfig meanders = geometry.getMeanders();
IrisRiverDropShapeConfig drops = geometry.getDrops();
return new HydrologyPlannerSettings.Geometry(
new HydrologyPlannerSettings.Meanders(
meanders.getPrimaryWavelength(),
meanders.getDetailWavelength(),
meanders.getPrimaryStrength(),
meanders.getDetailStrength(),
meanders.getMaximumOffsetRatio(),
meanders.getSmoothingPasses(),
meanders.getMaximumTurnDegrees()
),
surfaceShape,
channelShape(geometry.getUnderground()),
channelShape(geometry.getGrottos()),
new HydrologyPlannerSettings.Drops(
drops.getCascadeRunPerBlock(),
drops.getCascadeExponent(),
drops.getMaximumCascadeStep(),
drops.getFlowWidthRatio(),
drops.getMaximumFlowDepth(),
drops.getBasinWidthRatio(),
drops.getMaximumBasinDepth(),
drops.getUndergroundCascadeRunPerBlock()
)
);
}
private static HydrologyPlannerSettings.ChannelShape channelShape(IrisRiverChannelShapeConfig shape) {
return new HydrologyPlannerSettings.ChannelShape(
shape.getBedRoundness(),
shape.getBedRoughness(),
shape.getWallRoughness(),
shape.getRoughnessWavelength(),
shape.getRadialBase(),
shape.getRadialMinimum(),
shape.getRadialMaximum(),
shape.getPrimaryLobeStrength(),
shape.getDetailLobeStrength(),
shape.getCeilingRoughness(),
shape.getAspectMinimum(),
shape.getAspectRange()
);
}
// The surface shape's roughness fields are optional and fall back to the channel roughness.
private static HydrologyPlannerSettings.ChannelShape surfaceShape(
IrisSurfaceRiverShapeConfig shape,
IrisSurfaceRiverChannelConfig channel
) {
return new HydrologyPlannerSettings.ChannelShape(
shape.getBedRoundness(),
shape.getBedRoughness() == null ? channel.getRoughness() : shape.getBedRoughness(),
shape.getWallRoughness() == null ? channel.getRoughness() : shape.getWallRoughness(),
shape.getRoughnessWavelength() == null ? channel.getRoughnessWavelength() : shape.getRoughnessWavelength(),
shape.getRadialBase(),
shape.getRadialMinimum(),
shape.getRadialMaximum(),
shape.getPrimaryLobeStrength(),
shape.getDetailLobeStrength(),
shape.getCeilingRoughness(),
shape.getAspectMinimum(),
shape.getAspectRange()
);
}
private static HydrologyPlannerSettings.Grotto grotto(
boolean enabled,
int horizontalRadius,
int verticalRadius,
int headroom,
int maximumVolume
) {
return new HydrologyPlannerSettings.Grotto(
enabled,
horizontalRadius,
verticalRadius,
headroom,
maximumVolume
);
}
// A sea cave is a coastal grotto without a river, so it needs the coastal grotto itself enabled.
private static HydrologyPlannerSettings.SeaCaves seaCaves(IrisCoastalRiverGrottoConfig coastal) {
IrisSeaCaveConfig seaCaves = coastal.getSeaCaves();
return new HydrologyPlannerSettings.SeaCaves(
coastal.isEnabled() && seaCaves.isEnabled(),
seaCaves.getMaximumPerTile(),
seaCaves.getMinimumSpacing(),
seaCaves.getMinimumCoastHeight(),
seaCaves.getDepth(),
seaCaves.getSweepJitterDegrees()
);
}
private static List<HydrologyPlannerSettings.DeepFluid> deepFluids(
List<IrisDeepFluidConfig> configurations,
int minimumWorldY,
IrisRiverRoutingConfig routing,
int refinementSpacing
) {
ArrayList<HydrologyPlannerSettings.DeepFluid> deepFluids = new ArrayList<>();
for (IrisDeepFluidConfig configuration : configurations) {
int radius = configuration.getHorizontalRadius();
int verticalRadius = configuration.getVerticalRadius();
int maximumVolume = boundedVolume(radius, verticalRadius, configuration.getHeadroom());
int maximumPerTile = Math.min(64, maximumSources(configuration.getDensity(), 0));
int maximumChannelLength = maximumDeepChannelLength(configuration, routing);
int minimumChannelLength = maximumChannelLength == 0
? 0
: Math.min(maximumChannelLength, Math.max(refinementSpacing, radius));
deepFluids.add(new HydrologyPlannerSettings.DeepFluid(
configuration.getId(),
configuration.getDensity() > 0D
&& (configuration.isContainedPools() || configuration.isShortChannels()),
configuration.getDensity(),
configuration.getSpacing(),
minimumInt(configuration.getHeight()) - minimumWorldY,
maximumInt(configuration.getHeight()) - minimumWorldY,
radius,
radius,
verticalRadius,
verticalRadius,
minimumChannelLength,
maximumChannelLength,
configuration.getChannelWidth(),
configuration.getDepth(),
configuration.getHeadroom(),
maximumVolume,
maximumPerTile,
configuration.isContainedPools(),
configuration.isShortChannels()
));
}
return List.copyOf(deepFluids);
}
static int maximumDeepChannelLength(
IrisDeepFluidConfig configuration,
IrisRiverRoutingConfig routing
) {
if (!configuration.isShortChannels()) {
return 0;
}
int configuredLength = Math.max(
HydrologyPlannerSettings.Routing.refinementSpacing(routing.getSampleSpacing()),
configuration.getSpacing() / 3);
return Math.min(configuredLength, routing.getTileSize() / 2);
}
private static int maximumRouteNodes(IrisRiverRoutingConfig routing) {
int halo = Math.min(routing.getMaximumRouteLength(), routing.getTileSize() / 2);
int alignedHalo = Math.floorDiv(halo, routing.getSampleSpacing()) * routing.getSampleSpacing();
int width = routing.getTileSize() / routing.getSampleSpacing() + 1
+ alignedHalo * 2 / routing.getSampleSpacing();
long nodes = (long) width * width;
if (nodes > 1_000_000L) {
throw new IllegalArgumentException("Hydrology routing lattice exceeds one million nodes.");
}
return (int) nodes;
}
private static int tileCoordinate(long blockCoordinate, int tileSize) {
return Math.toIntExact(Math.floorDiv(blockCoordinate, tileSize));
}
private static int maximumSources(double density, int minimumPerTile) {
int expected = (int) Math.ceil(Math.max(0D, density));
return Math.min(64, Math.max(minimumPerTile, expected));
}
private static int boundedVolume(int radius, int verticalRadius, int headroom) {
long diameter = radius * 2L + 1L;
long volume = diameter * diameter * (verticalRadius + headroom + 1L);
return (int) Math.min(1_048_576L, Math.max(64L, volume));
}
private static HydrologyPlannerSettings.Pond pond(IrisSurfaceRiverPondConfig config) {
return new HydrologyPlannerSettings.Pond(
config.isEnabled(),
config.getMinimumRadius(),
config.getMaximumRadius(),
config.getDepth()
);
}
private static int minimumInt(IrisStyledRange range) {
return (int) StrictMath.floor(Math.min(range.getMin(), range.getMax()));
}
private static int maximumInt(IrisStyledRange range) {
return (int) StrictMath.ceil(Math.max(range.getMin(), range.getMax()));
}
private static Set<String> profileKeys(List<IrisRiverProfile> profiles) {
LinkedHashSet<String> keys = new LinkedHashSet<>();
for (IrisRiverProfile profile : profiles) {
@@ -0,0 +1,499 @@
package art.arcane.iris.engine.hydrology.runtime;
import art.arcane.iris.engine.hydrology.HydrologyHash;
import art.arcane.iris.engine.hydrology.HydrologyPlannerSettings;
import art.arcane.iris.engine.hydrology.policy.SurfaceRiverPolicy;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisCoastalRiverGrottoConfig;
import art.arcane.iris.engine.object.IrisDeepFluidConfig;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisHydrology;
import art.arcane.iris.engine.object.IrisInlandRiverGrottoConfig;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisRiverChannelShapeConfig;
import art.arcane.iris.engine.object.IrisRiverDropShapeConfig;
import art.arcane.iris.engine.object.IrisRiverGeometryConfig;
import art.arcane.iris.engine.object.IrisRiverGrottoConfig;
import art.arcane.iris.engine.object.IrisRiverHydrology;
import art.arcane.iris.engine.object.IrisRiverInlandOutlet;
import art.arcane.iris.engine.object.IrisRiverMeanderConfig;
import art.arcane.iris.engine.object.IrisRiverMouthConfig;
import art.arcane.iris.engine.object.IrisRiverPolicy;
import art.arcane.iris.engine.object.IrisRiverRoutingConfig;
import art.arcane.iris.engine.object.IrisSeaCaveConfig;
import art.arcane.iris.engine.object.IrisStyledRange;
import art.arcane.iris.engine.object.IrisSurfacePoolConfig;
import art.arcane.iris.engine.object.IrisSurfaceRiverBankConfig;
import art.arcane.iris.engine.object.IrisSurfaceRiverChannelConfig;
import art.arcane.iris.engine.object.IrisSurfaceRiverConfig;
import art.arcane.iris.engine.object.IrisSurfaceRiverErosionConfig;
import art.arcane.iris.engine.object.IrisSurfaceRiverFlowConfig;
import art.arcane.iris.engine.object.IrisSurfaceRiverPondConfig;
import art.arcane.iris.engine.object.IrisSurfaceRiverShapeConfig;
import art.arcane.iris.engine.object.IrisSurfaceRiverSourceConfig;
import art.arcane.iris.engine.object.IrisUndergroundRiverConfig;
import art.arcane.iris.engine.object.IrisUndergroundRiverSourceConfig;
import art.arcane.iris.util.common.data.DataProvider;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
final class IrisHydrologySettingsCompiler {
private IrisHydrologySettingsCompiler() {
}
static HydrologyPlannerSettings compile(
IrisDimension dimension,
IrisHydrology hydrology,
DataProvider data
) {
IrisRiverHydrology rivers = hydrology.getRivers();
IrisRiverRoutingConfig routing = rivers.getRouting();
IrisSurfaceRiverConfig surface = rivers.getSurface();
IrisSurfaceRiverSourceConfig surfaceSources = surface.getSources();
IrisSurfaceRiverChannelConfig channel = surface.getChannel();
IrisSurfaceRiverBankConfig banks = surface.getBanks();
IrisSurfaceRiverErosionConfig erosion = surface.getErosion();
IrisSurfaceRiverFlowConfig flow = surface.getFlow();
IrisRiverMouthConfig mouths = surface.getMouths();
IrisUndergroundRiverConfig underground = rivers.getUnderground();
IrisUndergroundRiverSourceConfig undergroundSources = underground.getSources();
int routeNodes = maximumRouteNodes(routing);
int minimumWorldY = dimension.getMinHeight();
boolean riversEnabled = rivers.isEnabled();
boolean surfaceEnabled = riversEnabled && surface.isEnabled();
boolean undergroundEnabled = riversEnabled && underground.isEnabled();
HydrologyPlannerSettings.Routing plannerRouting = new HydrologyPlannerSettings.Routing(
routing.getTileSize(),
routing.getSampleSpacing(),
routeNodes,
routing.getMaximumRouteLength(),
routing.getMinimumSurfaceCourseLength(),
routing.getMinimumUndergroundCourseLength(),
routing.getValleyPreference(),
routing.getUphillPenalty(),
routing.getSlopePenalty(),
routing.getConfluenceAttraction(),
routing.getLengthPreference(),
routing.getTributaries(),
new HydrologyPlannerSettings.Regional(
routing.getRegional().isEnabled(),
routing.getRegional().getSampleSpacing(),
routing.getRegional().getMinimumLength(),
routing.getRegional().getMaximumTrunks(),
routing.getRegional().getMaximumCachedBasins(),
routing.getRegional().getMaximumCachedStations(),
routing.getRegional().isCoastalChannels(),
routing.getRegional().getCoastalChannelChance(),
routing.getRegional().getMaximumCoastalIncision()
)
);
HydrologyPlannerSettings.Source plannerSurfaceSources = new HydrologyPlannerSettings.Source(
surfaceEnabled,
surfaceSources.getDensity(),
surfaceSources.getMinimumElevation() - minimumWorldY,
surfaceSources.getMinimumPerTile(),
maximumSources(surfaceSources.getDensity(), surfaceSources.getMinimumPerTile()),
surfaceSources.getMinimumSpacing()
);
HydrologyPlannerSettings.Surface plannerSurface = new HydrologyPlannerSettings.Surface(
surfaceEnabled,
plannerSurfaceSources,
minimumInt(channel.getWidth()),
maximumInt(channel.getWidth()),
minimumInt(channel.getDepth()),
maximumInt(channel.getDepth()),
channel.getMaximumIncision(),
banks.getShoreWidth(),
new HydrologyPlannerSettings.Banks(
channel.getSink(),
banks.getBlendSlope(),
banks.getMinimumBlendWidth(),
banks.getMaximumBlendWidth(),
channel.getRoughness(),
channel.getRoughnessWavelength(),
flow.getCascadeRun(),
flow.getWaterfallMinimumDrop(),
mouths.getFlareRatio(),
new HydrologyPlannerSettings.Inlet(
mouths.getInletLength(),
mouths.getInletDepth(),
mouths.getMaximumIncision(),
mouths.getInletCourseFraction(),
mouths.getInletRampSlope()
),
channel.getSpringWidthRatio(),
channel.getSpringLength(),
banks.isExposeCutStrata(),
new HydrologyPlannerSettings.Erosion(
erosion.isEnabled(),
erosion.getSmoothingRadius(),
erosion.getThalwegFraction(),
erosion.getBlendCurve(),
erosion.getBedNoise(),
erosion.getStyle(),
erosion.getTerraceSteps(),
erosion.getCliffFraction(),
erosion.getBedProfile(),
banks.getShoreRise(),
banks.getBlendBaseWidth(),
new HydrologyPlannerSettings.Excavation(
banks.getExcavation().getMaximumDepth(),
banks.getExcavation().getMaximumWidth(),
banks.getExcavation().getMaximumVolumePerBlock()
)
),
new HydrologyPlannerSettings.Ponds(
pond(surface.getPonds().getSource()),
pond(surface.getPonds().getTerminal())
),
new HydrologyPlannerSettings.Channel(
channel.getSmoothingRadius(),
channel.getOutlineMinimumRatio(),
channel.getOutlineMaximumRatio(),
channel.getSpringExtraDepth()
),
new HydrologyPlannerSettings.Flow(
flow.getWaterfallThalwegFraction(),
flow.getPlungeBasinMinimumDrop(),
flow.getPlungeBasinLengthRatio(),
flow.getPlungeBasinDepth()
)
)
);
HydrologyPlannerSettings.Hydraulics plannerHydraulics =
new HydrologyPlannerSettings.Hydraulics(flow.getWaterfallMinimumDrop());
HydrologyPlannerSettings.Source plannerUndergroundSources = new HydrologyPlannerSettings.Source(
undergroundEnabled,
undergroundSources.getDensity(),
Integer.MIN_VALUE,
undergroundSources.getMinimumPerTile(),
maximumSources(undergroundSources.getDensity(), undergroundSources.getMinimumPerTile()),
undergroundSources.getMinimumSpacing()
);
HydrologyPlannerSettings.Underground plannerUnderground = new HydrologyPlannerSettings.Underground(
undergroundEnabled,
plannerUndergroundSources,
minimumInt(underground.getFluidLevel()) - minimumWorldY,
maximumInt(underground.getFluidLevel()) - minimumWorldY,
minimumInt(underground.getChannelWidth()),
maximumInt(underground.getChannelWidth()),
minimumInt(underground.getDepth()),
maximumInt(underground.getDepth()),
minimumInt(underground.getHeadroom()),
maximumInt(underground.getHeadroom()),
underground.isConnectToExistingCaves(),
underground.getTributaries(),
underground.getMinimumRockCover(),
underground.getMinimumFloorCover(),
underground.getWideningSources()
);
IrisRiverGrottoConfig grottos = rivers.getGrottos();
IrisCoastalRiverGrottoConfig coastal = grottos.getCoastal();
IrisInlandRiverGrottoConfig inland = grottos.getInland();
boolean inlandEnabled = inland.isEnabled()
&& routing.getInlandOutlets().contains(IrisRiverInlandOutlet.SINKHOLE_GROTTO);
HydrologyPlannerSettings.Outlets plannerOutlets = new HydrologyPlannerSettings.Outlets(
routing.isOceanOutlets(),
grotto(coastal.isEnabled(), coastal.getHorizontalRadius(), coastal.getVerticalRadius(),
coastal.getHeadroom(), coastal.getMaximumVolume()),
grotto(inlandEnabled, inland.getHorizontalRadius(), inland.getVerticalRadius(),
inland.getHeadroom(), inland.getMaximumVolume()),
inlandEnabled && inland.isConnectSurfaceRivers(),
coastal.getCliffMinimumHeight() == null
? Math.max(4, coastal.getVerticalRadius())
: coastal.getCliffMinimumHeight(),
underground.getMouthLevelingDistance(),
mouths.getMaximumOceanApron(),
routing.getMaximumOutletsPerTile(),
routing.getMaximumCoastalOutletsPerTile(),
coastal.getCliffSlopeFactor()
);
HydrologyPlannerSettings.Geometry plannerGeometry = geometry(
rivers.getGeometry(),
surfaceShape(rivers.getGeometry().getSurface(), channel)
);
return new HydrologyPlannerSettings(
dimension.getFluidHeight(),
plannerRouting,
plannerSurface,
plannerHydraulics,
plannerUnderground,
plannerOutlets,
plannerGeometry,
deepFluids(hydrology.getDeepFluids(), minimumWorldY, routing, plannerRouting.refinementSpacing()),
surfacePools(hydrology.getSurfacePools()),
widestShoreBiomeWidth(dimension, data, banks.getShoreWidth()),
seaCaves(coastal),
surfacePolicyBounds(dimension, data)
);
}
static HydrologyPlannerSettings.SurfacePolicyBounds surfacePolicyBounds(IrisDimension dimension, DataProvider data) {
TreeMap<String, SurfaceRiverPolicy> policies = new TreeMap<>();
addSurfacePolicy(policies, "dimension", dimension.getRiverPolicy());
for (IrisRegion region : dimension.getAllRegions(data)) {
addSurfacePolicy(policies, "region:" + region.getLoadKey(), region.getRiverPolicy());
}
for (IrisBiome biome : dimension.getAllBiomes(data)) {
addSurfacePolicy(policies, "biome:" + biome.getLoadKey(), biome.getRiverPolicy());
}
long fingerprint = 0L;
int maximumSpacing = 0;
for (SurfaceRiverPolicy policy : policies.values()) {
fingerprint = HydrologyHash.mix(fingerprint, policy.hashCode(), HydrologyHash.text(policy.areaKey()));
maximumSpacing = Math.max(maximumSpacing, policy.sourceSpacing(0));
}
return new HydrologyPlannerSettings.SurfacePolicyBounds(fingerprint, maximumSpacing);
}
private static void addSurfacePolicy(Map<String, SurfaceRiverPolicy> policies, String key, IrisRiverPolicy policy) {
if (policy == null) {
return;
}
SurfaceRiverPolicy surface = policy.surfacePolicy(key);
if (surface.configured()) {
policies.put(key, surface);
}
}
/**
* The widest shore band any policy of the dimension asks for, whether as a shore biome band or as a
* geometric shore width, never below the dimension's geometric shore width, so the publication
* envelope reaches every column a river can give shore content to.
*/
static double widestShoreBiomeWidth(IrisDimension dimension, DataProvider data, double shoreWidth) {
double widest = Math.max(0D, shoreWidth);
widest = Math.max(widest, policyShoreReach(dimension.getRiverPolicy()));
for (IrisRegion region : dimension.getAllRegions(data)) {
widest = Math.max(widest, policyShoreReach(region.getRiverPolicy()));
}
for (IrisBiome biome : dimension.getAllBiomes(data)) {
widest = Math.max(widest, policyShoreReach(biome.getRiverPolicy()));
}
return widest;
}
private static double policyShoreReach(IrisRiverPolicy policy) {
if (policy == null) {
return 0D;
}
double shoreBiomeWidth = policy.getShoreBiomeWidth() == null ? 0D : policy.getShoreBiomeWidth();
double shoreWidth = policy.getShoreWidth() == null ? 0D : policy.getShoreWidth();
return Math.max(shoreBiomeWidth, shoreWidth);
}
private static List<HydrologyPlannerSettings.SurfacePool> surfacePools(List<IrisSurfacePoolConfig> configurations) {
ArrayList<HydrologyPlannerSettings.SurfacePool> pools = new ArrayList<>();
for (IrisSurfacePoolConfig configuration : configurations) {
pools.add(new HydrologyPlannerSettings.SurfacePool(
configuration.getId(),
configuration.getDensity() > 0D,
configuration.getDensity(),
configuration.getSpacing(),
configuration.getMinimumRadius(),
configuration.getMaximumRadius(),
configuration.getDepth(),
Math.min(64, maximumSources(configuration.getDensity(), 0)),
configuration.getBiome()
));
}
return List.copyOf(pools);
}
private static HydrologyPlannerSettings.Geometry geometry(
IrisRiverGeometryConfig geometry,
HydrologyPlannerSettings.ChannelShape surfaceShape
) {
IrisRiverMeanderConfig meanders = geometry.getMeanders();
IrisRiverDropShapeConfig drops = geometry.getDrops();
return new HydrologyPlannerSettings.Geometry(
new HydrologyPlannerSettings.Meanders(
meanders.getPrimaryWavelength(),
meanders.getDetailWavelength(),
meanders.getPrimaryStrength(),
meanders.getDetailStrength(),
meanders.getMaximumOffsetRatio(),
meanders.getSmoothingPasses(),
meanders.getMaximumTurnDegrees()
),
surfaceShape,
channelShape(geometry.getUnderground()),
channelShape(geometry.getGrottos()),
new HydrologyPlannerSettings.Drops(
drops.getCascadeRunPerBlock(),
drops.getCascadeExponent(),
drops.getMaximumCascadeStep(),
drops.getFlowWidthRatio(),
drops.getMaximumFlowDepth(),
drops.getBasinWidthRatio(),
drops.getMaximumBasinDepth(),
drops.getUndergroundCascadeRunPerBlock()
)
);
}
private static HydrologyPlannerSettings.ChannelShape channelShape(IrisRiverChannelShapeConfig shape) {
return new HydrologyPlannerSettings.ChannelShape(
shape.getBedRoundness(),
shape.getBedRoughness(),
shape.getWallRoughness(),
shape.getRoughnessWavelength(),
shape.getRadialBase(),
shape.getRadialMinimum(),
shape.getRadialMaximum(),
shape.getPrimaryLobeStrength(),
shape.getDetailLobeStrength(),
shape.getCeilingRoughness(),
shape.getAspectMinimum(),
shape.getAspectRange()
);
}
// The surface shape's roughness fields are optional and fall back to the channel roughness.
private static HydrologyPlannerSettings.ChannelShape surfaceShape(
IrisSurfaceRiverShapeConfig shape,
IrisSurfaceRiverChannelConfig channel
) {
return new HydrologyPlannerSettings.ChannelShape(
shape.getBedRoundness(),
shape.getBedRoughness() == null ? channel.getRoughness() : shape.getBedRoughness(),
shape.getWallRoughness() == null ? channel.getRoughness() : shape.getWallRoughness(),
shape.getRoughnessWavelength() == null ? channel.getRoughnessWavelength() : shape.getRoughnessWavelength(),
shape.getRadialBase(),
shape.getRadialMinimum(),
shape.getRadialMaximum(),
shape.getPrimaryLobeStrength(),
shape.getDetailLobeStrength(),
shape.getCeilingRoughness(),
shape.getAspectMinimum(),
shape.getAspectRange()
);
}
private static HydrologyPlannerSettings.Grotto grotto(
boolean enabled,
int horizontalRadius,
int verticalRadius,
int headroom,
int maximumVolume
) {
return new HydrologyPlannerSettings.Grotto(
enabled,
horizontalRadius,
verticalRadius,
headroom,
maximumVolume
);
}
// A sea cave is a coastal grotto without a river, so it needs the coastal grotto itself enabled.
private static HydrologyPlannerSettings.SeaCaves seaCaves(IrisCoastalRiverGrottoConfig coastal) {
IrisSeaCaveConfig seaCaves = coastal.getSeaCaves();
return new HydrologyPlannerSettings.SeaCaves(
coastal.isEnabled() && seaCaves.isEnabled(),
seaCaves.getMaximumPerTile(),
seaCaves.getMinimumSpacing(),
seaCaves.getMinimumCoastHeight(),
seaCaves.getDepth(),
seaCaves.getSweepJitterDegrees()
);
}
private static List<HydrologyPlannerSettings.DeepFluid> deepFluids(
List<IrisDeepFluidConfig> configurations,
int minimumWorldY,
IrisRiverRoutingConfig routing,
int refinementSpacing
) {
ArrayList<HydrologyPlannerSettings.DeepFluid> deepFluids = new ArrayList<>();
for (IrisDeepFluidConfig configuration : configurations) {
int radius = configuration.getHorizontalRadius();
int verticalRadius = configuration.getVerticalRadius();
int maximumVolume = boundedVolume(radius, verticalRadius, configuration.getHeadroom());
int maximumPerTile = Math.min(64, maximumSources(configuration.getDensity(), 0));
int maximumChannelLength = maximumDeepChannelLength(configuration, routing);
int minimumChannelLength = maximumChannelLength == 0
? 0
: Math.min(maximumChannelLength, Math.max(refinementSpacing, radius));
deepFluids.add(new HydrologyPlannerSettings.DeepFluid(
configuration.getId(),
configuration.getDensity() > 0D
&& (configuration.isContainedPools() || configuration.isShortChannels()),
configuration.getDensity(),
configuration.getSpacing(),
minimumInt(configuration.getHeight()) - minimumWorldY,
maximumInt(configuration.getHeight()) - minimumWorldY,
radius,
radius,
verticalRadius,
verticalRadius,
minimumChannelLength,
maximumChannelLength,
configuration.getChannelWidth(),
configuration.getDepth(),
configuration.getHeadroom(),
maximumVolume,
maximumPerTile,
configuration.isContainedPools(),
configuration.isShortChannels()
));
}
return List.copyOf(deepFluids);
}
static int maximumDeepChannelLength(
IrisDeepFluidConfig configuration,
IrisRiverRoutingConfig routing
) {
if (!configuration.isShortChannels()) {
return 0;
}
int configuredLength = Math.max(
HydrologyPlannerSettings.Routing.refinementSpacing(routing.getSampleSpacing()),
configuration.getSpacing() / 3);
return Math.min(configuredLength, routing.getTileSize() / 2);
}
private static int maximumRouteNodes(IrisRiverRoutingConfig routing) {
int halo = Math.min(routing.getMaximumRouteLength(), routing.getTileSize() / 2);
int alignedHalo = Math.floorDiv(halo, routing.getSampleSpacing()) * routing.getSampleSpacing();
int width = routing.getTileSize() / routing.getSampleSpacing() + 1
+ alignedHalo * 2 / routing.getSampleSpacing();
long nodes = (long) width * width;
if (nodes > 1_000_000L) {
throw new IllegalArgumentException("Hydrology routing lattice exceeds one million nodes.");
}
return (int) nodes;
}
private static int maximumSources(double density, int minimumPerTile) {
int expected = (int) Math.ceil(Math.max(0D, density));
return Math.min(64, Math.max(minimumPerTile, expected));
}
private static int boundedVolume(int radius, int verticalRadius, int headroom) {
long diameter = radius * 2L + 1L;
long volume = diameter * diameter * (verticalRadius + headroom + 1L);
return (int) Math.min(1_048_576L, Math.max(64L, volume));
}
private static HydrologyPlannerSettings.Pond pond(IrisSurfaceRiverPondConfig config) {
return new HydrologyPlannerSettings.Pond(
config.isEnabled(),
config.getMinimumRadius(),
config.getMaximumRadius(),
config.getDepth()
);
}
private static int minimumInt(IrisStyledRange range) {
return (int) StrictMath.floor(Math.min(range.getMin(), range.getMax()));
}
private static int maximumInt(IrisStyledRange range) {
return (int) StrictMath.ceil(Math.max(range.getMin(), range.getMax()));
}
}
@@ -316,10 +316,7 @@ public final class IrisObjectIO {
private static void writeValidated(IrisObject self, OutputStream o) throws IOException {
DataOutputStream dos = new DataOutputStream(o);
dos.writeInt(self.w);
dos.writeInt(self.h);
dos.writeInt(self.d);
dos.writeUTF("Iris V2 IOB;");
writeHeader(self, dos);
Palette palette = buildPalette(self);
dos.writeShort(palette.size());
@@ -330,21 +327,13 @@ public final class IrisObjectIO {
dos.writeInt(self.blocks.size());
for (var entry : self.blocks) {
var i = entry.getKey();
dos.writeShort(i.getBlockX());
dos.writeShort(i.getBlockY());
dos.writeShort(i.getBlockZ());
dos.writeShort(palette.indexOf(entry.getValue().key()));
for (Map.Entry<IrisBlockVector, PlatformBlockState> entry : self.blocks) {
writeBlock(dos, palette, entry);
}
dos.writeInt(self.states.size());
for (var entry : self.states) {
var i = entry.getKey();
dos.writeShort(i.getBlockX());
dos.writeShort(i.getBlockY());
dos.writeShort(i.getBlockZ());
entry.getValue().toBinary(dos);
for (Map.Entry<IrisBlockVector, TileData> entry : self.states) {
writeState(dos, entry);
}
}
@@ -369,10 +358,7 @@ public final class IrisObjectIO {
public void execute() {
try {
DataOutputStream dos = new DataOutputStream(o);
dos.writeInt(self.w);
dos.writeInt(self.h);
dos.writeInt(self.d);
dos.writeUTF("Iris V2 IOB;");
writeHeader(self, dos);
Palette palette = buildPalette(self);
c += self.blocks.size();
@@ -387,22 +373,14 @@ public final class IrisObjectIO {
dos.writeInt(self.blocks.size());
for (var entry : self.blocks) {
var i = entry.getKey();
dos.writeShort(i.getBlockX());
dos.writeShort(i.getBlockY());
dos.writeShort(i.getBlockZ());
dos.writeShort(palette.indexOf(entry.getValue().key()));
for (Map.Entry<IrisBlockVector, PlatformBlockState> entry : self.blocks) {
writeBlock(dos, palette, entry);
++c;
}
dos.writeInt(self.states.size());
for (var entry : self.states) {
var i = entry.getKey();
dos.writeShort(i.getBlockX());
dos.writeShort(i.getBlockY());
dos.writeShort(i.getBlockZ());
entry.getValue().toBinary(dos);
for (Map.Entry<IrisBlockVector, TileData> entry : self.states) {
writeState(dos, entry);
++c;
}
} catch (IOException e) {
@@ -436,6 +414,31 @@ public final class IrisObjectIO {
throw ref.get();
}
private static void writeHeader(IrisObject self, DataOutputStream output) throws IOException {
output.writeInt(self.w);
output.writeInt(self.h);
output.writeInt(self.d);
output.writeUTF(V2_HEADER);
}
private static void writeBlock(DataOutputStream output, Palette palette,
Map.Entry<IrisBlockVector, PlatformBlockState> entry) throws IOException {
IrisBlockVector position = entry.getKey();
output.writeShort(position.getBlockX());
output.writeShort(position.getBlockY());
output.writeShort(position.getBlockZ());
output.writeShort(palette.indexOf(entry.getValue().key()));
}
private static void writeState(DataOutputStream output,
Map.Entry<IrisBlockVector, TileData> entry) throws IOException {
IrisBlockVector position = entry.getKey();
output.writeShort(position.getBlockX());
output.writeShort(position.getBlockY());
output.writeShort(position.getBlockZ());
entry.getValue().toBinary(output);
}
static void write(IrisObject self, File file) throws IOException {
if (file == null) {
return;
@@ -33,32 +33,27 @@ public final class TreeBlockResolver {
}
public static PlatformBlockState resolve(IrisProceduralTree tree, IrisData data, TreeBlockCanvas.Cell cell, TreeBlockCanvas.Vec pos, RNG paletteRng) {
switch (cell.role()) {
case TRUNK -> {
PlatformBlockState state = resolveBlock(tree.getTrunk(), tree.getTrunkPalette(), data, pos, paletteRng);
return finishTrunk(state, cell);
}
return switch (cell.role()) {
case TRUNK -> finishTrunk(resolveBlock(tree.getTrunk(), tree.getTrunkPalette(), data, pos, paletteRng), cell);
case SECONDARY_TRUNK -> {
PlatformBlockState state = resolveBlock(tree.getSecondaryTrunk(), tree.getSecondaryTrunkPalette(), data, pos, paletteRng);
if (state == null) {
state = resolveBlock(tree.getTrunk(), tree.getTrunkPalette(), data, pos, paletteRng);
}
return finishTrunk(state, cell);
}
case LEAF -> {
return resolveBlock(tree.getLeaves(), tree.getLeavesPalette(), data, pos, paletteRng);
yield finishTrunk(state, cell);
}
case LEAF -> resolveBlock(tree.getLeaves(), tree.getLeavesPalette(), data, pos, paletteRng);
case SECONDARY_LEAF -> {
PlatformBlockState state = resolveSecondaryLeaf(tree, data, pos, paletteRng);
if (state == null) {
state = resolveBlock(tree.getLeaves(), tree.getLeavesPalette(), data, pos, paletteRng);
}
return state;
yield state;
}
case DECORATOR -> {
int index = cell.decoratorIndex();
if (index < 0 || tree.getDecorators() == null || index >= tree.getDecorators().size()) {
return null;
yield null;
}
IrisTreeDecorator dec = tree.getDecorators().get(index);
PlatformBlockState state = resolveBlock(dec.getBlock(), dec.getPalette(), data, pos, paletteRng);
@@ -68,12 +63,10 @@ public final class TreeBlockResolver {
} catch (IllegalArgumentException ignored) {
}
}
return state;
}
default -> {
return null;
}
yield state;
}
default -> null;
};
}
private static PlatformBlockState finishTrunk(PlatformBlockState state, TreeBlockCanvas.Cell cell) {
@@ -93,7 +86,7 @@ public final class TreeBlockResolver {
}
private static PlatformBlockState resolveSecondaryLeaf(IrisProceduralTree tree, IrisData data, TreeBlockCanvas.Vec pos, RNG paletteRng) {
if (TreeTrunkBuilder.paletteSet(tree.getSecondaryLeavesPalette())) {
if (IrisProceduralBlocks.paletteSet(tree.getSecondaryLeavesPalette())) {
return tree.getSecondaryLeavesPalette().get(paletteRng, pos.x(), pos.y(), pos.z(), data);
}
if (tree.getWeightedSecondaryLeaves() != null && !tree.getWeightedSecondaryLeaves().isEmpty()) {
@@ -126,7 +119,7 @@ public final class TreeBlockResolver {
}
private static PlatformBlockState resolveBlock(String block, IrisMaterialPalette palette, IrisData data, TreeBlockCanvas.Vec pos, RNG paletteRng) {
if (TreeTrunkBuilder.paletteSet(palette)) {
if (IrisProceduralBlocks.paletteSet(palette)) {
return palette.get(paletteRng, pos.x(), pos.y(), pos.z(), data);
}
if (block != null && !block.isEmpty()) {
@@ -18,6 +18,7 @@
package art.arcane.iris.engine.object.tree;
import art.arcane.iris.engine.object.IrisProceduralBlocks;
import art.arcane.iris.engine.object.IrisProceduralTree;
import art.arcane.iris.engine.object.IrisTreeBranches;
import art.arcane.iris.engine.object.IrisTreeCanopy;
@@ -245,7 +246,7 @@ public final class TreeCanopyBuilder {
}
private static TreeBlockCanvas.Role resolveLeaf(IrisProceduralTree tree, RNG rng) {
boolean hasSecondary = TreeTrunkBuilder.paletteSet(tree.getSecondaryLeavesPalette())
boolean hasSecondary = IrisProceduralBlocks.paletteSet(tree.getSecondaryLeavesPalette())
|| (tree.getWeightedSecondaryLeaves() != null && !tree.getWeightedSecondaryLeaves().isEmpty())
|| (tree.getSecondaryLeaves() != null && !tree.getSecondaryLeaves().isEmpty());
if (!hasSecondary || tree.getSecondaryLeafFraction() <= 0.0) {
@@ -18,6 +18,7 @@
package art.arcane.iris.engine.object.tree;
import art.arcane.iris.engine.object.IrisProceduralBlocks;
import art.arcane.iris.engine.object.IrisProceduralTree;
import art.arcane.iris.engine.object.IrisTreeDecorator;
import art.arcane.volmlib.util.collection.KList;
@@ -40,7 +41,7 @@ public final class TreeDecoratorApplier {
for (int idx = 0; idx < decorators.size(); idx++) {
IrisTreeDecorator dec = decorators.get(idx);
boolean hasBlock = (dec.getBlock() != null && !dec.getBlock().isEmpty()) || TreeTrunkBuilder.paletteSet(dec.getPalette());
boolean hasBlock = (dec.getBlock() != null && !dec.getBlock().isEmpty()) || IrisProceduralBlocks.paletteSet(dec.getPalette());
if (!hasBlock) {
continue;
}
@@ -18,7 +18,7 @@
package art.arcane.iris.engine.object.tree;
import art.arcane.iris.engine.object.IrisMaterialPalette;
import art.arcane.iris.engine.object.IrisProceduralBlocks;
import art.arcane.iris.engine.object.IrisProceduralTree;
import art.arcane.iris.engine.object.IrisTreeAzimuthMode;
@@ -146,16 +146,12 @@ public final class TreeTrunkBuilder {
}
private static boolean hasSecondaryTrunk(IrisProceduralTree tree) {
if (paletteSet(tree.getSecondaryTrunkPalette())) {
if (IrisProceduralBlocks.paletteSet(tree.getSecondaryTrunkPalette())) {
return true;
}
return tree.getSecondaryTrunk() != null && !tree.getSecondaryTrunk().isEmpty();
}
static boolean paletteSet(IrisMaterialPalette palette) {
return palette != null && palette.getPalette() != null && !palette.getPalette().isEmpty();
}
private static double[] leanOffset(IrisProceduralTree tree, int y, int height) {
if (tree.getLeanAngle() == 0.0 || height <= 1) {
return new double[]{0.0, 0.0};
@@ -0,0 +1,47 @@
package art.arcane.iris.platform;
import java.util.LinkedHashMap;
import java.util.Map;
public final class BlockStateKey {
private BlockStateKey() {
}
public static String namespace(String key) {
String base = key;
int bracket = base.indexOf('[');
if (bracket >= 0) {
base = base.substring(0, bracket);
}
int colon = base.indexOf(':');
return colon >= 0 ? base.substring(0, colon) : "minecraft";
}
public static String withProperty(String key, String name, String value) {
int bracket = key.indexOf('[');
if (bracket < 0) {
return key + "[" + name + "=" + value + "]";
}
String base = key.substring(0, bracket);
String body = key.substring(bracket + 1, key.lastIndexOf(']'));
LinkedHashMap<String, String> properties = new LinkedHashMap<>();
for (String entry : body.split(",")) {
int equals = entry.indexOf('=');
if (equals < 0) {
continue;
}
properties.put(entry.substring(0, equals).trim(), entry.substring(equals + 1).trim());
}
properties.put(name, value);
StringBuilder merged = new StringBuilder(base).append('[');
boolean first = true;
for (Map.Entry<String, String> property : properties.entrySet()) {
if (!first) {
merged.append(',');
}
merged.append(property.getKey()).append('=').append(property.getValue());
first = false;
}
return merged.append(']').toString();
}
}
@@ -23,6 +23,7 @@ import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.nms.container.Pair;
import art.arcane.iris.core.service.ExternalDataSVC;
import art.arcane.iris.engine.object.IrisObjectRotation;
import art.arcane.iris.platform.BlockStateKey;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.IrisCustomData;
import art.arcane.iris.util.common.math.IrisBlockVector;
@@ -33,7 +34,6 @@ import org.bukkit.Tag;
import org.bukkit.block.BlockFace;
import org.bukkit.block.data.BlockData;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -84,7 +84,7 @@ public final class BukkitBlockState implements PlatformBlockState {
private BukkitBlockState(BlockData data, String key) {
this.data = data;
this.key = key;
this.namespace = parseNamespace(key);
this.namespace = BlockStateKey.namespace(key);
}
public static BukkitBlockState of(BlockData data) {
@@ -175,44 +175,6 @@ public final class BukkitBlockState implements PlatformBlockState {
return key.hashCode();
}
private static String parseNamespace(String key) {
String base = key;
int bracket = base.indexOf('[');
if (bracket >= 0) {
base = base.substring(0, bracket);
}
int colon = base.indexOf(':');
return colon >= 0 ? base.substring(0, colon) : "minecraft";
}
private static String mergeProperty(String key, String name, String value) {
int bracket = key.indexOf('[');
if (bracket < 0) {
return key + "[" + name + "=" + value + "]";
}
String base = key.substring(0, bracket);
String body = key.substring(bracket + 1, key.lastIndexOf(']'));
LinkedHashMap<String, String> properties = new LinkedHashMap<>();
for (String entry : body.split(",")) {
int equals = entry.indexOf('=');
if (equals < 0) {
continue;
}
properties.put(entry.substring(0, equals).trim(), entry.substring(equals + 1).trim());
}
properties.put(name, value);
StringBuilder merged = new StringBuilder(base).append('[');
boolean first = true;
for (Map.Entry<String, String> property : properties.entrySet()) {
if (!first) {
merged.append(',');
}
merged.append(property.getKey()).append('=').append(property.getValue());
first = false;
}
return merged.append(']').toString();
}
@Override
public String key() {
return key;
@@ -448,7 +410,7 @@ public final class BukkitBlockState implements PlatformBlockState {
public PlatformBlockState withProperty(String name, String value) {
if (data instanceof IrisCustomData custom) {
if (ExternalDataSVC.parseState(custom.getCustom()).getB().containsKey(name)) {
String merged = mergeProperty(key, name, value);
String merged = BlockStateKey.withProperty(key, name, value);
BlockData resolved = BukkitBlockResolution.resolveOrNull(merged);
if (!(resolved instanceof IrisCustomData)) {
throw new IllegalArgumentException("Cannot resolve custom block state " + merged);
@@ -457,11 +419,11 @@ public final class BukkitBlockState implements PlatformBlockState {
}
// Re-attach the custom identity (as the proxy's own merge/clone cases do) after
// editing the base block, so auto-waterlogging cannot turn custom blocks into vanilla.
String merged = mergeProperty(custom.getBase().getAsString(), name, value);
String merged = BlockStateKey.withProperty(custom.getBase().getAsString(), name, value);
BlockData resolved = Bukkit.createBlockData(merged);
return of(IrisCustomData.of(resolved, custom.getCustom()));
}
return of(Bukkit.createBlockData(mergeProperty(key, name, value)));
return of(Bukkit.createBlockData(BlockStateKey.withProperty(key, name, value)));
}
@Override
@@ -37,18 +37,11 @@ public class ObjectTargetHandler implements DirectorParameterHandler<String> {
IrisData data = data();
if (data != null) {
for (String k : data.getObjectLoader().getPossibleKeys()) {
out.add(k);
collectPrefixes(k, prefixes);
}
collectObjectKeys(data, out, prefixes);
} else {
File packsFolder = IrisPlatforms.get().packsFolder();
for (File pack : PackDirectoryResolver.listVisiblePackDirectories(packsFolder)) {
IrisData d = IrisData.get(pack);
for (String k : d.getObjectLoader().getPossibleKeys()) {
out.add(k);
collectPrefixes(k, prefixes);
}
collectObjectKeys(IrisData.get(pack), out, prefixes);
}
}
@@ -58,6 +51,13 @@ public class ObjectTargetHandler implements DirectorParameterHandler<String> {
return out;
}
private static void collectObjectKeys(IrisData data, KList<String> out, Set<String> prefixes) {
for (String key : data.getObjectLoader().getPossibleKeys()) {
out.add(key);
collectPrefixes(key, prefixes);
}
}
private static void collectPrefixes(String key, Set<String> prefixes) {
int idx = 0;
while ((idx = key.indexOf('/', idx)) >= 0) {
@@ -43,22 +43,12 @@ public class NullSafeStream<T> extends BasicStream<T> implements ProceduralStrea
@Override
public T get(double x, double z) {
T t = stream.get(x, z);
if (t == null) {
return ifNull;
}
return t;
return t == null ? ifNull : t;
}
@Override
public T get(double x, double y, double z) {
T t = stream.get(x, y, z);
if (t == null) {
return ifNull;
}
return t;
return t == null ? ifNull : t;
}
}
@@ -64,8 +64,27 @@ public class PackCompatReportTest {
public void findingLineAndDedupKey() {
CompatFinding finding = finding(CompatAction.EXCLUDED, "cave/sulfur", "layers[0].palette[1]");
assertEquals("minecraft:sulfur (block): excluded Biome cave/sulfur at layers[0].palette[1]", finding.line());
assertEquals("excluded Biome cave/sulfur at layers[0].palette[1]", finding.subjectLine());
assertEquals("BLOCK:minecraft:sulfur|EXCLUDED|Biome|cave/sulfur", finding.dedupKey());
CompatFinding blank = new CompatFinding(CompatRegistry.ENTITY, "minecraft:camel", CompatAction.DROPPED, null, null, null);
assertEquals("minecraft:camel (entity): dropped", blank.line());
assertEquals("dropped", blank.subjectLine());
}
@Test
public void findingRendersEachOptionalSubjectFieldIndependently() {
CompatFinding typeOnly = new CompatFinding(
CompatRegistry.BLOCK, "minecraft:sulfur", CompatAction.EXCLUDED, "Biome", "", "");
CompatFinding keyOnly = new CompatFinding(
CompatRegistry.BLOCK, "minecraft:sulfur", CompatAction.EXCLUDED, "", "cave/sulfur", "");
CompatFinding detailOnly = new CompatFinding(
CompatRegistry.BLOCK, "minecraft:sulfur", CompatAction.EXCLUDED, "", "", "layers[0]");
assertEquals("excluded Biome", typeOnly.subjectLine());
assertEquals("excluded cave/sulfur", keyOnly.subjectLine());
assertEquals("excluded at layers[0]", detailOnly.subjectLine());
assertEquals("minecraft:sulfur (block): excluded Biome", typeOnly.line());
assertEquals("minecraft:sulfur (block): excluded cave/sulfur", keyOnly.line());
assertEquals("minecraft:sulfur (block): excluded at layers[0]", detailOnly.line());
}
}
@@ -16,6 +16,9 @@ import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -77,4 +80,29 @@ public class IrisDataAuthoringCacheTest {
assertEquals(3.0, retained.getZoom(), 0.0);
assertEquals("{\"zoom\":3.0}", Files.readString(epochRoot.resolve("generators/sample.json")));
}
@Test
public void resourceKeyDiscoveryPreservesNestedNamesAndCachedArrays() throws Exception {
Path root = temporary.newFolder("resource-keys").toPath();
for (String name : List.of(
"generators/plain.json", "generators/nested.json/ridge.json.alt.json", "generators/ignored.JSON",
"images/plain.png", "images/nested.png/ridge.png.alt.png", "images/ignored.PNG",
"matter/plain.mat", "matter/nested.mat/ridge.mat.alt.mat", "matter/ignored.MAT")) {
Path file = root.resolve(name);
Files.createDirectories(file.getParent());
Files.write(file, new byte[0]);
}
source = IrisData.get(root.toFile());
Map<ResourceLoader<?>, Set<String>> expected = Map.of(
source.getGeneratorLoader(), Set.of("plain", "nested/ridge.alt"),
source.getImageLoader(), Set.of("plain", "nested.png/ridge.alt"),
source.getMatterLoader(), Set.of("plain", "nested.mat/ridge.alt"));
for (Map.Entry<ResourceLoader<?>, Set<String>> entry : expected.entrySet()) {
String[] keys = entry.getKey().getPossibleKeys();
assertEquals(entry.getValue(), Set.of(keys));
assertEquals(entry.getValue().size(), keys.length);
assertSame(keys, entry.getKey().getPossibleKeys());
}
}
}
@@ -703,6 +703,33 @@ public class StructureTransactionWriterTest {
assertFalse(Files.exists(transactionRoot(root, token.transactionId())));
}
@Test
public void preparedRemovalRejectsMissingAndInvalidJournalsWithoutDeletingRecoveryData() throws IOException {
Path root = temporaryFolder.newFolder("owned-removal-invalid-journal").toPath();
StructureTransactionWriter writer = new StructureTransactionWriter(root);
StructureTransactionWriter.PreparedRemovalToken token =
new StructureTransactionWriter.PreparedRemovalToken(root, UUID.randomUUID());
Path transactionRoot = transactionRoot(token.packRoot(), token.transactionId());
Path journalPath = transactionRoot.resolve(StructureTransactionJournal.FILE_NAME);
Files.createDirectories(transactionRoot);
for (boolean commit : new boolean[]{false, true}) {
IOException failure = assertThrows(
IOException.class, () -> writer.resolvePreparedRemoval(token, commit));
assertEquals("Missing prepared removal journal at " + transactionRoot, failure.getMessage());
assertTrue(Files.isDirectory(transactionRoot));
}
Files.writeString(journalPath, "not-json", StandardCharsets.UTF_8);
for (boolean commit : new boolean[]{false, true}) {
IOException failure = assertThrows(
IOException.class, () -> writer.resolvePreparedRemoval(token, commit));
assertEquals("Invalid prepared removal journal at " + journalPath, failure.getMessage());
assertTrue(failure.getCause() instanceof RuntimeException);
assertEquals("not-json", Files.readString(journalPath, StandardCharsets.UTF_8));
}
}
@Test
public void symbolicLinkAncestorsCannotEscapeThePackRoot() throws IOException {
Path root = temporaryFolder.newFolder("symlink-pack").toPath();
@@ -0,0 +1,191 @@
package art.arcane.iris.engine.hydrology;
import art.arcane.iris.engine.hydrology.cave.CavePosition;
import art.arcane.iris.engine.hydrology.cave.CaveVoxel;
import art.arcane.iris.engine.hydrology.cave.CaveVoxelPrecondition;
import art.arcane.iris.engine.hydrology.cave.CaveVoxelView;
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveAction;
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveCandidate;
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveFluidPolicy;
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveGrottoShape;
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveMode;
import art.arcane.iris.engine.hydrology.cave.HydrologyCavePlan;
import art.arcane.iris.engine.hydrology.cave.HydrologyCavePlannerSettings;
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveRejection;
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveSource;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.OptionalLong;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
public class HydrologyCaveTerminalAlignmentTest {
private static final CavePosition SHARED = new CavePosition(0, 20, 0);
private static final List<HydrologyCaveAction> PRIORITY = List.of(
HydrologyCaveAction.FALLING_FLUID,
HydrologyCaveAction.WET_SOURCE,
HydrologyCaveAction.DRY_AIR,
HydrologyCaveAction.SEAL_GUARD
);
@Test
public void candidatesPreserveActionOrderAndExposureIdentityForEveryPriorityPair() {
HydrologyCaveCourseFilter filter = filter();
Map<Long, RiverCourse> courses = Map.of(1L, course(1L, 10L), 2L, course(2L, 10L));
for (int firstPriority = 0; firstPriority < PRIORITY.size(); firstPriority++) {
for (int secondPriority = 0; secondPriority < PRIORITY.size(); secondPriority++) {
HydrologyCaveCandidate first = candidate(1L, PRIORITY.get(firstPriority));
HydrologyCaveCandidate second = candidate(2L, PRIORITY.get(secondPriority));
ArrayList<HydrologyCaveCandidate> candidates = new ArrayList<>(List.of(first, second));
Set<HydrologyCaveCandidate> exposed = Collections.newSetFromMap(new IdentityHashMap<>());
exposed.add(first);
filter.alignSharedTerminalCandidateActions(candidates, courses, exposed);
HydrologyCaveAction expected = PRIORITY.get(Math.min(firstPriority, secondPriority));
assertEquals(expected, candidates.getFirst().actions().get(SHARED));
assertEquals(expected, candidates.getLast().actions().get(SHARED));
assertEquals(List.copyOf(first.actions().keySet()), List.copyOf(candidates.getFirst().actions().keySet()));
assertEquals(List.copyOf(second.actions().keySet()), List.copyOf(candidates.getLast().actions().keySet()));
assertEquals(PRIORITY.get(firstPriority), first.actions().get(SHARED));
assertEquals(PRIORITY.get(secondPriority), second.actions().get(SHARED));
assertEquals(1, exposed.size());
assertTrue(exposed.contains(candidates.getFirst()));
assertFalse(exposed.contains(candidates.getLast()));
if (firstPriority == secondPriority) {
assertSame(first, candidates.getFirst());
assertSame(second, candidates.getLast());
} else {
assertNotSame(first, candidates.getFirst());
assertNotSame(second, candidates.getLast());
assertFalse(exposed.contains(first));
}
}
}
}
@Test
public void plansPreserveActionOrderAndBaselineForEveryPriorityPair() {
HydrologyCaveCourseFilter filter = filter();
Map<Long, RiverCourse> courses = Map.of(1L, course(1L, 10L), 2L, course(2L, 10L));
for (int firstPriority = 0; firstPriority < PRIORITY.size(); firstPriority++) {
for (int secondPriority = 0; secondPriority < PRIORITY.size(); secondPriority++) {
HydrologyCaveCandidate firstCandidate = candidate(1L, PRIORITY.get(firstPriority));
HydrologyCaveCandidate secondCandidate = candidate(2L, PRIORITY.get(secondPriority));
HydrologyCavePlan first = plan(firstCandidate);
HydrologyCavePlan second = plan(secondCandidate);
List<HydrologyCavePlan> aligned = filter.alignSharedTerminalPlanActions(
List.of(first, second), List.of(firstCandidate, secondCandidate), courses);
HydrologyCaveAction expected = PRIORITY.get(Math.min(firstPriority, secondPriority));
assertEquals(expected, aligned.getFirst().actions().get(SHARED));
assertEquals(expected, aligned.getLast().actions().get(SHARED));
assertEquals(List.copyOf(first.actions().keySet()), List.copyOf(aligned.getFirst().actions().keySet()));
assertEquals(List.copyOf(second.actions().keySet()), List.copyOf(aligned.getLast().actions().keySet()));
assertEquals(first.baselinePreconditions(), aligned.getFirst().baselinePreconditions());
assertEquals(second.baselinePreconditions(), aligned.getLast().baselinePreconditions());
assertEquals(List.copyOf(first.baselinePreconditions().keySet()),
List.copyOf(aligned.getFirst().baselinePreconditions().keySet()));
assertEquals(List.copyOf(second.baselinePreconditions().keySet()),
List.copyOf(aligned.getLast().baselinePreconditions().keySet()));
assertEquals(PRIORITY.get(firstPriority), first.actions().get(SHARED));
assertEquals(PRIORITY.get(secondPriority), second.actions().get(SHARED));
if (firstPriority == secondPriority) {
assertSame(first, aligned.getFirst());
assertSame(second, aligned.getLast());
} else {
assertNotSame(first, aligned.getFirst());
assertNotSame(second, aligned.getLast());
}
}
}
}
@Test
public void unrelatedOutletsRetainCandidateIdentity() {
HydrologyCaveCandidate first = candidate(1L, HydrologyCaveAction.WET_SOURCE);
HydrologyCaveCandidate second = candidate(2L, HydrologyCaveAction.DRY_AIR);
ArrayList<HydrologyCaveCandidate> candidates = new ArrayList<>(List.of(first, second));
filter().alignSharedTerminalCandidateActions(candidates,
Map.of(1L, course(1L, 10L), 2L, course(2L, 20L)), Set.of());
assertSame(first, candidates.getFirst());
assertSame(second, candidates.getLast());
}
@Test
public void incompatibleOrRejectedPlansRetainIdentity() {
HydrologyCaveCandidate firstCandidate = candidate(1L, HydrologyCaveAction.WET_SOURCE);
HydrologyCaveCandidate secondCandidate = candidate(2L, HydrologyCaveAction.DRY_AIR);
HydrologyCaveCandidate incompatible = new HydrologyCaveCandidate(secondCandidate.source(), "lava",
secondCandidate.settings(), false, secondCandidate.actions(), secondCandidate.intentionalOpenings());
HydrologyCavePlan first = plan(firstCandidate);
HydrologyCavePlan second = plan(secondCandidate);
HydrologyCavePlan rejected = new HydrologyCavePlan(secondCandidate.source(), HydrologyCaveRejection.OPEN_SURFACE,
Map.of(), Map.of(), OptionalLong.empty());
Map<Long, RiverCourse> courses = Map.of(1L, course(1L, 10L), 2L, course(2L, 10L));
List<HydrologyCavePlan> incompatibleResult = filter().alignSharedTerminalPlanActions(
List.of(first, second), List.of(firstCandidate, incompatible), courses);
List<HydrologyCavePlan> rejectedResult = filter().alignSharedTerminalPlanActions(
List.of(first, rejected), List.of(firstCandidate, secondCandidate), courses);
assertSame(first, incompatibleResult.getFirst());
assertSame(second, incompatibleResult.getLast());
assertSame(first, rejectedResult.getFirst());
assertSame(rejected, rejectedResult.getLast());
}
private HydrologyCaveCourseFilter filter() {
return new HydrologyCaveCourseFilter(mock(CaveVoxelView.class),
new HydrologyCaveCourseFilter.Options(false, 8192, 8192));
}
private HydrologyCaveCandidate candidate(long id, HydrologyCaveAction sharedAction) {
CavePosition own = new CavePosition((int) id, 20, 0);
LinkedHashMap<CavePosition, HydrologyCaveAction> actions = new LinkedHashMap<>();
if (id == 2L) {
actions.put(SHARED, sharedAction);
}
actions.put(own, HydrologyCaveAction.DRY_AIR);
actions.put(SHARED, sharedAction);
return new HydrologyCaveCandidate(
new HydrologyCaveSource(id, SHARED, SHARED, 20, HydrologyCaveMode.GENERATED_GROTTO),
"water", new HydrologyCavePlannerSettings(8, 8, 32, 1, 1, 1, 1, 0,
HydrologyCaveFluidPolicy.REJECT_EXISTING, HydrologyCaveGrottoShape.ELLIPSOID, 8, 8),
false, actions, Set.of(own));
}
private HydrologyCavePlan plan(HydrologyCaveCandidate candidate) {
LinkedHashMap<CavePosition, CaveVoxelPrecondition> baseline = new LinkedHashMap<>();
for (CavePosition position : candidate.actions().keySet()) {
baseline.put(position, new CaveVoxelPrecondition(CaveVoxel.SOLID, false));
}
baseline.put(new CavePosition(-(int) candidate.source().sourceId(), 20, 0),
new CaveVoxelPrecondition(CaveVoxel.SOLID, false));
return new HydrologyCavePlan(candidate.source(), HydrologyCaveRejection.NONE, candidate.actions(),
baseline, OptionalLong.empty());
}
private RiverCourse course(long id, long outletId) {
HydraulicSegment segment = new HydraulicSegment(id, id, HydrologyFeatureType.UNDERGROUND_POOL,
20, 20, 2, 2, false, false, List.of(new HydrologyPoint(0, 20, 0)),
HydraulicChannelProfile.uniform(2, 2));
return new RiverCourse(id, RiverCourseType.UNDERGROUND, OptionalLong.of(id), OptionalLong.of(outletId),
"water", 1, List.of(), List.of(segment));
}
}
@@ -27,7 +27,7 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisHydrologyRuntimeSettingsTest {
public class IrisHydrologySettingsCompilerTest {
@Test
@SuppressWarnings("unchecked")
public void regionalSurfaceControlsFingerprintAllScopesAndExpandSpacingReach() {
@@ -47,20 +47,20 @@ public class IrisHydrologyRuntimeSettingsTest {
when(biomes.loadAll(any(String[].class))).thenReturn(new KList<>(volcanic));
HydrologyPlannerSettings baseline = settings(dimension);
HydrologyPlannerSettings regional = IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology(), () -> data);
HydrologyPlannerSettings regional = IrisHydrologySettingsCompiler.compile(dimension, dimension.getHydrology(), () -> data);
assertEquals(HydrologyPlannerSettings.SurfacePolicyBounds.NONE, baseline.surfacePolicyBounds());
assertNotEquals(baseline.fingerprint(), regional.fingerprint());
assertEquals(384, regional.maximumSurfaceSourceSpacing());
volcanic.getRiverPolicy().setSurfaceSourceSpacing(1600);
HydrologyPlannerSettings widened = IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology(), () -> data);
HydrologyPlannerSettings widened = IrisHydrologySettingsCompiler.compile(dimension, dimension.getHydrology(), () -> data);
assertNotEquals(regional.fingerprint(), widened.fingerprint());
assertEquals(1600, widened.maximumSurfaceSourceSpacing());
assertTrue(widened.publicationRadius() >= 1600);
assertEquals(widened.fingerprint(), IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology(), () -> data).fingerprint());
assertEquals(widened.fingerprint(), IrisHydrologySettingsCompiler.compile(dimension, dimension.getHydrology(), () -> data).fingerprint());
dimension.getRiverPolicy().setSurfaceInlandOutlets(7);
assertNotEquals(widened.fingerprint(), IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology(), () -> data).fingerprint());
assertNotEquals(widened.fingerprint(), IrisHydrologySettingsCompiler.compile(dimension, dimension.getHydrology(), () -> data).fingerprint());
}
@Test
@@ -90,7 +90,7 @@ public class IrisHydrologyRuntimeSettingsTest {
IrisRiverRoutingConfig routing = new IrisRiverRoutingConfig()
.setTileSize(256);
assertEquals(128, IrisHydrologyRuntime.maximumDeepChannelLength(deepFluid, routing));
assertEquals(128, IrisHydrologySettingsCompiler.maximumDeepChannelLength(deepFluid, routing));
}
@Test
@@ -101,7 +101,7 @@ public class IrisHydrologyRuntimeSettingsTest {
IrisRiverRoutingConfig routing = new IrisRiverRoutingConfig()
.setTileSize(256);
assertEquals(0, IrisHydrologyRuntime.maximumDeepChannelLength(deepFluid, routing));
assertEquals(0, IrisHydrologySettingsCompiler.maximumDeepChannelLength(deepFluid, routing));
}
@Test
@@ -112,12 +112,12 @@ public class IrisHydrologyRuntimeSettingsTest {
rivers.getGrottos().getInland().setEnabled(true).setConnectSurfaceRivers(true);
rivers.getRouting().getInlandOutlets().add(IrisRiverInlandOutlet.SINKHOLE_GROTTO);
assertTrue(IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology(), () -> null)
assertTrue(IrisHydrologySettingsCompiler.compile(dimension, dimension.getHydrology(), () -> null)
.outlets().surfaceSinkholesEnabled());
rivers.getRouting().getInlandOutlets().clear();
assertFalse(IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology(), () -> null)
assertFalse(IrisHydrologySettingsCompiler.compile(dimension, dimension.getHydrology(), () -> null)
.outlets().surfaceSinkholesEnabled());
}
@@ -132,13 +132,13 @@ public class IrisHydrologyRuntimeSettingsTest {
rivers.getUnderground().getSources().setDensity(4.5D);
rivers.getRouting().setSampleSpacing(64);
assertEquals(5, IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology(), () -> null)
assertEquals(5, IrisHydrologySettingsCompiler.compile(dimension, dimension.getHydrology(), () -> null)
.surface().sources().maximumPerTile());
assertEquals(384, IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology(), () -> null)
assertEquals(384, IrisHydrologySettingsCompiler.compile(dimension, dimension.getHydrology(), () -> null)
.surface().sources().minimumSpacing());
assertEquals(5, IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology(), () -> null)
assertEquals(5, IrisHydrologySettingsCompiler.compile(dimension, dimension.getHydrology(), () -> null)
.underground().sources().maximumPerTile());
assertEquals(512, IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology(), () -> null)
assertEquals(512, IrisHydrologySettingsCompiler.compile(dimension, dimension.getHydrology(), () -> null)
.underground().sources().minimumSpacing());
}
@@ -173,7 +173,7 @@ public class IrisHydrologyRuntimeSettingsTest {
.setSlopePenalty(3D)
.setConfluenceAttraction(0.4D);
HydrologyPlannerSettings settings = IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology(), () -> null);
HydrologyPlannerSettings settings = IrisHydrologySettingsCompiler.compile(dimension, dimension.getHydrology(), () -> null);
HydrologyPlannerSettings.Banks banks = settings.surface().banks();
assertEquals(2, banks.sink());
@@ -220,8 +220,7 @@ public class IrisHydrologyRuntimeSettingsTest {
.setMaximumOutletsPerTile(2)
.setMaximumCoastalOutletsPerTile(5);
HydrologyPlannerSettings.Outlets outlets = IrisHydrologyRuntime
.createSettings(dimension, dimension.getHydrology(), () -> null)
HydrologyPlannerSettings.Outlets outlets = IrisHydrologySettingsCompiler.compile(dimension, dimension.getHydrology(), () -> null)
.outlets();
assertEquals(2, outlets.maximumPerTile());
@@ -252,8 +251,7 @@ public class IrisHydrologyRuntimeSettingsTest {
.setBasinWidthRatio(2.1D)
.setMaximumBasinDepth(9);
HydrologyPlannerSettings.Geometry geometry = IrisHydrologyRuntime
.createSettings(dimension, dimension.getHydrology(), () -> null)
HydrologyPlannerSettings.Geometry geometry = IrisHydrologySettingsCompiler.compile(dimension, dimension.getHydrology(), () -> null)
.geometry();
assertEquals(47, geometry.meanders().primaryWavelength());
@@ -284,12 +282,12 @@ public class IrisHydrologyRuntimeSettingsTest {
coastal.setEnabled(true);
coastal.getSeaCaves().setEnabled(true).setMaximumPerTile(5).setMinimumSpacing(200).setMinimumCoastHeight(9).setDepth(20);
HydrologyPlannerSettings.SeaCaves mapped = IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology(), () -> null)
HydrologyPlannerSettings.SeaCaves mapped = IrisHydrologySettingsCompiler.compile(dimension, dimension.getHydrology(), () -> null)
.seaCaves();
assertEquals(HydrologyPlannerSettings.SeaCaves.of(true, 5, 200, 9, 20), mapped);
coastal.setEnabled(false);
HydrologyPlannerSettings.SeaCaves withoutCoastalGrottos = IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology(), () -> null)
HydrologyPlannerSettings.SeaCaves withoutCoastalGrottos = IrisHydrologySettingsCompiler.compile(dimension, dimension.getHydrology(), () -> null)
.seaCaves();
assertFalse(withoutCoastalGrottos.enabled());
assertEquals(5, withoutCoastalGrottos.maximumPerTile());
@@ -297,7 +295,7 @@ public class IrisHydrologyRuntimeSettingsTest {
coastal.setEnabled(true);
coastal.getSeaCaves().setEnabled(false);
assertFalse(IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology(), () -> null).seaCaves().enabled());
assertFalse(IrisHydrologySettingsCompiler.compile(dimension, dimension.getHydrology(), () -> null).seaCaves().enabled());
}
@Test
@@ -488,7 +486,7 @@ public class IrisHydrologyRuntimeSettingsTest {
IrisDimension dimension = new IrisDimension().setRegions(new KList<>("valley"));
dimension.getRiverPolicy().setShoreWidth(4D);
assertEquals(4D, IrisHydrologyRuntime.widestShoreBiomeWidth(dimension, () -> null, 1.5D), 0D);
assertEquals(4D, IrisHydrologySettingsCompiler.widestShoreBiomeWidth(dimension, () -> null, 1.5D), 0D);
assertEquals(4D, settings(dimension).widestShoreBiomeWidth(), 0D);
IrisRegion valley = new IrisRegion().setRiverPolicy(new IrisRiverPolicy().setShoreBiomeWidth(6D));
@@ -503,18 +501,18 @@ public class IrisHydrologyRuntimeSettingsTest {
when(biomeLoader.getPossibleKeys()).thenReturn(new String[]{"beach", "plain"});
when(biomeLoader.loadAll(any(String[].class))).thenReturn(new KList<>(beach, plain));
assertEquals(9D, IrisHydrologyRuntime.widestShoreBiomeWidth(dimension, () -> data, 1.5D), 0D);
assertEquals(9D, IrisHydrologySettingsCompiler.widestShoreBiomeWidth(dimension, () -> data, 1.5D), 0D);
beach.getRiverPolicy().setShoreWidth(null).setShoreBiomeWidth(11D);
assertEquals(11D, IrisHydrologyRuntime.widestShoreBiomeWidth(dimension, () -> data, 1.5D), 0D);
assertEquals(11D, IrisHydrologySettingsCompiler.widestShoreBiomeWidth(dimension, () -> data, 1.5D), 0D);
valley.getRiverPolicy().setShoreWidth(12D);
assertEquals(12D, IrisHydrologyRuntime.widestShoreBiomeWidth(dimension, () -> data, 1.5D), 0D);
assertEquals(12D, IrisHydrologySettingsCompiler.widestShoreBiomeWidth(dimension, () -> data, 1.5D), 0D);
dimension.getRiverPolicy().setShoreWidth(null);
valley.getRiverPolicy().setShoreWidth(null).setShoreBiomeWidth(null);
beach.getRiverPolicy().setShoreBiomeWidth(null);
assertEquals(1.5D, IrisHydrologyRuntime.widestShoreBiomeWidth(dimension, () -> data, 1.5D), 0D);
assertEquals(1.5D, IrisHydrologySettingsCompiler.widestShoreBiomeWidth(dimension, () -> data, 1.5D), 0D);
}
@Test
@@ -537,6 +535,6 @@ public class IrisHydrologyRuntimeSettingsTest {
}
private static HydrologyPlannerSettings settings(IrisDimension dimension) {
return IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology(), () -> null);
return IrisHydrologySettingsCompiler.compile(dimension, dimension.getHydrology(), () -> null);
}
}
@@ -6,6 +6,39 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class SurfaceNoiseTest {
@Test
public void scalarBitsPreserveChannelGeometry() {
long[][] samples = {
{0L, 0, 0, 1, 0x3fc9ff45ea7ce2bcL, 0xbfe3005d0ac18ea2L},
{77L, -40, 39, 1, 0x3fe1608c5d9f966eL, 0x3fb608c5d9f966e0L},
{77L, -40, 39, 2, 0x3fd12fd3b2255266L, 0xbfdda0589bb55b34L},
{77L, -40, 39, 3, 0x3fde59c9633f9d3eL, 0xbfaa6369cc062c20L},
{77L, -40, 39, 7, 0x3febee1ee702b638L, 0x3fe7dc3dce056c70L},
{77L, -40, 39, 12, 0x3fe58a3d8c6a3a26L, 0x3fd628f631a8e898L},
{77L, -40, 39, 16, 0x3fe379956dd75dfbL, 0x3fcbccab6ebaefd8L},
{77L, -40, 39, 31, 0x3fdd9757689aa0dcL, 0xbfb34544bb2af920L},
{77L, -40, 39, 64, 0x3fe0968a66559de7L, 0x3fa2d14ccab3bce0L},
{0L, -12, -12, 12, 0x3fa2ed0fbd6e4760L, 0xbfeda25e08523714L},
{0L, -13, -11, 12, 0x3fabab89552088a4L, 0xbfec8a8ed55beeecL},
{-1L, 12, -12, 12, 0x3fc3b99ed5ce495cL, 0xbfe623309518db52L},
{Long.MIN_VALUE, -1, 1, 31, 0x3fe213950b5ed096L, 0x3fc09ca85af684b0L},
{Long.MAX_VALUE, 255, -257, 256, 0x3fcb1a5a870174baL, 0xbfe272d2bc7f45a3L},
{1337L, -12345, 67890, 8192, 0x3fd4e8d04021f6aaL, 0xbfd62e5f7fbc12acL},
{1337L, Integer.MIN_VALUE, Integer.MAX_VALUE, 1, 0x3fdc5d75fb3dcf0cL, 0xbfbd1450261187a0L},
{1337L, Integer.MIN_VALUE, Integer.MAX_VALUE, 12, 0x3feb8585bd4cd270L, 0x3fe70b0b7a99a4e0L},
{1337L, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE, 0x3fed6ff84c27d2e8L, 0x3feadff0984fa5d0L}
};
for (long[] sample : samples) {
long seed = sample[0];
int x = (int) sample[1];
int z = (int) sample[2];
int wavelength = (int) sample[3];
String context = "seed=" + seed + ", x=" + x + ", z=" + z + ", wavelength=" + wavelength;
assertEquals(context, sample[4], Double.doubleToLongBits(SurfaceNoise.value(seed, x, z, wavelength)));
assertEquals(context, sample[5], Double.doubleToLongBits(SurfaceNoise.signed(seed, x, z, wavelength)));
}
}
@Test
public void valuesStayInRangeAndAreDeterministic() {
for (int x = -40; x < 40; x++) {
@@ -2,12 +2,15 @@ package art.arcane.iris.engine.object;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.math.IrisBlockVector;
import art.arcane.volmlib.util.collection.KMap;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
@@ -51,6 +54,39 @@ public class IrisObjectIoWriteLimitsTest {
return object;
}
@Test
public void preservesV2HeaderSignedCoordinatesAndTilePayload() throws IOException {
IrisObject object = new IrisObject(7, 9, 11);
object.blocks.put(new IrisBlockVector(-3, 2, -1), state("minecraft:chest"));
KMap<String, Object> properties = new KMap<>();
properties.put("marker", "stored");
object.states.put(new IrisBlockVector(-3, 2, -1), new TileData("minecraft:chest", properties));
ByteArrayOutputStream encoded = new ByteArrayOutputStream();
IrisObjectIO.write(object, encoded);
try (DataInputStream input = new DataInputStream(new ByteArrayInputStream(encoded.toByteArray()))) {
assertEquals(7, input.readInt());
assertEquals(9, input.readInt());
assertEquals(11, input.readInt());
assertEquals("Iris V2 IOB;", input.readUTF());
assertEquals(1, input.readShort());
assertEquals("minecraft:chest", input.readUTF());
assertEquals(1, input.readInt());
assertEquals(-3, input.readShort());
assertEquals(2, input.readShort());
assertEquals(-1, input.readShort());
assertEquals(0, input.readShort());
assertEquals(1, input.readInt());
assertEquals(-3, input.readShort());
assertEquals(2, input.readShort());
assertEquals(-1, input.readShort());
assertEquals("minecraft:chest", input.readUTF());
assertEquals("{\"marker\":\"stored\"}", input.readUTF());
assertEquals(-1, input.read());
}
}
@Test
public void rejectsPaletteOverflowInsteadOfWrappingTheShort() {
IrisObject object = oversizedPaletteObject;
@@ -0,0 +1,36 @@
package art.arcane.iris.platform;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
public class BlockStateKeyTest {
@Test
public void namespaceIgnoresPropertyValues() {
assertEquals("minecraft", BlockStateKey.namespace("stone"));
assertEquals("minecraft", BlockStateKey.namespace("stone[variant=custom:stone]"));
assertEquals("custom", BlockStateKey.namespace("custom:stone[variant=minecraft:stone]"));
assertEquals("", BlockStateKey.namespace(":stone"));
}
@Test
public void addsAndReplacesPropertiesInEncounterOrder() {
assertEquals("minecraft:stairs[facing=east]",
BlockStateKey.withProperty("minecraft:stairs", "facing", "east"));
assertEquals("minecraft:stairs[facing=east,half=top]",
BlockStateKey.withProperty("minecraft:stairs[facing=north,half=top]", "facing", "east"));
assertEquals("minecraft:stairs[facing=north,half=top,waterlogged=true]",
BlockStateKey.withProperty("minecraft:stairs[facing=north,half=top]", "waterlogged", "true"));
}
@Test
public void retainsDuplicateAndMalformedEntryHandling() {
assertEquals("custom:block[a=last,b=left=right,c=value]",
BlockStateKey.withProperty("custom:block[ a = first ,invalid,b=left=right,a=last]", "c", "value"));
assertEquals("stone[x=y]", BlockStateKey.withProperty("stone[]", "x", "y"));
assertThrows(StringIndexOutOfBoundsException.class,
() -> BlockStateKey.withProperty("stone[x=y", "x", "z"));
assertThrows(NullPointerException.class, () -> BlockStateKey.namespace(null));
}
}
@@ -211,21 +211,7 @@ public final class GenerationOrderProbe {
System.out.println(LOG_PREFIX + " FAIL: probe execution failed");
failure.printStackTrace(System.out);
AggregateSignature unavailable = new AggregateSignature("unavailable", "unavailable", "unavailable");
ProbeResult failed = new ProbeResult(
"FAIL",
configuration.dimensionKey(),
configuration.seed(),
configuration.chunkCount(),
configuration.minimumChunkX(),
configuration.maximumChunkX(),
configuration.minimumChunkZ(),
configuration.maximumChunkZ(),
configuration.parallelism(),
configuration.shuffleSeed(),
configuration.multicore(),
configuration.studio(),
unavailable
);
ProbeResult failed = result(configuration, "FAIL", unavailable);
System.out.println(failed.machineLine());
System.exit(1);
}
@@ -264,11 +250,12 @@ public final class GenerationOrderProbe {
for (Map.Entry<ChunkCoordinate, ChunkHash> entry : ordered.entrySet()) {
ChunkCoordinate coordinate = entry.getKey();
ChunkHash hash = entry.getValue();
updateDigest(blocks, coordinate.x() + "," + coordinate.z());
byte[] encodedCoordinate = (coordinate.x() + "," + coordinate.z()).getBytes(StandardCharsets.UTF_8);
updateDigest(blocks, encodedCoordinate);
updateDigest(blocks, hash.blocks());
updateDigest(biomes, coordinate.x() + "," + coordinate.z());
updateDigest(biomes, encodedCoordinate);
updateDigest(biomes, hash.biomes());
updateDigest(combined, coordinate.x() + "," + coordinate.z());
updateDigest(combined, encodedCoordinate);
updateDigest(combined, hash.combined());
}
return new AggregateSignature(
@@ -391,8 +378,12 @@ public final class GenerationOrderProbe {
+ mismatches.size() + " chunk(s).");
}
return result(configuration, "PASS", baseline.signature());
}
private static ProbeResult result(ProbeConfiguration configuration, String status, AggregateSignature signature) {
return new ProbeResult(
"PASS",
status,
configuration.dimensionKey(),
configuration.seed(),
configuration.chunkCount(),
@@ -404,7 +395,7 @@ public final class GenerationOrderProbe {
configuration.shuffleSeed(),
configuration.multicore(),
configuration.studio(),
baseline.signature()
signature
);
}
@@ -102,6 +102,11 @@ public final class GenerationOrderProbeTest {
GenerationOrderProbe.aggregateSignature(forward),
GenerationOrderProbe.aggregateSignature(reverse)
);
assertEquals(new GenerationOrderProbe.AggregateSignature(
"f5a500c97351e576fb335317c0a716c48c320b1304a1f4dc9658f1634b80de98",
"1bf3a08fecb828a69b7c504459d2a53b7d089aa9b16040000bb5e06f9e1e2b5f",
"d667a0482bb425cee8f7384284472ce915cfdd6a1b9a123fa8a58ee26b67f9f9"),
GenerationOrderProbe.aggregateSignature(forward));
reverse.put(first, new GenerationOrderProbe.ChunkHash("changed", "b", "changed"));
assertNotEquals(
GenerationOrderProbe.aggregateSignature(forward),
@@ -125,13 +125,7 @@ public final class IrisWireReader {
* @throws ProtocolException if the length is negative or exceeds the remaining bytes
*/
public String readString() throws ProtocolException {
int declaredLength = readVarInt();
if (declaredLength < 0) {
throw new ProtocolException("negative string length");
}
if (declaredLength > remaining()) {
throw new ProtocolException("string length exceeds remaining frame bytes");
}
int declaredLength = readLength("negative string length", "string length exceeds remaining frame bytes");
String value = new String(frame, position, declaredLength, StandardCharsets.UTF_8);
position += declaredLength;
return value;
@@ -143,19 +137,24 @@ public final class IrisWireReader {
* @throws ProtocolException if the length is negative or exceeds the remaining bytes
*/
public byte[] readBytes() throws ProtocolException {
int declaredLength = readVarInt();
if (declaredLength < 0) {
throw new ProtocolException("negative byte array length");
}
if (declaredLength > remaining()) {
throw new ProtocolException("byte array length exceeds remaining frame bytes");
}
int declaredLength = readLength("negative byte array length", "byte array length exceeds remaining frame bytes");
byte[] value = new byte[declaredLength];
System.arraycopy(frame, position, value, 0, declaredLength);
position += declaredLength;
return value;
}
private int readLength(String negativeMessage, String oversizedMessage) throws ProtocolException {
int declaredLength = readVarInt();
if (declaredLength < 0) {
throw new ProtocolException(negativeMessage);
}
if (declaredLength > remaining()) {
throw new ProtocolException(oversizedMessage);
}
return declaredLength;
}
private int remaining() {
return limit - position;
}
@@ -104,11 +104,7 @@ public final class IrisWireWriter {
* Writes a varint-length-prefixed UTF-8 string. {@code value} must not be null.
*/
public void writeString(String value) {
byte[] encoded = value.getBytes(StandardCharsets.UTF_8);
writeVarInt(encoded.length);
ensure(encoded.length);
System.arraycopy(encoded, 0, buffer, length, encoded.length);
length += encoded.length;
writeBytes(value.getBytes(StandardCharsets.UTF_8));
}
/**
@@ -0,0 +1,87 @@
package art.arcane.iris.spi.protocol;
import org.junit.Test;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class IrisWirePayloadTest {
@Test
public void preservesMixedPayloadEncodingAndIndependentCopies() throws Exception {
IrisWireWriter writer = new IrisWireWriter();
byte[] payload = {0, (byte) 0xff};
writer.writeString("é\u0000");
writer.writeBytes(payload);
writer.writeString("");
writer.writeBytes(new byte[0]);
writer.writeInt(0x12345678);
payload[0] = 42;
byte[] frame = writer.toByteArray();
assertArrayEquals(new byte[]{3, (byte) 0xc3, (byte) 0xa9, 0, 2, 0, (byte) 0xff,
0, 0, 0x12, 0x34, 0x56, 0x78}, frame);
IrisWireReader reader = new IrisWireReader(frame);
assertEquals("é\u0000", reader.readString());
byte[] decoded = reader.readBytes();
assertArrayEquals(new byte[]{0, (byte) 0xff}, decoded);
decoded[0] = 42;
assertEquals(0, frame[5]);
assertEquals("", reader.readString());
assertArrayEquals(new byte[0], reader.readBytes());
assertEquals(0x12345678, reader.readInt());
}
@Test
public void preservesPayloadLengthErrorsAndConsumesOnlyThePrefix() throws Exception {
byte[] negative = {(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, 0x0f, 1};
byte[] oversized = {2, 1};
IrisWireReader negativeString = new IrisWireReader(negative);
IrisWireReader negativeBytes = new IrisWireReader(negative);
IrisWireReader oversizedString = new IrisWireReader(oversized);
IrisWireReader oversizedBytes = new IrisWireReader(oversized);
assertEquals("negative string length", assertThrows(ProtocolException.class, negativeString::readString).getMessage());
assertEquals("negative byte array length", assertThrows(ProtocolException.class, negativeBytes::readBytes).getMessage());
assertEquals("string length exceeds remaining frame bytes",
assertThrows(ProtocolException.class, oversizedString::readString).getMessage());
assertEquals("byte array length exceeds remaining frame bytes",
assertThrows(ProtocolException.class, oversizedBytes::readBytes).getMessage());
assertTrue(negativeString.readBoolean());
assertTrue(negativeBytes.readBoolean());
assertTrue(oversizedString.readBoolean());
assertTrue(oversizedBytes.readBoolean());
}
@Test
public void preservesFrameCapAndPrefixWrittenBeforePayloadOverflow() {
int maximumPayload = IrisProtocol.MAX_FRAME_BYTES - 3;
IrisWireWriter exactString = new IrisWireWriter();
IrisWireWriter exactBytes = new IrisWireWriter();
exactString.writeString("x".repeat(maximumPayload));
exactBytes.writeBytes(new byte[maximumPayload]);
assertEquals(IrisProtocol.MAX_FRAME_BYTES, exactString.toByteArray().length);
assertEquals(IrisProtocol.MAX_FRAME_BYTES, exactBytes.toByteArray().length);
IrisWireWriter oversizedString = new IrisWireWriter();
IrisWireWriter oversizedBytes = new IrisWireWriter();
IrisWireWriter prefix = new IrisWireWriter();
prefix.writeVarInt(maximumPayload + 1);
String message = "Iris protocol frame exceeds " + IrisProtocol.MAX_FRAME_BYTES + " byte cap";
assertEquals(message, assertThrows(IllegalStateException.class,
() -> oversizedString.writeString("x".repeat(maximumPayload + 1))).getMessage());
assertEquals(message, assertThrows(IllegalStateException.class,
() -> oversizedBytes.writeBytes(new byte[maximumPayload + 1])).getMessage());
assertArrayEquals(prefix.toByteArray(), oversizedString.toByteArray());
assertArrayEquals(prefix.toByteArray(), oversizedBytes.toByteArray());
}
@Test
public void rejectsNullPayloadsWithoutWriting() {
IrisWireWriter writer = new IrisWireWriter();
assertThrows(NullPointerException.class, () -> writer.writeString(null));
assertThrows(NullPointerException.class, () -> writer.writeBytes(null));
assertArrayEquals(new byte[0], writer.toByteArray());
}
}