Terrain-first hydrology

- Replace the river package with the hydrology planner, footprint compiler, cave containment and runtime
- Add the hydrology pack schema, validator, probes and tests
- Wire hydrology into IrisComplex, actuators, mantle, Vision and commands
This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-01 18:28:00 -04:00
1 parent bdaa84975a
commit 48afa0d93f
352 files changed
+57955 -19113

No files matched your search

@@ -18,6 +18,7 @@
package art.arcane.iris.modded;
import art.arcane.iris.engine.IrisEngineMantle;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@@ -41,7 +42,7 @@ public final class MainWorldService {
"region",
"entities",
"poi",
"mantle",
IrisEngineMantle.STORAGE_FOLDER_NAME,
"dimensions/minecraft/overworld",
"dimensions/minecraft/the_nether",
"dimensions/minecraft/the_end",
@@ -18,6 +18,7 @@
package art.arcane.iris.modded;
import art.arcane.iris.engine.IrisEngineMantle;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.level.Level;
@@ -33,7 +34,12 @@ import java.util.List;
import java.util.stream.Stream;
public final class ModdedDimensionStorage {
private static final List<String> CHUNK_DATA_FOLDERS = List.of("region", "entities", "poi", "mantle");
private static final List<String> CHUNK_DATA_FOLDERS = List.of(
"region",
"entities",
"poi",
IrisEngineMantle.STORAGE_FOLDER_NAME
);
private ModdedDimensionStorage() {
}
@@ -325,7 +325,7 @@ public final class IrisModdedCommands {
return 0;
}
String target = downloadDisplayTarget(request);
String downloadSource = request.pack() == null ? "direct ZIP URL" : "built-in beta release";
String downloadSource = request.pack() == null ? "direct ZIP URL" : "built-in stable release";
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
if (scheduler == null) {
fail(source, IrisLanguage.plain(
@@ -25,6 +25,8 @@ import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
import art.arcane.iris.engine.framework.StructureReachability;
import art.arcane.iris.engine.hydrology.HydrologyFeatureQuery;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
@@ -62,6 +64,7 @@ final class ModdedCommandSuggestions {
static final SuggestionProvider<CommandSourceStack> OBJECT_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestObjectKeys(context, builder);
static final SuggestionProvider<CommandSourceStack> STRUCTURE_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestStructureKeys(context, builder);
static final SuggestionProvider<CommandSourceStack> POI_TYPES = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> SharedSuggestionProvider.suggest(List.of("buried_treasure"), builder);
static final SuggestionProvider<CommandSourceStack> HYDROLOGY_TYPES = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestHydrologyTypes(context, builder);
static final SuggestionProvider<CommandSourceStack> PACK_NAMES = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestPackNames(context, builder);
static final SuggestionProvider<CommandSourceStack> DIMENSION_NAMES = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestDimensionNames(context, builder);
@@ -79,7 +82,7 @@ final class ModdedCommandSuggestions {
try {
Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel());
if (engine != null) {
return SharedSuggestionProvider.suggest(engine.getData().getBiomeLoader().getPossibleKeys(), builder);
return SharedSuggestionProvider.suggest(reachableBiomeKeys(engine), builder);
}
} catch (Throwable e) {
warnTabFailure("biome keys", context.getSource(), e);
@@ -87,6 +90,46 @@ final class ModdedCommandSuggestions {
return builder.buildFuture();
}
private static CompletableFuture<Suggestions> suggestHydrologyTypes(
CommandContext<CommandSourceStack> context,
SuggestionsBuilder builder
) {
ModdedCommandFeedback.tab(context.getSource());
try {
Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel());
if (engine != null && engine.getComplex().getHydrologyRuntime() != null) {
return SharedSuggestionProvider.suggest(
engine.getComplex().getHydrologyRuntime().featureQueryKeys(), builder);
}
} catch (Throwable error) {
warnTabFailure("hydrology types", context.getSource(), error);
}
return SharedSuggestionProvider.suggest(
HydrologyFeatureQuery.suggestions(List.of()), builder);
}
static Set<String> reachableBiomeKeys(Engine engine) {
return reachableBiomeKeys(engine.getAllBiomes());
}
static Set<String> reachableBiomeKeys(Iterable<IrisBiome> biomes) {
Set<String> keys = new TreeSet<>();
for (IrisBiome biome : biomes) {
if (biome != null && biome.getLoadKey() != null && !biome.getLoadKey().isBlank()) {
keys.add(biome.getLoadKey());
}
}
return keys;
}
static boolean isReachableBiome(Engine engine, String biomeKey) {
return isReachableBiome(engine.getAllBiomes(), biomeKey);
}
static boolean isReachableBiome(Iterable<IrisBiome> biomes, String biomeKey) {
return biomeKey != null && reachableBiomeKeys(biomes).contains(biomeKey.trim());
}
private static CompletableFuture<Suggestions> suggestRegionKeys(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) {
ModdedCommandFeedback.tab(context.getSource());
try {
@@ -227,6 +227,9 @@ final class ModdedCommandTree {
.then(Commands.literal("object")
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.OBJECT_KEYS)
.executes((CommandContext<CommandSourceStack> context) -> ModdedLocateCommands.gotoObject(context.getSource(), StringArgumentType.getString(context, "key")))))
.then(Commands.literal("river")
.then(Commands.argument("type", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.HYDROLOGY_TYPES)
.executes((CommandContext<CommandSourceStack> context) -> ModdedLocateCommands.gotoRiver(context.getSource(), StringArgumentType.getString(context, "type")))))
.then(Commands.literal("unregistered")
.executes((CommandContext<CommandSourceStack> context) ->
ModdedUnregisteredStructures.print(context.getSource())))
@@ -29,12 +29,16 @@ import art.arcane.iris.engine.framework.Locator;
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
import art.arcane.iris.engine.framework.StructureReachability;
import art.arcane.iris.engine.framework.WrongEngineBroException;
import art.arcane.iris.engine.hydrology.HydrologyFeatureQuery;
import art.arcane.iris.engine.hydrology.HydrologyFeatureRef;
import art.arcane.iris.engine.hydrology.runtime.IrisHydrologyRuntime;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.volmlib.util.localization.MessageArgument;
import art.arcane.volmlib.util.math.Position2;
import com.mojang.datafixers.util.Pair;
@@ -87,6 +91,10 @@ final class ModdedLocateCommands {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_BIOME_2, MessageArgument.untrusted("key", key)));
return 0;
}
if (!ModdedCommandSuggestions.isReachableBiome(engine, biome.getLoadKey())) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_IS_NOT_DEFINED_DIMENSION, MessageArgument.untrusted("value", biome.getLoadKey())));
return 0;
}
locate(source, level, engine, player, Locator.surfaceBiome(biome.getLoadKey()), "biome " + biome.getLoadKey());
return 1;
}
@@ -137,6 +145,65 @@ final class ModdedLocateCommands {
return 1;
}
static int gotoRiver(CommandSourceStack source, String type) {
ServerPlayer player = source.getPlayer();
if (player == null) {
IrisModdedCommands.fail(source, "This command can only be used by a player.");
return 0;
}
ServerLevel level = source.getLevel();
Engine engine = IrisModdedCommands.engineFor(level);
if (engine == null) {
IrisModdedCommands.fail(source, "This dimension is not generated by Iris.");
return 0;
}
IrisHydrologyRuntime runtime = engine.getComplex().getHydrologyRuntime();
if (runtime == null) {
IrisModdedCommands.fail(source, "Hydrology is not active in this dimension.");
return 0;
}
HydrologyFeatureQuery query;
try {
query = HydrologyFeatureQuery.parse(type);
} catch (IllegalArgumentException error) {
IrisModdedCommands.fail(source, error.getMessage());
return 0;
}
int originX = player.blockPosition().getX();
int originZ = player.blockPosition().getZ();
int requestedDistance = Math.min(8192, runtime.settings().routing().tileSize() * 15);
int maximumDistance = runtime.maximumFeatureSearchDistance(originX, originZ, requestedDistance);
MinecraftServer server = source.getServer();
IrisModdedCommands.ok(source, "Searching accepted hydrology plans for " + type + "...");
MultiBurst.burst.completeValueAsync(() -> runtime.nearestFeature(
query.types(), query.profileKey(), originX, originZ, maximumDistance).orElse(null))
.whenComplete((HydrologyFeatureRef feature, Throwable error) -> server.execute(() -> {
if (error != null) {
ModdedIrisLog.error("Hydrology locate failed for {}", type, error);
IrisModdedCommands.fail(source,
"Could not locate " + type + " hydrology: " + error.getClass().getSimpleName());
return;
}
if (feature == null) {
IrisModdedCommands.fail(source,
"No accepted " + type + " hydrology feature found within "
+ maximumDistance + " blocks.");
return;
}
int worldY = feature.y() + engine.getDimension().getMinHeight() + 2;
teleportToStructure(
source,
level,
player,
feature.x(),
worldY,
feature.z(),
type + " hydrology feature"
);
}));
return 1;
}
static int gotoStructure(CommandSourceStack source, String keyRaw) {
ServerLevel level = source.getLevel();
Engine engine = IrisModdedCommands.engineFor(level);
@@ -341,8 +341,7 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
}
markCompleted();
listener.onChunkGenerated(x, z);
cleanupMantleChunk(x, z);
listener.onChunkCleaned(x, z);
cleanupMantleChunksCoveredBy(x, z, listener);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (TimeoutException | ExecutionException e) {
@@ -406,8 +405,7 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
onSuccess();
markCompleted();
listener.onChunkGenerated(x, z);
cleanupMantleChunk(x, z);
listener.onChunkCleaned(x, z);
cleanupMantleChunksCoveredBy(x, z, listener);
} finally {
markFinished();
semaphore.release();
@@ -494,9 +492,9 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
}
}
private void cleanupMantleChunk(int x, int z) {
private void cleanupMantleChunksCoveredBy(int x, int z, PregenListener listener) {
try {
engine.getMantle().forceCleanupChunk(x, z);
engine.getMantle().cleanupChunksCoveredBy(x, z, true, listener::onChunkCleaned);
} catch (Throwable e) {
ModdedIrisLog.debug("Iris pregen mantle cleanup skipped for {},{}: {}", x, z, e.toString());
}
@@ -301,7 +301,7 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
}
}
private void runUpdatePass(Engine engine, ServerLevel level, int chunkX, int chunkZ, MantleChunk<Matter> chunk) {
void runUpdatePass(Engine engine, ServerLevel level, int chunkX, int chunkZ, MantleChunk<Matter> chunk) {
PrecisionStopwatch stopwatch = PrecisionStopwatch.start();
int minHeight = engine.getWorld().minHeight();
int baseX = chunkX << 4;
@@ -187,6 +187,17 @@ public class ModdedGenerationLeaseContractTest {
assertTrue(logFailure.contains("cause.toString()"));
}
@Test
public void pregenReportsOnlyCleanupCallbacksFromCoveredTargets() throws IOException {
String source = source("art/arcane/iris/modded/command/ModdedPregenMethod.java");
String cleanup = method(source, "private void cleanupMantleChunksCoveredBy(");
assertTrue(source.contains("cleanupMantleChunksCoveredBy(x, z, listener);"));
assertFalse(source.contains("cleanupMantleChunk(x, z);"));
assertTrue(cleanup.contains("cleanupChunksCoveredBy(x, z, true, listener::onChunkCleaned)"));
assertFalse(cleanup.contains("forceCleanupChunk"));
}
@Test
public void invalidPregenBoundsFailBeforeRuntimeMutation() throws IOException {
String jobSource = source("art/arcane/iris/modded/command/ModdedPregenJob.java");
@@ -0,0 +1,30 @@
package art.arcane.iris.modded.command;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.volmlib.util.collection.KList;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class IrisModdedBiomeCommandTest {
@Test
public void biomeKeysAreSortedAndLimitedToReachableBiomes() {
KList<IrisBiome> reachable = new KList<>();
IrisBiome river = new IrisBiome();
river.setLoadKey("river");
IrisBiome natural = new IrisBiome();
natural.setLoadKey("natural");
reachable.add(river);
reachable.add(natural);
assertEquals(
List.of("natural", "river"),
List.copyOf(ModdedCommandSuggestions.reachableBiomeKeys(reachable))
);
assertTrue(ModdedCommandSuggestions.isReachableBiome(reachable, "river"));
assertFalse(ModdedCommandSuggestions.isReachableBiome(reachable, "unused"));
}
}
@@ -1,11 +1,27 @@
package art.arcane.iris.modded.service;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineMetrics;
import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.iris.util.project.matter.TileWrapper;
import art.arcane.volmlib.util.function.Consumer4;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.mantle.runtime.MantleDataAdapter;
import art.arcane.volmlib.util.mantle.runtime.MantleHooks;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterUpdate;
import art.arcane.volmlib.util.matter.slices.UpdateMatter;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.server.Bootstrap;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.LiquidBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.material.FlowingFluid;
import org.junit.Test;
import org.mockito.InOrder;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
@@ -15,8 +31,42 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class ModdedChunkUpdateServiceTest {
@Test
public void fallingFluidUpdateRetainsLevelEightStateAndRequestsPhysics() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
IrisWorld world = mock(IrisWorld.class);
when(world.minHeight()).thenReturn(-64);
Engine engine = mock(Engine.class);
when(engine.getWorld()).thenReturn(world);
when(engine.getMetrics()).thenReturn(new EngineMetrics(8));
ServerLevel level = mock(ServerLevel.class);
when(level.getMinY()).thenReturn(-64);
when(level.getMaxY()).thenReturn(320);
BlockPos position = new BlockPos(15, -59, 14);
BlockState falling = Blocks.WATER.defaultBlockState().setValue(LiquidBlock.LEVEL, 8);
when(level.getBlockState(position)).thenReturn(falling);
UpdateRecordingMantleChunk chunk = new UpdateRecordingMantleChunk();
new ModdedChunkUpdateService().runUpdatePass(engine, level, 0, 0, chunk);
assertEquals(8, falling.getValue(LiquidBlock.LEVEL).intValue());
if (falling.getFluidState().hasProperty(FlowingFluid.FALLING)) {
assertTrue(falling.getFluidState().getValue(FlowingFluid.FALLING));
}
InOrder order = inOrder(level);
order.verify(level).setBlock(eq(position), eq(Blocks.AIR.defaultBlockState()), anyInt());
order.verify(level).setBlock(position, falling, Block.UPDATE_ALL);
assertTrue(chunk.deleted());
}
@Test
public void scansWhenPlayersArePresent() {
assertTrue(ModdedChunkUpdateService.hasUpdateTargets(true, false));
@@ -84,6 +134,35 @@ public class ModdedChunkUpdateServiceTest {
}
}
private static final class UpdateRecordingMantleChunk extends MantleChunk<Matter> {
private boolean deleted;
private UpdateRecordingMantleChunk() {
super(1, 0, 0, emptyAdapter(), MantleHooks.NONE);
}
@Override
public <T> void iterate(
Class<T> type,
Consumer4<Integer, Integer, Integer, T> iterator
) {
if (type == MatterUpdate.class) {
iterator.accept(-1, 5, -2, type.cast(UpdateMatter.ON));
}
}
@Override
public void deleteSlices(Class<?> type) {
if (type == MatterUpdate.class) {
deleted = true;
}
}
private boolean deleted() {
return deleted;
}
}
@SuppressWarnings("unchecked")
private static MantleDataAdapter<Matter> emptyAdapter() {
return (MantleDataAdapter<Matter>) Proxy.newProxyInstance(