mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-12 03:24:14 +00:00
Patching Cleanup
This commit is contained in:
+2
-26
@@ -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,
|
||||
|
||||
+5
-26
@@ -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;
|
||||
|
||||
+47
-1
@@ -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);
|
||||
|
||||
+32
@@ -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());
|
||||
}
|
||||
|
||||
+2
-26
@@ -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);
|
||||
|
||||
+5
-30
@@ -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) {
|
||||
|
||||
+30
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+18
-28
@@ -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(
|
||||
|
||||
+36
-36
@@ -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,
|
||||
|
||||
+2
-484
@@ -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) {
|
||||
|
||||
+499
@@ -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
|
||||
|
||||
+9
-9
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+27
@@ -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();
|
||||
|
||||
+191
@@ -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));
|
||||
}
|
||||
}
|
||||
+25
-27
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user