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;