Way too much

This commit is contained in:
Brian Neumann-Fopiano
2026-08-25 12:52:12 -04:00
parent 2797a5bf22
commit 07cc76614f
131 changed files with 6826 additions and 1094 deletions
+2
View File
@@ -53,6 +53,8 @@ service-account*.json
.qa/ .qa/
.repro/ .repro/
.perf/
.release-gate/
# Throwaway worktree copies used by the API / PlaceholderAPI rebuild lanes. Generated, never source. # Throwaway worktree copies used by the API / PlaceholderAPI rebuild lanes. Generated, never source.
.apiwt/ .apiwt/
@@ -11,6 +11,7 @@ import art.arcane.iris.engine.framework.GenerationSessionLease;
import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom; import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisDimensionCarvingResolver;
import art.arcane.iris.util.project.context.IrisContext; import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.volmlib.util.collection.KMap; import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.math.RNG;
@@ -521,6 +522,16 @@ public class CustomBiomeSource extends BiomeSource {
int y, int y,
int z, int z,
Climate.Sampler sampler Climate.Sampler sampler
) {
return getVisibleNoiseBiomeWithActiveGenerationLease(x, y, z, sampler, null);
}
Holder<Biome> getVisibleNoiseBiomeWithActiveGenerationLease(
int x,
int y,
int z,
Climate.Sampler sampler,
IrisDimensionCarvingResolver.State resolverState
) { ) {
long cacheKey = packNoiseKey(x, y, z); long cacheKey = packNoiseKey(x, y, z);
Holder<Biome> cachedHolder = noiseBiomeCache.get(cacheKey); Holder<Biome> cachedHolder = noiseBiomeCache.get(cacheKey);
@@ -528,7 +539,7 @@ public class CustomBiomeSource extends BiomeSource {
return cachedHolder; return cachedHolder;
} }
Holder<Biome> resolvedHolder = resolveVisibleBiomeHolder(x, y, z); Holder<Biome> resolvedHolder = resolveVisibleBiomeHolder(x, y, z, resolverState);
Holder<Biome> existingHolder = noiseBiomeCache.putIfAbsent(cacheKey, resolvedHolder); Holder<Biome> existingHolder = noiseBiomeCache.putIfAbsent(cacheKey, resolvedHolder);
if (existingHolder != null) { if (existingHolder != null) {
return existingHolder; return existingHolder;
@@ -602,8 +613,13 @@ public class CustomBiomeSource extends BiomeSource {
return holder; return holder;
} }
private Holder<Biome> resolveVisibleBiomeHolder(int x, int y, int z) { private Holder<Biome> resolveVisibleBiomeHolder(
BiomeResolution resolution = resolveBiomeResolution(x, y, z); int x,
int y,
int z,
IrisDimensionCarvingResolver.State resolverState
) {
BiomeResolution resolution = resolveBiomeResolution(x, y, z, resolverState);
if (resolution == null) { if (resolution == null) {
return getFallbackBiome(); return getFallbackBiome();
} }
@@ -636,6 +652,15 @@ public class CustomBiomeSource extends BiomeSource {
} }
private BiomeResolution resolveBiomeResolution(int x, int y, int z) { private BiomeResolution resolveBiomeResolution(int x, int y, int z) {
return resolveBiomeResolution(x, y, z, null);
}
private BiomeResolution resolveBiomeResolution(
int x,
int y,
int z,
IrisDimensionCarvingResolver.State resolverState
) {
if (engine == null || engine.isClosed()) { if (engine == null || engine.isClosed()) {
return null; return null;
} }
@@ -657,7 +682,7 @@ public class CustomBiomeSource extends BiomeSource {
int surfaceInternalY = engine.getComplex().getHeightStream().get(blockX, blockZ).intValue(); int surfaceInternalY = engine.getComplex().getHeightStream().get(blockX, blockZ).intValue();
underground = internalY <= surfaceInternalY - 8; underground = internalY <= surfaceInternalY - 8;
irisBiome = underground irisBiome = underground
? engine.getCaveBiome(blockX, internalY, blockZ) ? engine.getCaveBiome(blockX, internalY, blockZ, resolverState)
: engine.getComplex().getTrueBiomeStream().get(blockX, blockZ); : engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
} else { } else {
irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ); irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
@@ -10,6 +10,7 @@ import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
import art.arcane.iris.engine.IrisEngine; import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.platform.BukkitChunkGenerator; import art.arcane.iris.engine.platform.BukkitChunkGenerator;
import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisDimensionCarvingResolver;
import art.arcane.iris.engine.object.IrisMaterialPalette; import art.arcane.iris.engine.object.IrisMaterialPalette;
import art.arcane.iris.engine.object.IrisNativeStructureDecision; import art.arcane.iris.engine.object.IrisNativeStructureDecision;
import art.arcane.iris.nativegen.NativeStructureGenerationException; import art.arcane.iris.nativegen.NativeStructureGenerationException;
@@ -643,8 +644,10 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_create_biomes"); GenerationSessionLease lease = requireGenerationLease("bukkit_nms_create_biomes");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
customBiomeSource.prepareVisibleBiomeBatch(); customBiomeSource.prepareVisibleBiomeBatch();
IrisDimensionCarvingResolver.State resolverState = new IrisDimensionCarvingResolver.State();
ichunkaccess.fillBiomesFromNoise( ichunkaccess.fillBiomesFromNoise(
customBiomeSource::getVisibleNoiseBiomeWithActiveGenerationLease, (x, y, z, sampler) -> customBiomeSource.getVisibleNoiseBiomeWithActiveGenerationLease(
x, y, z, sampler, resolverState),
randomstate.sampler()); randomstate.sampler());
return CompletableFuture.completedFuture(ichunkaccess); return CompletableFuture.completedFuture(ichunkaccess);
} }
@@ -258,7 +258,8 @@ public class IrisChunkGeneratorFailureContractTest {
assertTrue(createBiomes.contains("requireGenerationStage(\"bukkit_nms_create_biomes\")")); assertTrue(createBiomes.contains("requireGenerationStage(\"bukkit_nms_create_biomes\")"));
assertTrue(createBiomes.contains("customBiomeSource.prepareVisibleBiomeBatch()")); assertTrue(createBiomes.contains("customBiomeSource.prepareVisibleBiomeBatch()"));
assertTrue(createBiomes.contains("customBiomeSource::getVisibleNoiseBiomeWithActiveGenerationLease")); assertTrue(createBiomes.contains("new IrisDimensionCarvingResolver.State()"));
assertTrue(createBiomes.contains("sampler, resolverState"));
assertTrue(buildSurface.contains("delegate.buildSurface(")); assertTrue(buildSurface.contains("delegate.buildSurface("));
assertTrue(carvers.contains("delegate.applyCarvers(")); assertTrue(carvers.contains("delegate.applyCarvers("));
assertTrue(noise.contains("requireNoiseGenerationStage(")); assertTrue(noise.contains("requireNoiseGenerationStage("));
@@ -573,7 +573,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
} }
private static String logPrefix(Iris plugin) { private static String logPrefix(Iris plugin) {
return plugin == null ? "[Iris] " : plugin.getTag(); return plugin == null ? ComponentLog.discriminator("Iris", "&a") : plugin.getTag();
} }
/** /**
@@ -9,6 +9,7 @@ import art.arcane.iris.core.lifecycle.WorldReplacementBootstrap;
import art.arcane.iris.core.lifecycle.WorldReplacementBootstrapMarker; import art.arcane.iris.core.lifecycle.WorldReplacementBootstrapMarker;
import art.arcane.iris.core.pack.DefaultPackBootstrapProvisioner; import art.arcane.iris.core.pack.DefaultPackBootstrapProvisioner;
import art.arcane.iris.core.pack.DefaultPackBootstrapProvisioner.ProvisionResult; import art.arcane.iris.core.pack.DefaultPackBootstrapProvisioner.ProvisionResult;
import art.arcane.iris.util.common.misc.SlimJar;
import io.papermc.paper.plugin.bootstrap.BootstrapContext; import io.papermc.paper.plugin.bootstrap.BootstrapContext;
import io.papermc.paper.plugin.bootstrap.PluginBootstrap; import io.papermc.paper.plugin.bootstrap.PluginBootstrap;
import io.papermc.paper.plugin.lifecycle.event.LifecycleEvent; import io.papermc.paper.plugin.lifecycle.event.LifecycleEvent;
@@ -26,6 +27,7 @@ public final class IrisBootstrap implements PluginBootstrap {
public void bootstrap(BootstrapContext context) { public void bootstrap(BootstrapContext context) {
WorldReplacementBootstrapMarker.markBootstrapped(); WorldReplacementBootstrapMarker.markBootstrapped();
try { try {
loadRuntimeLibraries(context);
BukkitStartupPaths startupPaths = BukkitStartupPaths.resolveCurrent(); BukkitStartupPaths startupPaths = BukkitStartupPaths.resolveCurrent();
reconcilePendingWorldReplacements(context, startupPaths); reconcilePendingWorldReplacements(context, startupPaths);
quarantineWorthlessHusks(startupPaths, message -> context.getLogger().warn(message)); quarantineWorthlessHusks(startupPaths, message -> context.getLogger().warn(message));
@@ -38,6 +40,27 @@ public final class IrisBootstrap implements PluginBootstrap {
} }
} }
private static void loadRuntimeLibraries(BootstrapContext context) {
SlimJar.loadBootstrap(
context.getDataDirectory().resolve("cache").resolve("libraries"),
new SlimJar.BootstrapLogger() {
@Override
public void info(String message) {
context.getLogger().info(message);
}
@Override
public void error(String message) {
context.getLogger().error(message);
}
@Override
public void debug(String message) {
context.getLogger().debug(message);
}
});
}
private static void reconcilePendingWorldReplacements( private static void reconcilePendingWorldReplacements(
BootstrapContext context, BootstrapContext context,
BukkitStartupPaths startupPaths BukkitStartupPaths startupPaths
@@ -0,0 +1,69 @@
package art.arcane.iris;
import art.arcane.iris.util.common.misc.SlimJar;
import io.papermc.paper.plugin.bootstrap.PluginProviderContext;
import io.papermc.paper.plugin.loader.PluginClasspathBuilder;
import io.papermc.paper.plugin.loader.PluginLoader;
import io.papermc.paper.plugin.loader.library.impl.JarLibrary;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Stream;
@SuppressWarnings("UnstableApiUsage")
public final class IrisPluginLoader implements PluginLoader {
@Override
public void classloader(PluginClasspathBuilder classpathBuilder) {
PluginProviderContext context = classpathBuilder.getContext();
Path libraryRoot = context.getDataDirectory().resolve("cache").resolve("libraries");
SlimJar.loadBootstrap(libraryRoot, new SlimJar.BootstrapLogger() {
@Override
public void info(String message) {
context.getLogger().info(message);
}
@Override
public void error(String message) {
context.getLogger().error(message);
}
@Override
public void debug(String message) {
context.getLogger().debug(message);
}
});
for (Path library : relocatedLibraries(libraryRoot)) {
classpathBuilder.addLibrary(new JarLibrary(library));
}
}
static List<Path> relocatedLibraries(Path libraryRoot) {
try (Stream<Path> paths = Files.walk(libraryRoot)) {
List<Path> libraries = paths
.filter(Files::isRegularFile)
.filter(path -> path.getFileName().toString().endsWith(".jar"))
.filter(IrisPluginLoader::isRelocatedLibrary)
.sorted()
.toList();
if (libraries.isEmpty()) {
throw new IllegalStateException("Iris runtime library provisioning produced no relocated libraries.");
}
return libraries;
} catch (IOException failure) {
throw new IllegalStateException("Unable to enumerate provisioned Iris runtime libraries.", failure);
}
}
private static boolean isRelocatedLibrary(Path path) {
int count = path.getNameCount();
for (int i = 0; i < count - 2; i++) {
if ("relocated".equals(path.getName(i).toString())
&& "Iris".equals(path.getName(i + 1).toString())) {
return true;
}
}
return false;
}
}
@@ -96,6 +96,7 @@ import java.time.temporal.ChronoUnit;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
@@ -412,7 +413,8 @@ public class CommandStudio implements DirectorExecutor {
return; return;
} }
VisionGUI.launch(IrisToolbelt.access(world).getEngine()); UUID openerId = sender().isPlayer() ? player().getUniqueId() : null;
VisionGUI.launch(IrisToolbelt.access(world).getEngine(), openerId);
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_MAP)); sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_MAP));
} }
@@ -30,6 +30,7 @@ import org.bukkit.event.Listener;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Map; import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
public final class BukkitGuiHost implements GuiHost.Provider { public final class BukkitGuiHost implements GuiHost.Provider {
@@ -72,8 +73,8 @@ public final class BukkitGuiHost implements GuiHost.Provider {
} }
@Override @Override
public GuiOverlay overlayFor(Engine engine) { public GuiOverlay overlayFor(Engine engine, UUID openerId) {
return engine == null ? null : new BukkitVisionOverlay(engine); return engine == null ? null : new BukkitVisionOverlay(engine, openerId);
} }
private static final class HotloadListener implements Listener { private static final class HotloadListener implements Listener {
@@ -18,7 +18,6 @@
package art.arcane.iris.core.gui; package art.arcane.iris.core.gui;
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
import art.arcane.iris.engine.IrisComplex; import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.render.RenderType; import art.arcane.iris.engine.framework.render.RenderType;
@@ -28,7 +27,6 @@ import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.util.common.scheduling.J; import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.format.Form; import art.arcane.volmlib.util.format.Form;
import org.bukkit.Chunk;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.World; import org.bukkit.World;
import org.bukkit.entity.LivingEntity; import org.bukkit.entity.LivingEntity;
@@ -39,6 +37,7 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.UUID;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
@@ -50,14 +49,16 @@ import static art.arcane.iris.util.common.data.registry.Attributes.MAX_HEALTH;
public final class BukkitVisionOverlay implements GuiOverlay { public final class BukkitVisionOverlay implements GuiOverlay {
private final Engine engine; private final Engine engine;
private final UUID openerId;
private final AtomicBoolean nativeTeleportActive = new AtomicBoolean(); private final AtomicBoolean nativeTeleportActive = new AtomicBoolean();
private final AtomicBoolean playerRefreshQueued = new AtomicBoolean(); private final AtomicBoolean playerRefreshQueued = new AtomicBoolean();
private final AtomicLong teleportSequence = new AtomicLong(); private final AtomicLong teleportSequence = new AtomicLong();
private final AtomicReference<VisionTeleportRequest> latestTeleport = new AtomicReference<>(); private final AtomicReference<VisionTeleportRequest> latestTeleport = new AtomicReference<>();
private volatile List<GuiMarker> playerMarkers = List.of(); private volatile List<GuiMarker> playerMarkers = List.of();
public BukkitVisionOverlay(Engine engine) { public BukkitVisionOverlay(Engine engine, UUID openerId) {
this.engine = engine; this.engine = engine;
this.openerId = openerId;
} }
/** /**
@@ -177,67 +178,20 @@ public final class BukkitVisionOverlay implements GuiOverlay {
return; return;
} }
List<Player> players = BukkitWorldBinding.players(target); List<Player> players = BukkitWorldBinding.players(target);
if (players.isEmpty()) { Player player = selectPlayer(players);
if (player == null) {
finish(request); finish(request);
return; return;
} }
Player player = players.get(0); int blockX = request.blockX;
requestTeleportChunk(request, target, player, world); int blockZ = request.blockZ;
}); try {
if (!scheduled) { int blockY = engine.getMinHeight() + engine.getHeight(blockX, blockZ, false) + 2;
finish(request); Location destination = new Location(
} world,
} blockX + 0.5D,
blockY,
private void requestTeleportChunk( blockZ + 0.5D);
VisionTeleportRequest request,
IrisWorld target,
Player player,
World world
) {
int blockX = request.blockX;
int blockZ = request.blockZ;
int chunkX = blockX >> 4;
int chunkZ = blockZ >> 4;
CompletableFuture<Chunk> requested;
try {
requested = WorldRuntimeControlService.get().requestChunkAsync(
world,
chunkX,
chunkZ,
true,
true
);
} catch (Throwable failure) {
fail(request, target, world, failure);
return;
}
if (requested == null) {
fail(request, target, world, new IllegalStateException(
"Vision destination chunk request returned no future."));
return;
}
requested.whenComplete((chunk, failure) -> {
if (!isCurrent(request, target)) {
finish(request);
return;
}
if (failure != null) {
fail(request, target, world, failure);
return;
}
if (chunk == null || chunk.getWorld() != world) {
fail(request, target, world, new IllegalStateException(
"Vision destination chunk request returned no chunk."));
return;
}
boolean scheduled = J.runRegion(world, chunkX, chunkZ, () -> {
if (!isCurrent(request, target)) {
finish(request);
return;
}
int yy = world.getHighestBlockYAt(blockX, blockZ) + 1;
Location destination = new Location(world, blockX, yy, blockZ);
if (!J.runEntity(player, () -> delegateTeleport( if (!J.runEntity(player, () -> delegateTeleport(
request, request,
target, target,
@@ -247,12 +201,25 @@ public final class BukkitVisionOverlay implements GuiOverlay {
fail(request, target, world, new IllegalStateException( fail(request, target, world, new IllegalStateException(
"Failed to schedule the Vision teleport on the player entity.")); "Failed to schedule the Vision teleport on the player entity."));
} }
}); } catch (Throwable failure) {
if (!scheduled) { fail(request, target, world, failure);
fail(request, target, world, new IllegalStateException(
"Failed to schedule the Vision surface lookup on its owning region."));
} }
}); });
if (!scheduled) {
finish(request);
}
}
private Player selectPlayer(List<Player> players) {
if (openerId == null) {
return players.isEmpty() ? null : players.get(0);
}
for (Player player : players) {
if (openerId.equals(player.getUniqueId())) {
return player;
}
}
return null;
} }
private void delegateTeleport( private void delegateTeleport(
@@ -2,6 +2,7 @@ name: ${name}
version: ${version} version: ${version}
main: ${main} main: ${main}
bootstrapper: ${bootstrapper} bootstrapper: ${bootstrapper}
loader: art.arcane.iris.IrisPluginLoader
folia-supported: true folia-supported: true
api-version: '${apiVersion}' api-version: '${apiVersion}'
load: STARTUP load: STARTUP
@@ -0,0 +1,34 @@
package art.arcane.iris;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.junit.Assert.assertEquals;
public class IrisPluginLoaderTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void exposesOnlyRelocatedRuntimeLibrariesInStableOrder() throws Exception {
Path root = temporaryFolder.newFolder("libraries").toPath();
Path original = Files.createDirectories(root.resolve("gson/2.14.0"))
.resolve("gson.jar");
Path relocatedGson = Files.createDirectories(root.resolve("gson/2.14.0/relocated/Iris"))
.resolve("gson.jar");
Path relocatedCaffeine = Files.createDirectories(root.resolve("caffeine/3.2.4/relocated/Iris"))
.resolve("caffeine.jar");
Files.createFile(original);
Files.createFile(relocatedGson);
Files.createFile(relocatedCaffeine);
List<Path> libraries = IrisPluginLoader.relocatedLibraries(root);
assertEquals(List.of(relocatedCaffeine, relocatedGson), libraries);
}
}
@@ -53,6 +53,7 @@ public class PaperPluginMetadataTest {
} }
assertTrue(metadata.contains("bootstrapper: " + IrisBootstrap.class.getName())); assertTrue(metadata.contains("bootstrapper: " + IrisBootstrap.class.getName()));
assertTrue(metadata.contains("loader: " + IrisPluginLoader.class.getName()));
assertTrue(metadata.contains("folia-supported: true")); assertTrue(metadata.contains("folia-supported: true"));
assertTrue(metadata.contains("load: STARTUP")); assertTrue(metadata.contains("load: STARTUP"));
assertFalse(metadata.contains("commands:")); assertFalse(metadata.contains("commands:"));
@@ -1,108 +1,67 @@
package art.arcane.iris.core.gui; package art.arcane.iris.core.gui;
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisWorld; import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.iris.platform.bukkit.BukkitPlatform; import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.platform.bukkit.BukkitWorldBinding; import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import art.arcane.iris.util.common.scheduling.J; import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.Chunk;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.World; import org.bukkit.World;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.junit.Test; import org.junit.Test;
import org.mockito.MockedStatic; import org.mockito.MockedStatic;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.same; import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
public class BukkitVisionOverlayFoliaContractTest { public class BukkitVisionOverlayFoliaContractTest {
@Test @Test
public void teleportLoadsTheDestinationChunkBeforeItsOwningRegionReadsTheSurface() throws Exception { public void teleportDelegatesImmediatelyToTheNativeAsyncPath() {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/gui/BukkitVisionOverlay.java"
)).replace("\r\n", "\n");
String request = method(source, "private void requestTeleportChunk(");
assertBefore(request, "requestChunkAsync(", "requested.whenComplete(");
assertBefore(request, "requested.whenComplete(", "J.runRegion(");
assertBefore(request, "J.runRegion(", "world.getHighestBlockYAt(");
assertBefore(request, "world.getHighestBlockYAt(", "J.runEntity(");
assertTrue(request.contains("chunkX,\n chunkZ,\n true,\n true"));
assertEquals(1, occurrences(request, "world.getHighestBlockYAt("));
}
@Test
public void teleportReportsAsyncAndSchedulingFailuresWithDestinationContext() throws Exception {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/gui/BukkitVisionOverlay.java"
)).replace("\r\n", "\n");
String request = method(source, "private void requestTeleportChunk(");
String reporter = method(source, "private void reportTeleportFailure(");
assertTrue(request.contains("if (requested == null)"));
assertTrue(request.contains("if (failure != null)"));
assertTrue(request.contains("if (chunk == null ||"));
assertTrue(request.contains("if (!J.runEntity("));
assertTrue(request.contains("if (!scheduled)"));
assertTrue(reporter.contains("IrisLogging.reportError("));
assertTrue(reporter.contains("world.getName()"));
assertTrue(reporter.contains("blockX + \",\" + blockZ"));
}
@Test
public void teleportObservesNativeCompletionAndRejectsFalseSettlement() throws Exception {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/gui/BukkitVisionOverlay.java"
)).replace("\r\n", "\n");
String delegate = method(source, "private void delegateTeleport(");
assertTrue(delegate.contains("teleport.whenComplete("));
assertTrue(delegate.contains("!Boolean.TRUE.equals(success)"));
assertTrue(delegate.contains("restartLatest(request)"));
}
@Test
public void staleChunkCompletionCannotTeleportOverTheLatestRequest() {
VisionHarness harness = new VisionHarness(); VisionHarness harness = new VisionHarness();
CompletableFuture<Chunk> firstChunk = new CompletableFuture<>();
CompletableFuture<Chunk> secondChunk = new CompletableFuture<>();
harness.stubChunk(0, 0, firstChunk);
harness.stubChunk(2, 2, secondChunk);
try (harness) { try (harness) {
harness.overlay.teleport(1.5D, 1.5D);
harness.overlay.teleport(33.5D, 33.5D); harness.overlay.teleport(33.5D, 33.5D);
firstChunk.complete(harness.chunk); assertEquals(1, harness.destinations.size());
assertEquals(0, harness.destinations.size()); Location destination = harness.destinations.get(0);
secondChunk.complete(harness.chunk); assertEquals(33.5D, destination.getX(), 0D);
assertEquals(76D, destination.getY(), 0D);
assertEquals(33.5D, destination.getZ(), 0D);
verify(harness.engine).getHeight(33, 33, false);
}
}
@Test
public void teleportFloorsNegativeCoordinatesBeforeCenteringTheDestination() {
VisionHarness harness = new VisionHarness();
try (harness) {
harness.overlay.teleport(-0.25D, -16.01D);
assertEquals(1, harness.destinations.size()); assertEquals(1, harness.destinations.size());
assertEquals(33, harness.destinations.get(0).getBlockX()); Location destination = harness.destinations.get(0);
assertEquals(33, harness.destinations.get(0).getBlockZ()); assertEquals(-0.5D, destination.getX(), 0D);
assertEquals(-16.5D, destination.getZ(), 0D);
verify(harness.engine).getHeight(-1, -17, false);
} }
} }
@Test @Test
public void latestRequestRunsAfterAnOlderNativeTeleportSettles() { public void latestRequestRunsAfterAnOlderNativeTeleportSettles() {
VisionHarness harness = new VisionHarness(); VisionHarness harness = new VisionHarness();
harness.stubChunk(0, 0, CompletableFuture.completedFuture(harness.chunk));
harness.stubChunk(2, 2, CompletableFuture.completedFuture(harness.chunk));
CompletableFuture<Boolean> firstTeleport = new CompletableFuture<>(); CompletableFuture<Boolean> firstTeleport = new CompletableFuture<>();
CompletableFuture<Boolean> secondTeleport = new CompletableFuture<>(); CompletableFuture<Boolean> secondTeleport = new CompletableFuture<>();
harness.nativeTeleports.add(firstTeleport); harness.nativeTeleports.add(firstTeleport);
@@ -121,42 +80,17 @@ public class BukkitVisionOverlayFoliaContractTest {
} }
} }
private static void assertBefore(String source, String first, String second) { @Test
int firstIndex = source.indexOf(first); public void missingOpenerDoesNotTeleportAnotherPlayer() {
int secondIndex = source.indexOf(second); VisionHarness harness = new VisionHarness();
assertTrue("Missing source contract token: " + first, firstIndex >= 0); harness.binding.when(() -> BukkitWorldBinding.players(harness.target))
assertTrue("Missing source contract token: " + second, secondIndex >= 0); .thenReturn(List.of(harness.otherPlayer));
assertTrue(first + " must occur before " + second, firstIndex < secondIndex);
}
private static int occurrences(String source, String match) { try (harness) {
int count = 0; harness.overlay.teleport(1.5D, 1.5D);
int offset = 0;
while ((offset = source.indexOf(match, offset)) >= 0) {
count++;
offset += match.length();
}
return count;
}
private static String method(String source, String signature) { assertEquals(0, harness.destinations.size());
int start = source.indexOf(signature);
assertTrue("Missing source contract signature: " + signature, start >= 0);
int openBrace = source.indexOf('{', start);
assertTrue("Missing source contract method body: " + signature, openBrace >= 0);
int depth = 0;
for (int index = openBrace; index < source.length(); index++) {
char current = source.charAt(index);
if (current == '{') {
depth++;
} else if (current == '}') {
depth--;
if (depth == 0) {
return source.substring(start, index + 1);
}
}
} }
throw new IllegalArgumentException("Unclosed source contract method: " + signature);
} }
private static final class VisionHarness implements AutoCloseable { private static final class VisionHarness implements AutoCloseable {
@@ -164,11 +98,10 @@ public class BukkitVisionOverlayFoliaContractTest {
private final IrisWorld target; private final IrisWorld target;
private final World world; private final World world;
private final Player player; private final Player player;
private final Chunk chunk; private final Player otherPlayer;
private final WorldRuntimeControlService runtime; private final UUID openerId;
private final MockedStatic<J> scheduling; private final MockedStatic<J> scheduling;
private final MockedStatic<BukkitWorldBinding> binding; private final MockedStatic<BukkitWorldBinding> binding;
private final MockedStatic<WorldRuntimeControlService> runtimeAccess;
private final MockedStatic<BukkitPlatform> platform; private final MockedStatic<BukkitPlatform> platform;
private final List<Location> destinations; private final List<Location> destinations;
private final List<CompletableFuture<Boolean>> nativeTeleports; private final List<CompletableFuture<Boolean>> nativeTeleports;
@@ -180,33 +113,26 @@ public class BukkitVisionOverlayFoliaContractTest {
target = mock(IrisWorld.class); target = mock(IrisWorld.class);
world = mock(World.class); world = mock(World.class);
player = mock(Player.class); player = mock(Player.class);
chunk = mock(Chunk.class); otherPlayer = mock(Player.class);
runtime = mock(WorldRuntimeControlService.class); openerId = UUID.randomUUID();
destinations = new ArrayList<>(); destinations = new ArrayList<>();
nativeTeleports = new ArrayList<>(); nativeTeleports = new ArrayList<>();
nativeTeleportIndex = new AtomicInteger(); nativeTeleportIndex = new AtomicInteger();
when(engine.getWorld()).thenReturn(target); when(engine.getWorld()).thenReturn(target);
when(engine.getMinHeight()).thenReturn(-64);
when(engine.getHeight(anyInt(), anyInt(), eq(false))).thenReturn(138);
when(target.hasPlatformWorld()).thenReturn(true); when(target.hasPlatformWorld()).thenReturn(true);
when(player.isOnline()).thenReturn(true); when(player.isOnline()).thenReturn(true);
when(player.getWorld()).thenReturn(world); when(player.getWorld()).thenReturn(world);
when(chunk.getWorld()).thenReturn(world); when(player.getUniqueId()).thenReturn(openerId);
when(world.getHighestBlockYAt(anyInt(), anyInt())).thenReturn(70); when(otherPlayer.getUniqueId()).thenReturn(UUID.randomUUID());
scheduling = mockStatic(J.class); scheduling = mockStatic(J.class);
scheduling.when(() -> J.runGlobal(any(Runnable.class))).thenAnswer(invocation -> { scheduling.when(() -> J.runGlobal(any(Runnable.class))).thenAnswer(invocation -> {
invocation.getArgument(0, Runnable.class).run(); invocation.getArgument(0, Runnable.class).run();
return true; return true;
}); });
scheduling.when(() -> J.runRegion(
same(world),
anyInt(),
anyInt(),
any(Runnable.class)))
.thenAnswer(invocation -> {
invocation.getArgument(3, Runnable.class).run();
return true;
});
scheduling.when(() -> J.runEntity(same(player), any(Runnable.class))).thenAnswer(invocation -> { scheduling.when(() -> J.runEntity(same(player), any(Runnable.class))).thenAnswer(invocation -> {
invocation.getArgument(1, Runnable.class).run(); invocation.getArgument(1, Runnable.class).run();
return true; return true;
@@ -214,10 +140,7 @@ public class BukkitVisionOverlayFoliaContractTest {
binding = mockStatic(BukkitWorldBinding.class); binding = mockStatic(BukkitWorldBinding.class);
binding.when(() -> BukkitWorldBinding.world(target)).thenReturn(world); binding.when(() -> BukkitWorldBinding.world(target)).thenReturn(world);
binding.when(() -> BukkitWorldBinding.players(target)).thenReturn(List.of(player)); binding.when(() -> BukkitWorldBinding.players(target)).thenReturn(List.of(otherPlayer, player));
runtimeAccess = mockStatic(WorldRuntimeControlService.class);
runtimeAccess.when(WorldRuntimeControlService::get).thenReturn(runtime);
platform = mockStatic(BukkitPlatform.class); platform = mockStatic(BukkitPlatform.class);
platform.when(() -> BukkitPlatform.teleportAsync(same(player), any(Location.class))) platform.when(() -> BukkitPlatform.teleportAsync(same(player), any(Location.class)))
@@ -228,26 +151,12 @@ public class BukkitVisionOverlayFoliaContractTest {
? nativeTeleports.get(index) ? nativeTeleports.get(index)
: CompletableFuture.completedFuture(true); : CompletableFuture.completedFuture(true);
}); });
overlay = new BukkitVisionOverlay(engine); overlay = new BukkitVisionOverlay(engine, openerId);
}
private void stubChunk(
int chunkX,
int chunkZ,
CompletableFuture<Chunk> requested
) {
when(runtime.requestChunkAsync(
same(world),
eq(chunkX),
eq(chunkZ),
eq(true),
eq(true))).thenReturn(requested);
} }
@Override @Override
public void close() { public void close() {
platform.close(); platform.close();
runtimeAccess.close();
binding.close(); binding.close();
scheduling.close(); scheduling.close();
} }
@@ -21,6 +21,7 @@ package art.arcane.iris.fabric;
import art.arcane.iris.modded.ModdedLoader; import art.arcane.iris.modded.ModdedLoader;
import art.arcane.iris.modded.service.ModdedTreeFellerService; import art.arcane.iris.modded.service.ModdedTreeFellerService;
import net.fabricmc.api.EnvType; import net.fabricmc.api.EnvType;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLevelEvents;
import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents; import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents;
import net.fabricmc.fabric.api.permission.v1.PermissionContextOwner; import net.fabricmc.fabric.api.permission.v1.PermissionContextOwner;
import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.api.FabricLoader;
@@ -71,6 +72,16 @@ public final class FabricModdedLoader implements ModdedLoader {
// map). Off-thread readers rely on the ModdedServerLevels snapshot, which the caller republishes. // map). Off-thread readers rely on the ModdedServerLevels snapshot, which the caller republishes.
} }
@Override
public void fireDynamicLevelLoad(MinecraftServer server, ServerLevel level) {
ServerLevelEvents.LOAD.invoker().onLevelLoad(server, level);
}
@Override
public void fireDynamicLevelUnload(MinecraftServer server, ServerLevel level) {
ServerLevelEvents.UNLOAD.invoker().onLevelUnload(server, level);
}
@Override @Override
public boolean clientEnvironment() { public boolean clientEnvironment() {
return FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT; return FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT;
@@ -28,6 +28,7 @@ import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.BlockState;
import net.minecraftforge.common.util.Result; import net.minecraftforge.common.util.Result;
import net.minecraftforge.event.level.BlockEvent; import net.minecraftforge.event.level.BlockEvent;
import net.minecraftforge.event.level.LevelEvent;
import net.minecraftforge.fml.ModList; import net.minecraftforge.fml.ModList;
import net.minecraftforge.fml.loading.FMLEnvironment; import net.minecraftforge.fml.loading.FMLEnvironment;
import net.minecraftforge.fml.loading.FMLLoader; import net.minecraftforge.fml.loading.FMLLoader;
@@ -77,6 +78,16 @@ public final class ForgeModdedLoader implements ModdedLoader {
server.markWorldsDirty(); server.markWorldsDirty();
} }
@Override
public void fireDynamicLevelLoad(MinecraftServer server, ServerLevel level) {
LevelEvent.Load.BUS.post(new LevelEvent.Load(level));
}
@Override
public void fireDynamicLevelUnload(MinecraftServer server, ServerLevel level) {
LevelEvent.Unload.BUS.post(new LevelEvent.Unload(level));
}
@Override @Override
public boolean clientEnvironment() { public boolean clientEnvironment() {
return FMLEnvironment.dist.isClient(); return FMLEnvironment.dist.isClient();
@@ -56,13 +56,16 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
public final class ModdedDimensionManager { public final class ModdedDimensionManager {
private static final int TELEPORT_WARM_RADIUS = 0;
private static final Object LOCK = new Object(); private static final Object LOCK = new Object();
private static final ConcurrentHashMap<String, Handle> HANDLES = new ConcurrentHashMap<>(); private static final ConcurrentHashMap<String, Handle> HANDLES = new ConcurrentHashMap<>();
private static final TicketType TELEPORT_WARM_TICKET = new TicketType(TicketType.NO_TIMEOUT, private static final TicketType TELEPORT_WARM_TICKET = new TicketType(TicketType.NO_TIMEOUT,
TicketType.FLAG_LOADING | TicketType.FLAG_KEEP_DIMENSION_ACTIVE); TicketType.FLAG_LOADING | TicketType.FLAG_KEEP_DIMENSION_ACTIVE);
private static final long TELEPORT_WARM_TIMEOUT_SECONDS = 30L; private static final long TELEPORT_TIMEOUT_SECONDS = 10L;
private static volatile ModdedServerAccess access; private static volatile ModdedServerAccess access;
private ModdedDimensionManager() { private ModdedDimensionManager() {
@@ -213,15 +216,16 @@ public final class ModdedDimensionManager {
instanceof IrisModdedChunkGenerator irisGenerator instanceof IrisModdedChunkGenerator irisGenerator
? irisGenerator ? irisGenerator
: null; : null;
boolean generatorUnbound = false; boolean unloadEventStarted = false;
try { try {
evacuate(server, level); evacuate(server, level);
level.save(null, true, false);
unloadEventStarted = true;
ModdedEngineBootstrap.loader().fireDynamicLevelUnload(server, level);
if (generator != null) { if (generator != null) {
generator.unbindEngine(level); generator.unbindEngine(level);
generatorUnbound = true;
} }
ModdedWorldEngines.evictOrThrow(level); ModdedWorldEngines.evictOrThrow(level);
level.save(null, true, false);
serverAccess.removeLevel(server, key); serverAccess.removeLevel(server, key);
// Undo snapshots pin the ServerLevel and could replay into the dead level. // Undo snapshots pin the ServerLevel and could replay into the dead level.
art.arcane.iris.modded.command.ModdedObjectUndo.forget(level); art.arcane.iris.modded.command.ModdedObjectUndo.forget(level);
@@ -233,7 +237,7 @@ public final class ModdedDimensionManager {
ModdedIrisLog.info("Iris removed runtime dimension '{}'", dimensionId); ModdedIrisLog.info("Iris removed runtime dimension '{}'", dimensionId);
return true; return true;
} catch (Throwable e) { } catch (Throwable e) {
rollbackRemoval(server, serverAccess, key, level, generator, generatorUnbound, e); rollbackRemoval(server, serverAccess, key, level, generator, unloadEventStarted, e);
ModdedIrisLog.error("Iris failed to remove runtime dimension '{}'", dimensionId, e); ModdedIrisLog.error("Iris failed to remove runtime dimension '{}'", dimensionId, e);
throw new IllegalStateException("Iris runtime dimension removal failed for " + dimensionId, e); throw new IllegalStateException("Iris runtime dimension removal failed for " + dimensionId, e);
} }
@@ -242,13 +246,16 @@ public final class ModdedDimensionManager {
private static void rollbackRemoval(MinecraftServer server, ModdedServerAccess serverAccess, private static void rollbackRemoval(MinecraftServer server, ModdedServerAccess serverAccess,
ResourceKey<Level> key, ServerLevel level, ResourceKey<Level> key, ServerLevel level,
IrisModdedChunkGenerator generator, boolean generatorUnbound, IrisModdedChunkGenerator generator, boolean unloadEventStarted,
Throwable failure) { Throwable failure) {
try { try {
if (!generatorUnbound || generator == null || !serverAccess.hasLevel(server, key)) { if (!unloadEventStarted || !serverAccess.hasLevel(server, key)) {
return; return;
} }
generator.bindLevel(level); if (generator != null) {
generator.bindLevel(level);
}
ModdedEngineBootstrap.loader().fireDynamicLevelLoad(server, level);
} catch (Throwable rollbackFailure) { } catch (Throwable rollbackFailure) {
if (rollbackFailure != failure) { if (rollbackFailure != failure) {
failure.addSuppressed(rollbackFailure); failure.addSuppressed(rollbackFailure);
@@ -258,44 +265,263 @@ public final class ModdedDimensionManager {
} }
} }
public static boolean teleport(ServerPlayer player, MinecraftServer server, String dimensionId, double x, double y, double z) { public static CompletableFuture<Boolean> teleportAsync(
ServerLevel level = level(server, dimensionId); ServerPlayer player,
if (level == null) { MinecraftServer server,
return false; String dimensionId,
} double x,
int blockX = (int) Math.floor(x); double y,
int blockZ = (int) Math.floor(z); double z
ChunkPos chunkPos = new ChunkPos(blockX >> 4, blockZ >> 4); ) {
if (level.getChunkSource().hasChunk(chunkPos.x(), chunkPos.z())) { return teleportAsync(player, server, dimensionId, x, y, z,
completeTeleport(player, level, x, y, z, blockX, blockZ); System.nanoTime() + TimeUnit.SECONDS.toNanos(TELEPORT_TIMEOUT_SECONDS));
return true;
}
UUID playerId = player.getUUID();
CompletableFuture
.supplyAsync(() -> level.getChunkSource().addTicketAndLoadWithRadius(TELEPORT_WARM_TICKET, chunkPos, 1), server)
.thenCompose((CompletableFuture<?> inner) -> inner)
// The ticket has no timeout of its own: bound the wait so the release below always runs.
.orTimeout(TELEPORT_WARM_TIMEOUT_SECONDS, TimeUnit.SECONDS)
.whenComplete((Object result, Throwable error) -> server.execute(() -> {
level.getChunkSource().removeTicketWithRadius(TELEPORT_WARM_TICKET, chunkPos, 1);
if (error != null) {
ModdedIrisLog.warn("Iris chunk warm for teleport into '{}' at {},{} failed: {}", dimensionId, chunkPos.x(), chunkPos.z(), error.toString());
}
ServerPlayer target = server.getPlayerList().getPlayer(playerId);
if (target == null) {
return;
}
completeTeleport(target, level, x, y, z, blockX, blockZ);
}));
return true;
} }
private static void completeTeleport(ServerPlayer player, ServerLevel level, double x, double y, double z, int blockX, int blockZ) { public static CompletableFuture<Boolean> teleportAsync(
double targetY = y; ServerPlayer player,
if (y == Double.MIN_VALUE) { MinecraftServer server,
targetY = level.getHeight(Heightmap.Types.MOTION_BLOCKING, blockX, blockZ); String dimensionId,
double x,
double y,
double z,
long deadlineNanos
) {
ServerLevel level = level(server, dimensionId);
if (level == null) {
return CompletableFuture.completedFuture(false);
} }
player.teleportTo(level, x, targetY, z, Set.<Relative>of(), player.getYRot(), player.getXRot(), false); return teleportAsync(player, server, level, x, y, z, deadlineNanos);
}
public static CompletableFuture<Boolean> teleportAsync(
ServerPlayer player,
MinecraftServer server,
ServerLevel level,
double x,
double y,
double z
) {
return teleportAsync(player, server, level, x, y, z, 0L);
}
public static CompletableFuture<Boolean> teleportAsync(
ServerPlayer player,
MinecraftServer server,
ServerLevel level,
double x,
double y,
double z,
long deadlineNanos
) {
CompletableFuture<Boolean> result = new CompletableFuture<>();
if (player == null || server == null || level == null || level.getServer() != server) {
result.complete(false);
return result;
}
if (!Double.isFinite(x) || !Double.isFinite(z)
|| (y != Double.MIN_VALUE && !Double.isFinite(y))) {
result.completeExceptionally(new IllegalArgumentException("Teleport coordinates must be finite."));
return result;
}
if (deadlineNanos != 0L) {
long remainingNanos = deadlineNanos - System.nanoTime();
if (remainingNanos <= 0L) {
result.completeExceptionally(teleportTimeout(level, x, z));
return result;
}
result.orTimeout(remainingNanos, TimeUnit.NANOSECONDS);
}
UUID playerId = player.getUUID();
runOnServer(server, () -> beginTeleport(
result,
playerId,
server,
level,
x,
y,
z,
deadlineNanos));
return result;
}
private static void beginTeleport(
CompletableFuture<Boolean> result,
UUID playerId,
MinecraftServer server,
ServerLevel level,
double x,
double y,
double z,
long deadlineNanos
) {
if (result.isDone()) {
return;
}
if (deadlineNanos != 0L && System.nanoTime() >= deadlineNanos) {
result.completeExceptionally(teleportTimeout(level, x, z));
return;
}
ServerLevel active = level(server, level.dimension().identifier().toString());
if (active != level) {
result.complete(false);
return;
}
int blockX = ModdedTeleportBounds.blockCoordinate(x);
int blockZ = ModdedTeleportBounds.blockCoordinate(z);
ChunkPos chunkPos = new ChunkPos(blockX >> 4, blockZ >> 4);
if (level.getChunkSource().hasChunk(chunkPos.x(), chunkPos.z())) {
completeTeleport(result, playerId, server, level, x, y, z, blockX, blockZ, deadlineNanos);
return;
}
warmAndTeleport(result, playerId, server, level, x, y, z, blockX, blockZ, chunkPos, deadlineNanos);
}
private static void warmAndTeleport(
CompletableFuture<Boolean> result,
UUID playerId,
MinecraftServer server,
ServerLevel level,
double x,
double y,
double z,
int blockX,
int blockZ,
ChunkPos chunkPos,
long deadlineNanos
) {
AtomicBoolean ticketReleased = new AtomicBoolean();
CompletableFuture<?> chunkLoad;
try {
chunkLoad = level.getChunkSource().addTicketAndLoadWithRadius(
TELEPORT_WARM_TICKET,
chunkPos,
TELEPORT_WARM_RADIUS);
} catch (Throwable failure) {
result.completeExceptionally(failure);
return;
}
if (chunkLoad == null) {
releaseTeleportTicket(level, chunkPos, ticketReleased);
result.completeExceptionally(new IllegalStateException(
"Chunk warm returned no completion future for " + level.dimension().identifier()
+ " at " + chunkPos.x() + "," + chunkPos.z() + "."));
return;
}
result.whenComplete((success, failure) -> runOnServer(server,
() -> releaseTeleportTicket(level, chunkPos, ticketReleased)));
chunkLoad.whenComplete((ignored, failure) -> runOnServer(server, () -> {
releaseTeleportTicket(level, chunkPos, ticketReleased);
if (result.isDone()) {
return;
}
if (failure != null) {
result.completeExceptionally(failure);
return;
}
completeTeleport(result, playerId, server, level, x, y, z, blockX, blockZ, deadlineNanos);
}));
}
private static void releaseTeleportTicket(
ServerLevel level,
ChunkPos chunkPos,
AtomicBoolean ticketReleased
) {
if (ticketReleased.compareAndSet(false, true)) {
level.getChunkSource().removeTicketWithRadius(
TELEPORT_WARM_TICKET,
chunkPos,
TELEPORT_WARM_RADIUS);
}
}
private static void completeTeleport(
CompletableFuture<Boolean> result,
UUID playerId,
MinecraftServer server,
ServerLevel level,
double x,
double y,
double z,
int blockX,
int blockZ,
long deadlineNanos
) {
if (result.isDone()) {
return;
}
if (deadlineNanos != 0L && System.nanoTime() >= deadlineNanos) {
result.completeExceptionally(teleportTimeout(level, x, z));
return;
}
ServerLevel active = level(server, level.dimension().identifier().toString());
ServerPlayer player = server.getPlayerList().getPlayer(playerId);
if (active != level || player == null) {
result.complete(false);
return;
}
try {
int targetY = resolveSafeTeleportY(level, blockX, blockZ, y);
boolean teleported = player.teleportTo(
level,
x,
targetY,
z,
Set.<Relative>of(),
player.getYRot(),
player.getXRot(),
false);
result.complete(teleported);
} catch (Throwable failure) {
result.completeExceptionally(failure);
}
}
private static int resolveSafeTeleportY(ServerLevel level, int blockX, int blockZ, double requestedY) {
int initialY = requestedY == Double.MIN_VALUE
? level.getHeight(Heightmap.Types.MOTION_BLOCKING, blockX, blockZ)
: (int) Math.floor(requestedY);
int startY = ModdedTeleportBounds.clampY(level.getMinY(), level.getMaxY(), initialY);
int maximumY = ModdedTeleportBounds.maximumY(level.getMinY(), level.getMaxY());
int minimumY = ModdedTeleportBounds.minimumY(level.getMinY(), level.getMaxY());
for (int candidateY = startY; candidateY <= maximumY; candidateY++) {
if (isSafeStandingPosition(level, blockX, candidateY, blockZ)) {
return candidateY;
}
}
for (int candidateY = startY - 1; candidateY >= minimumY; candidateY--) {
if (isSafeStandingPosition(level, blockX, candidateY, blockZ)) {
return candidateY;
}
}
throw new IllegalStateException("No safe teleport position exists in "
+ level.dimension().identifier() + " at " + blockX + "," + blockZ + ".");
}
private static boolean isSafeStandingPosition(ServerLevel level, int blockX, int blockY, int blockZ) {
BlockPos feet = new BlockPos(blockX, blockY, blockZ);
BlockPos head = feet.above();
BlockPos support = feet.below();
return level.getBlockState(feet).getCollisionShape(level, feet).isEmpty()
&& level.getBlockState(feet).getFluidState().isEmpty()
&& level.getBlockState(head).getCollisionShape(level, head).isEmpty()
&& level.getBlockState(head).getFluidState().isEmpty()
&& !level.getBlockState(support).getCollisionShape(level, support).isEmpty();
}
private static TimeoutException teleportTimeout(ServerLevel level, double x, double z) {
return new TimeoutException("Teleport into " + level.dimension().identifier()
+ " at " + ModdedTeleportBounds.blockCoordinate(x) + ","
+ ModdedTeleportBounds.blockCoordinate(z)
+ " exceeded " + TELEPORT_TIMEOUT_SECONDS + " seconds.");
}
private static void runOnServer(MinecraftServer server, Runnable task) {
if (server.isSameThread()) {
task.run();
return;
}
server.execute(task);
} }
private static Holder<DimensionType> resolveDimensionType(RegistryAccess registryAccess, String pack, String packDimensionKey) { private static Holder<DimensionType> resolveDimensionType(RegistryAccess registryAccess, String pack, String packDimensionKey) {
@@ -362,6 +588,7 @@ public final class ModdedDimensionManager {
List.of(), List.of(),
false); false);
boolean loadEventStarted = false;
try { try {
generator.bindLevel(level); generator.bindLevel(level);
Handle handle = new Handle(dimensionId, pack, packDimensionKey, seed, level, generator); Handle handle = new Handle(dimensionId, pack, packDimensionKey, seed, level, generator);
@@ -371,9 +598,11 @@ public final class ModdedDimensionManager {
+ "': the level was registered concurrently"); + "': the level was registered concurrently");
} }
server.getPlayerList().addWorldborderListener(level); server.getPlayerList().addWorldborderListener(level);
loadEventStarted = true;
ModdedEngineBootstrap.loader().fireDynamicLevelLoad(server, level);
return handle; return handle;
} catch (Throwable error) { } catch (Throwable error) {
rollbackInjection(server, serverAccess, key, level, generator, error); rollbackInjection(server, serverAccess, key, level, generator, loadEventStarted, error);
if (error instanceof RuntimeException runtimeException) { if (error instanceof RuntimeException runtimeException) {
throw runtimeException; throw runtimeException;
} }
@@ -386,7 +615,17 @@ public final class ModdedDimensionManager {
private static void rollbackInjection(MinecraftServer server, ModdedServerAccess serverAccess, private static void rollbackInjection(MinecraftServer server, ModdedServerAccess serverAccess,
ResourceKey<Level> key, ServerLevel level, ResourceKey<Level> key, ServerLevel level,
IrisModdedChunkGenerator generator, Throwable failure) { IrisModdedChunkGenerator generator, boolean loadEventStarted,
Throwable failure) {
if (loadEventStarted) {
try {
ModdedEngineBootstrap.loader().fireDynamicLevelUnload(server, level);
} catch (Throwable cleanupError) {
failure.addSuppressed(cleanupError);
ModdedIrisLog.error("Iris failed to publish rollback unload for {}",
key.identifier(), cleanupError);
}
}
try { try {
if (serverAccess.hasLevel(server, key)) { if (serverAccess.hasLevel(server, key)) {
ServerLevel removed = serverAccess.removeLevel(server, key); ServerLevel removed = serverAccess.removeLevel(server, key);
@@ -38,6 +38,10 @@ public interface ModdedLoader {
void invalidateLevelCache(MinecraftServer server); void invalidateLevelCache(MinecraftServer server);
void fireDynamicLevelLoad(MinecraftServer server, ServerLevel level);
void fireDynamicLevelUnload(MinecraftServer server, ServerLevel level);
boolean clientEnvironment(); boolean clientEnvironment();
Path configDir(); Path configDir();
@@ -26,12 +26,14 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
public final class ModdedPrimaryWorldRouter { public final class ModdedPrimaryWorldRouter {
private static final int TICK_INTERVAL = 20; private static final int TICK_INTERVAL = 20;
private static final Set<UUID> routed = ConcurrentHashMap.newKeySet(); private static final Set<UUID> routed = ConcurrentHashMap.newKeySet();
private static final Set<UUID> inFlight = ConcurrentHashMap.newKeySet();
private static int tickCounter = 0; private static int tickCounter = 0;
private ModdedPrimaryWorldRouter() { private ModdedPrimaryWorldRouter() {
@@ -39,6 +41,7 @@ public final class ModdedPrimaryWorldRouter {
public static void clear() { public static void clear() {
routed.clear(); routed.clear();
inFlight.clear();
} }
/** /**
@@ -48,6 +51,7 @@ public final class ModdedPrimaryWorldRouter {
public static void forget(UUID player) { public static void forget(UUID player) {
if (player != null) { if (player != null) {
routed.remove(player); routed.remove(player);
inFlight.remove(player);
} }
} }
@@ -82,17 +86,35 @@ public final class ModdedPrimaryWorldRouter {
List<ServerPlayer> players = new ArrayList<>(server.getPlayerList().getPlayers()); List<ServerPlayer> players = new ArrayList<>(server.getPlayerList().getPlayers());
for (ServerPlayer player : players) { for (ServerPlayer player : players) {
UUID id = player.getUUID(); UUID id = player.getUUID();
if (routed.contains(id)) { if (routed.contains(id) || !inFlight.add(id)) {
continue; continue;
} }
if (player.level() != overworld) { if (player.level() != overworld) {
inFlight.remove(id);
routed.add(id); routed.add(id);
continue; continue;
} }
try { try {
ModdedDimensionManager.teleport(player, server, primary, player.getX(), Double.MIN_VALUE, player.getZ()); CompletableFuture<Boolean> teleport = ModdedDimensionManager.teleportAsync(
routed.add(id); player,
server,
primary,
player.getX(),
Double.MIN_VALUE,
player.getZ());
teleport.whenComplete((success, failure) -> {
inFlight.remove(id);
if (Boolean.TRUE.equals(success) && failure == null) {
routed.add(id);
return;
}
if (failure != null) {
ModdedIrisLog.error("Iris failed to route player {} to primary world '{}'",
id, primary, failure);
}
});
} catch (Throwable e) { } catch (Throwable e) {
inFlight.remove(id);
ModdedIrisLog.error("Iris failed to route player {} to primary world '{}'", id, primary, e); ModdedIrisLog.error("Iris failed to route player {} to primary world '{}'", id, primary, e);
} }
} }
@@ -0,0 +1,50 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded;
final class ModdedTeleportBounds {
private ModdedTeleportBounds() {
}
static int blockCoordinate(double coordinate) {
return (int) Math.floor(coordinate);
}
static int clampY(int minY, int maxY, int requestedY) {
int minimumY = minimumY(minY, maxY);
int maximumY = maximumY(minY, maxY);
return Math.max(minimumY, Math.min(maximumY, requestedY));
}
static int minimumY(int minY, int maxY) {
requireStandingSpace(minY, maxY);
return minY + 1;
}
static int maximumY(int minY, int maxY) {
requireStandingSpace(minY, maxY);
return maxY - 2;
}
private static void requireStandingSpace(int minY, int maxY) {
if (maxY - minY < 3) {
throw new IllegalArgumentException("Level height must provide support, feet, and head space.");
}
}
}
@@ -131,10 +131,26 @@ public final class IrisModdedCommands {
return 0; return 0;
} }
String dimensionId = level.dimension().identifier().toString(); String dimensionId = level.dimension().identifier().toString();
if (!ModdedDimensionManager.teleport(player, source.getServer(), dimensionId, 8.5D, Double.MIN_VALUE, 8.5D)) { MinecraftServer server = source.getServer();
fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_TELEPORT_FAILED_DIMENSION_IS_NOT_LOADED, MessageArgument.untrusted("dimensionId", dimensionId))); CompletableFuture<Boolean> teleport = ModdedDimensionManager.teleportAsync(
return 0; player,
} server,
dimensionId,
8.5D,
Double.MIN_VALUE,
8.5D);
teleport.whenComplete((success, failure) -> {
if (Boolean.TRUE.equals(success) && failure == null) {
return;
}
if (failure != null) {
ModdedIrisLog.error("Iris teleport into '{}' failed for {}",
dimensionId, player.getUUID(), failure);
}
server.execute(() -> fail(source, IrisLanguage.plain(
ModdedCommandMessages.IRIS_MODDED_COMMANDS_TELEPORT_FAILED_DIMENSION_IS_NOT_LOADED,
MessageArgument.untrusted("dimensionId", dimensionId))));
});
ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_TELEPORTING, MessageArgument.untrusted("value", player.getScoreboardName()), MessageArgument.untrusted("dimensionId", dimensionId))); ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_TELEPORTING, MessageArgument.untrusted("value", player.getScoreboardName()), MessageArgument.untrusted("dimensionId", dimensionId)));
return 1; return 1;
} }
@@ -107,7 +107,7 @@ public final class ModdedGuiHost implements GuiHost.Provider {
} }
@Override @Override
public GuiOverlay overlayFor(Engine engine) { public GuiOverlay overlayFor(Engine engine, UUID openerId) {
if (engine == null) { if (engine == null) {
return null; return null;
} }
@@ -115,6 +115,7 @@ public final class ModdedGuiHost implements GuiHost.Provider {
if (level == null || server == null) { if (level == null || server == null) {
return null; return null;
} }
return new ModdedVisionOverlay(server, level, engine, openers.get(engine)); UUID resolvedOpenerId = openerId == null ? openers.get(engine) : openerId;
return new ModdedVisionOverlay(server, level, engine, resolvedOpenerId);
} }
} }
@@ -44,6 +44,7 @@ import art.arcane.iris.engine.object.IrisSpawner;
import art.arcane.iris.engine.object.IrisStructurePlacement; import art.arcane.iris.engine.object.IrisStructurePlacement;
import art.arcane.iris.modded.ModdedDimensionManager; import art.arcane.iris.modded.ModdedDimensionManager;
import art.arcane.iris.modded.ModdedEngineBootstrap; import art.arcane.iris.modded.ModdedEngineBootstrap;
import art.arcane.iris.modded.ModdedScheduler;
import art.arcane.iris.modded.ModdedWorkspaceGenerator; import art.arcane.iris.modded.ModdedWorkspaceGenerator;
import art.arcane.iris.util.common.parallel.BurstExecutor; import art.arcane.iris.util.common.parallel.BurstExecutor;
import art.arcane.iris.util.common.parallel.MultiBurst; import art.arcane.iris.util.common.parallel.MultiBurst;
@@ -67,7 +68,6 @@ import net.minecraft.commands.SharedSuggestionProvider;
import net.minecraft.server.MinecraftServer; import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Relative;
import org.zeroturnaround.zip.ZipUtil; import org.zeroturnaround.zip.ZipUtil;
import java.awt.Desktop; import java.awt.Desktop;
@@ -79,6 +79,8 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Predicate; import java.util.function.Predicate;
@@ -96,6 +98,7 @@ public final class ModdedStudioCommands {
private static final String DEFAULT_TEMPLATE = "example"; private static final String DEFAULT_TEMPLATE = "example";
private static final UUID CONSOLE_OWNER = new UUID(0L, 0L); private static final UUID CONSOLE_OWNER = new UUID(0L, 0L);
private static final Map<UUID, String> STUDIOS = new ConcurrentHashMap<>(); private static final Map<UUID, String> STUDIOS = new ConcurrentHashMap<>();
private static final ModdedStudioTransitionQueue TRANSITIONS = new ModdedStudioTransitionQueue();
private static final SuggestionProvider<CommandSourceStack> GENERATOR_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> { private static final SuggestionProvider<CommandSourceStack> GENERATOR_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> {
ModdedCommandFeedback.tab(context.getSource()); ModdedCommandFeedback.tab(context.getSource());
try { try {
@@ -192,6 +195,7 @@ public final class ModdedStudioCommands {
} }
public static void clear() { public static void clear() {
TRANSITIONS.clear();
STUDIOS.clear(); STUDIOS.clear();
} }
@@ -272,7 +276,7 @@ public final class ModdedStudioCommands {
} }
ServerPlayer player = source.getPlayer(); ServerPlayer player = source.getPlayer();
ModdedGuiHost.bindContext(source.getServer(), level, engine, player == null ? null : player.getUUID()); ModdedGuiHost.bindContext(source.getServer(), level, engine, player == null ? null : player.getUUID());
VisionGUI.launch(engine); VisionGUI.launch(engine, player == null ? null : player.getUUID());
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_OPENING_VISION_MAP_ON_SERVER_DISPLAY, MessageArgument.untrusted("value", level.dimension().identifier()))); IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_OPENING_VISION_MAP_ON_SERVER_DISPLAY, MessageArgument.untrusted("value", level.dimension().identifier())));
return 1; return 1;
} }
@@ -402,126 +406,244 @@ public final class ModdedStudioCommands {
String dimensionId = player == null ? studioConsoleDimensionId() : studioDimensionId(player); String dimensionId = player == null ? studioConsoleDimensionId() : studioDimensionId(player);
MinecraftServer server = source.getServer(); MinecraftServer server = source.getServer();
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_OPENING_STUDIO_SEED, MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("seed", seed))); IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_OPENING_STUDIO_SEED, MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("seed", seed)));
Thread thread = new Thread(() -> openAsync(source, server, owner, dimensionId, pack, seed), "Iris Studio Open"); CompletableFuture<Void> transition = TRANSITIONS.submit(
thread.setDaemon(true); owner,
thread.start(); () -> openTransition(source, server, owner, dimensionId, pack, seed));
reportOpenFailure(source, server, dimensionId, pack, transition);
return 1; return 1;
} }
private static void openAsync(CommandSourceStack source, MinecraftServer server, UUID owner, String dimensionId, String pack, long seed) { private static CompletableFuture<Void> openTransition(
CommandSourceStack source,
MinecraftServer server,
UUID owner,
String dimensionId,
String pack,
long seed
) {
CompletableFuture<Void> transition = new CompletableFuture<>();
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
if (scheduler == null) {
transition.completeExceptionally(new IllegalStateException(
"Iris modded scheduler is unavailable for Studio open."));
return transition;
}
scheduler.asyncIfRunning(() -> prepareStudioOpen(
source,
server,
owner,
dimensionId,
pack,
seed,
transition),
() -> transition.completeExceptionally(new IllegalStateException(
"Iris modded scheduler rejected Studio open.")));
return transition;
}
private static void prepareStudioOpen(
CommandSourceStack source,
MinecraftServer server,
UUID owner,
String dimensionId,
String pack,
long seed,
CompletableFuture<Void> transition
) {
try { try {
File packFolder = new File(ModdedPackCommands.packsRoot(), pack); File packFolder = new File(ModdedPackCommands.packsRoot(), pack);
if (!new File(packFolder, "dimensions/" + pack + ".json").isFile()) { if (!new File(packFolder, "dimensions/" + pack + ".json").isFile()) {
server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain( throw new IllegalStateException("Required Studio pack '" + pack
ModdedCommandMessages.MODDED_STUDIO_COMMANDS_REQUIRED_PACK_IS_NOT_INSTALLED_INSTALL_THEN_RESTART, + "' is not installed with dimensions/" + pack + ".json.");
MessageArgument.untrusted("pack", pack))));
return;
} }
IrisData data = IrisData.get(packFolder); IrisData data = IrisData.get(packFolder);
IrisDimension dimension = data.getDimensionLoader().load(pack); IrisDimension dimension = data.getDimensionLoader().load(pack);
if (dimension == null) { if (dimension == null) {
server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_PACK_HAS_NO_DIMENSIONS_JSON, MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("pack2", pack)))); throw new IllegalStateException("Studio pack '" + pack
return; + "' has no dimensions/" + pack + ".json definition.");
} }
try { ModdedWorkspaceGenerator.writeWorkspace(data, packFolder, true);
ModdedWorkspaceGenerator.writeWorkspace(data, packFolder, true); server.execute(() -> executeStudioOpen(
} catch (Throwable workspaceError) { source,
ModdedIrisLog.error("Iris workspace write failed for {}", packFolder, workspaceError); server,
server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain( owner,
ModdedCommandMessages.MODDED_STUDIO_COMMANDS_FAILED_WRITE_WORKSPACE, dimensionId,
MessageArgument.untrusted("value", packFolder.getAbsolutePath()), pack,
MessageArgument.untrusted("value2", String.valueOf(workspaceError.getMessage()))))); seed,
} transition));
server.execute(() -> { } catch (Throwable failure) {
if (owner.equals(CONSOLE_OWNER)) { transition.completeExceptionally(failure);
injectConsole(source, server, dimensionId, pack, seed);
} else {
injectAndTeleport(source, server, owner, dimensionId, pack, seed);
}
});
} catch (Throwable e) {
ModdedIrisLog.error("Iris studio open failed for {}", pack, e);
server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_STUDIO_OPEN_FAILED, MessageArgument.untrusted("value", e.getClass().getSimpleName()), MessageArgument.trusted("errorMessage", IrisLanguage.errorDetail(e)))));
} }
} }
private static void injectConsole(CommandSourceStack source, MinecraftServer server, String dimensionId, String pack, long seed) { private static void executeStudioOpen(
ModdedDimensionManager.Handle handle; CommandSourceStack source,
MinecraftServer server,
UUID owner,
String dimensionId,
String pack,
long seed,
CompletableFuture<Void> transition
) {
ModdedDimensionManager.Handle handle = null;
try { try {
replaceExistingStudio(server, owner, dimensionId);
handle = ModdedDimensionManager.create(server, dimensionId, pack, pack, seed); handle = ModdedDimensionManager.create(server, dimensionId, pack, pack, seed);
} catch (Throwable e) { STUDIOS.put(owner, dimensionId);
ModdedIrisLog.error("Iris console studio injection failed for {} ({})", dimensionId, pack, e); if (owner.equals(CONSOLE_OWNER)) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_STUDIO_INJECTION_FAILED, MessageArgument.untrusted("value", e.getClass().getSimpleName()), MessageArgument.trusted("errorMessage", IrisLanguage.errorDetail(e)))); completeConsoleOpen(source, dimensionId, pack, seed, handle);
return; transition.complete(null);
} return;
STUDIOS.put(CONSOLE_OWNER, dimensionId); }
ServerLevel studio = handle.level(); ServerPlayer player = server.getPlayerList().getPlayer(owner);
int surface = studio.getMaxY(); if (player == null) {
try { throw new IllegalStateException("Studio owner disconnected before teleport.");
Engine engine = IrisModdedCommands.engineFor(studio); }
if (engine != null) { ServerLevel studio = handle.level();
surface = engine.getMinHeight() + engine.getHeight(8, 8, false) + 2; Engine engine = IrisModdedCommands.engineFor(studio);
if (engine == null) {
throw new IllegalStateException("Studio engine is unavailable for " + dimensionId + ".");
}
int surfaceY = engine.getMinHeight() + engine.getHeight(8, 8, false) + 2;
CompletableFuture<Boolean> teleport = ModdedDimensionManager.teleportAsync(
player,
server,
studio,
8.5D,
surfaceY,
8.5D);
teleport.whenComplete((success, failure) -> server.execute(() -> {
if (transition.isDone()) {
return;
}
if (failure != null) {
transition.completeExceptionally(failure);
return;
}
if (!Boolean.TRUE.equals(success)) {
transition.completeExceptionally(new IllegalStateException(
"Studio native teleport did not complete successfully."));
return;
}
IrisModdedCommands.ok(source, IrisLanguage.plain(
ModdedCommandMessages.MODDED_STUDIO_COMMANDS_STUDIO_OPEN_NOW_RUNS_SEED_USE_IRIS_STUDIO_CLOSE_WHEN,
MessageArgument.untrusted("dimensionId", dimensionId),
MessageArgument.untrusted("pack", pack),
MessageArgument.untrusted("seed", seed)));
transition.complete(null);
}));
} catch (Throwable failure) {
transition.completeExceptionally(failure);
if (handle != null && transition.isCompletedExceptionally()) {
cleanupFailedOpen(server, owner, dimensionId, failure);
} }
} catch (Throwable e) {
ModdedIrisLog.error("Iris console studio surface probe failed for {}", dimensionId, e);
} }
}
private static void replaceExistingStudio(MinecraftServer server, UUID owner, String dimensionId) {
if (ModdedDimensionManager.level(server, dimensionId) != null
|| ModdedDimensionManager.handle(dimensionId) != null) {
ModdedDimensionManager.remove(server, dimensionId, true);
}
STUDIOS.remove(owner, dimensionId);
}
private static void cleanupFailedOpen(
MinecraftServer server,
UUID owner,
String dimensionId,
Throwable failure
) {
try {
ModdedDimensionManager.remove(server, dimensionId, true);
STUDIOS.remove(owner, dimensionId);
} catch (Throwable cleanupFailure) {
failure.addSuppressed(cleanupFailure);
ModdedIrisLog.error("Iris failed to clean up Studio '{}' after open failure",
dimensionId, cleanupFailure);
}
}
private static void completeConsoleOpen(
CommandSourceStack source,
String dimensionId,
String pack,
long seed,
ModdedDimensionManager.Handle handle
) {
ServerLevel studio = handle.level();
Engine engine = IrisModdedCommands.engineFor(studio);
int surface = engine == null
? studio.getMinY() + 1
: engine.getMinHeight() + engine.getHeight(8, 8, false) + 2;
surface = Math.max(studio.getMinY() + 1, Math.min(studio.getMaxY() - 2, surface));
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_CONSOLE_STUDIO_OPEN_NOW_RUNS_SEED_TRANSIENT_NOT_WRITTEN_IRIS, MessageArgument.untrusted("dimensionId", dimensionId), MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("seed", seed))); IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_CONSOLE_STUDIO_OPEN_NOW_RUNS_SEED_TRANSIENT_NOT_WRITTEN_IRIS, MessageArgument.untrusted("dimensionId", dimensionId), MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("seed", seed)));
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_ENTER_IT_WITH_EXECUTE_RUN_TP_S_8_5_8, MessageArgument.untrusted("dimensionId", dimensionId), MessageArgument.untrusted("surface", surface))); IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_ENTER_IT_WITH_EXECUTE_RUN_TP_S_8_5_8, MessageArgument.untrusted("dimensionId", dimensionId), MessageArgument.untrusted("surface", surface)));
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_PREGEN_IT_WITH_IRIS_PREGEN_START_RADIUS, MessageArgument.untrusted("dimensionId", dimensionId))); IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_PREGEN_IT_WITH_IRIS_PREGEN_START_RADIUS, MessageArgument.untrusted("dimensionId", dimensionId)));
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_REMOVE_IT_WITH_IRIS_STUDIO_CLOSE)); IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_REMOVE_IT_WITH_IRIS_STUDIO_CLOSE));
} }
private static void injectAndTeleport(CommandSourceStack source, MinecraftServer server, UUID owner, String dimensionId, String pack, long seed) { private static void reportOpenFailure(
ServerPlayer player = server.getPlayerList().getPlayer(owner); CommandSourceStack source,
if (player == null) { MinecraftServer server,
return; String dimensionId,
} String pack,
ModdedDimensionManager.Handle handle; CompletableFuture<Void> transition
try { ) {
handle = ModdedDimensionManager.create(server, dimensionId, pack, pack, seed); transition.whenComplete((ignored, failure) -> {
} catch (Throwable e) { if (failure == null) {
ModdedIrisLog.error("Iris studio injection failed for {} ({})", dimensionId, pack, e); return;
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_STUDIO_INJECTION_FAILED_2, MessageArgument.untrusted("value", e.getClass().getSimpleName()), MessageArgument.trusted("errorMessage", IrisLanguage.errorDetail(e))));
return;
}
STUDIOS.put(owner, dimensionId);
ServerLevel studio = handle.level();
int surface = studio.getMaxY();
try {
Engine engine = IrisModdedCommands.engineFor(studio);
if (engine != null) {
surface = engine.getMinHeight() + engine.getHeight(8, 8, false) + 2;
} }
} catch (Throwable e) { Throwable cause = unwrapFailure(failure);
ModdedIrisLog.error("Iris studio surface probe failed for {}", dimensionId, e); ModdedIrisLog.error("Iris Studio open failed for '{}' ({})", dimensionId, pack, cause);
} server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(
player.teleportTo(studio, 8.5D, surface, 8.5D, java.util.Set.<Relative>of(), player.getYRot(), player.getXRot(), false); ModdedCommandMessages.MODDED_STUDIO_COMMANDS_STUDIO_OPEN_FAILED,
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_STUDIO_OPEN_NOW_RUNS_SEED_USE_IRIS_STUDIO_CLOSE_WHEN, MessageArgument.untrusted("dimensionId", dimensionId), MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("seed", seed))); MessageArgument.untrusted("value", cause.getClass().getSimpleName()),
MessageArgument.trusted("errorMessage", IrisLanguage.errorDetail(cause)))));
});
} }
private static int close(CommandSourceStack source) { private static int close(CommandSourceStack source) {
ServerPlayer player = source.getPlayer(); ServerPlayer player = source.getPlayer();
MinecraftServer server = source.getServer(); MinecraftServer server = source.getServer();
UUID owner = player == null ? CONSOLE_OWNER : player.getUUID(); UUID owner = player == null ? CONSOLE_OWNER : player.getUUID();
// Commit the ownership drop only after removal succeeds: dropping it first orphaned a TRANSITIONS.submit(owner, () -> closeTransition(source, server, owner));
// still-registered studio that no command could ever remove again.
String dimensionId = STUDIOS.get(owner);
if (dimensionId == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_YOU_DO_NOT_HAVE_OPEN_STUDIO_USE_IRIS_STUDIO_OPEN));
return 0;
}
try {
ModdedDimensionManager.remove(server, dimensionId, true);
} catch (Throwable e) {
ModdedIrisLog.error("Iris studio close failed for {}", dimensionId, e);
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_STUDIO_CLOSE_FAILED, MessageArgument.untrusted("value", e.getClass().getSimpleName()), MessageArgument.trusted("errorMessage", IrisLanguage.errorDetail(e))));
return 0;
}
STUDIOS.remove(owner, dimensionId);
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_STUDIO_CLOSED_WAS_EVACUATED_UNLOADED_ITS_REGION_DATA_DELETED, MessageArgument.untrusted("dimensionId", dimensionId)));
return 1; return 1;
} }
private static CompletableFuture<Void> closeTransition(
CommandSourceStack source,
MinecraftServer server,
UUID owner
) {
CompletableFuture<Void> transition = new CompletableFuture<>();
server.execute(() -> {
String dimensionId = STUDIOS.get(owner);
if (dimensionId == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(
ModdedCommandMessages.MODDED_STUDIO_COMMANDS_YOU_DO_NOT_HAVE_OPEN_STUDIO_USE_IRIS_STUDIO_OPEN));
transition.complete(null);
return;
}
try {
ModdedDimensionManager.remove(server, dimensionId, true);
STUDIOS.remove(owner, dimensionId);
IrisModdedCommands.ok(source, IrisLanguage.plain(
ModdedCommandMessages.MODDED_STUDIO_COMMANDS_STUDIO_CLOSED_WAS_EVACUATED_UNLOADED_ITS_REGION_DATA_DELETED,
MessageArgument.untrusted("dimensionId", dimensionId)));
transition.complete(null);
} catch (Throwable failure) {
ModdedIrisLog.error("Iris Studio close failed for {}", dimensionId, failure);
IrisModdedCommands.fail(source, IrisLanguage.plain(
ModdedCommandMessages.MODDED_STUDIO_COMMANDS_STUDIO_CLOSE_FAILED,
MessageArgument.untrusted("value", failure.getClass().getSimpleName()),
MessageArgument.trusted("errorMessage", IrisLanguage.errorDetail(failure))));
transition.completeExceptionally(failure);
}
});
return transition;
}
private static int status(CommandSourceStack source) { private static int status(CommandSourceStack source) {
MinecraftServer server = source.getServer(); MinecraftServer server = source.getServer();
List<ModdedDimensionManager.Handle> handles = ModdedDimensionManager.handles(); List<ModdedDimensionManager.Handle> handles = ModdedDimensionManager.handles();
@@ -568,31 +690,102 @@ public final class ModdedStudioCommands {
return 0; return 0;
} }
MinecraftServer server = source.getServer(); MinecraftServer server = source.getServer();
String dimensionId = STUDIOS.get(player.getUUID()); UUID owner = player.getUUID();
if (dimensionId == null) { CompletableFuture<Void> transition = TRANSITIONS.submit(
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_YOU_DO_NOT_HAVE_OPEN_STUDIO_USE_IRIS_STUDIO_OPEN_2)); owner,
return 0; () -> teleportToStudio(source, server, owner));
} reportTeleportFailure(source, server, transition);
ServerLevel studio = ModdedDimensionManager.level(server, dimensionId);
if (studio == null) {
STUDIOS.remove(player.getUUID());
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_YOUR_STUDIO_DIMENSION_IS_NO_LONGER_LOADED_USE_IRIS_STUDIO));
return 0;
}
int surface = studio.getMaxY();
try {
Engine engine = IrisModdedCommands.engineFor(studio);
if (engine != null) {
surface = engine.getMinHeight() + engine.getHeight(8, 8, false) + 2;
}
} catch (Throwable e) {
ModdedIrisLog.error("Iris tpstudio surface probe failed", e);
}
player.teleportTo(studio, 8.5D, surface, 8.5D, java.util.Set.<Relative>of(), player.getYRot(), player.getXRot(), false);
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_TELEPORTED_YOUR_STUDIO, MessageArgument.untrusted("dimensionId", dimensionId)));
return 1; return 1;
} }
private static CompletableFuture<Void> teleportToStudio(
CommandSourceStack source,
MinecraftServer server,
UUID owner
) {
CompletableFuture<Void> transition = new CompletableFuture<>();
server.execute(() -> {
if (transition.isDone()) {
return;
}
String dimensionId = STUDIOS.get(owner);
if (dimensionId == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(
ModdedCommandMessages.MODDED_STUDIO_COMMANDS_YOU_DO_NOT_HAVE_OPEN_STUDIO_USE_IRIS_STUDIO_OPEN_2));
transition.complete(null);
return;
}
ServerLevel studio = ModdedDimensionManager.level(server, dimensionId);
if (studio == null) {
STUDIOS.remove(owner, dimensionId);
IrisModdedCommands.fail(source, IrisLanguage.plain(
ModdedCommandMessages.MODDED_STUDIO_COMMANDS_YOUR_STUDIO_DIMENSION_IS_NO_LONGER_LOADED_USE_IRIS_STUDIO));
transition.complete(null);
return;
}
ServerPlayer activePlayer = server.getPlayerList().getPlayer(owner);
Engine engine = IrisModdedCommands.engineFor(studio);
if (activePlayer == null || engine == null) {
transition.completeExceptionally(new IllegalStateException(
"Studio player or engine is unavailable for teleport."));
return;
}
int surfaceY = engine.getMinHeight() + engine.getHeight(8, 8, false) + 2;
CompletableFuture<Boolean> teleport = ModdedDimensionManager.teleportAsync(
activePlayer,
server,
studio,
8.5D,
surfaceY,
8.5D);
teleport.whenComplete((success, failure) -> server.execute(() -> {
if (transition.isDone()) {
return;
}
if (failure != null) {
transition.completeExceptionally(failure);
return;
}
if (!Boolean.TRUE.equals(success)) {
transition.completeExceptionally(new IllegalStateException(
"Studio native teleport did not complete successfully."));
return;
}
IrisModdedCommands.ok(source, IrisLanguage.plain(
ModdedCommandMessages.MODDED_STUDIO_COMMANDS_TELEPORTED_YOUR_STUDIO,
MessageArgument.untrusted("dimensionId", dimensionId)));
transition.complete(null);
}));
});
return transition;
}
private static void reportTeleportFailure(
CommandSourceStack source,
MinecraftServer server,
CompletableFuture<Void> transition
) {
transition.whenComplete((ignored, failure) -> {
if (failure == null) {
return;
}
Throwable cause = unwrapFailure(failure);
ModdedIrisLog.error("Iris Studio teleport failed", cause);
server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(
ModdedCommandMessages.MODDED_STUDIO_COMMANDS_STUDIO_OPEN_FAILED,
MessageArgument.untrusted("value", cause.getClass().getSimpleName()),
MessageArgument.trusted("errorMessage", IrisLanguage.errorDetail(cause)))));
});
}
private static Throwable unwrapFailure(Throwable failure) {
Throwable cause = failure;
while (cause instanceof CompletionException && cause.getCause() != null) {
cause = cause.getCause();
}
return cause;
}
private static int version(CommandSourceStack source, String pack) { private static int version(CommandSourceStack source, String pack) {
File folder = resolvePack(source, pack); File folder = resolvePack(source, pack);
if (folder == null) { if (folder == null) {
@@ -0,0 +1,58 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded.command;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier;
final class ModdedStudioTransitionQueue {
private final Object lock = new Object();
private final Map<UUID, CompletableFuture<Void>> tails = new HashMap<>();
CompletableFuture<Void> submit(UUID owner, Supplier<CompletableFuture<Void>> transition) {
Objects.requireNonNull(owner, "Studio transition owner");
Objects.requireNonNull(transition, "Studio transition");
synchronized (lock) {
CompletableFuture<Void> previous = tails.get(owner);
CompletableFuture<Void> admission = previous == null
? CompletableFuture.completedFuture(null)
: previous.handle((ignored, failure) -> null);
CompletableFuture<Void> current = admission.thenCompose((ignored) -> transition.get());
tails.put(owner, current);
current.whenComplete((ignored, failure) -> remove(owner, current));
return current;
}
}
void clear() {
synchronized (lock) {
tails.clear();
}
}
private void remove(UUID owner, CompletableFuture<Void> transition) {
synchronized (lock) {
tails.remove(owner, transition);
}
}
}
@@ -24,12 +24,13 @@ import art.arcane.iris.core.gui.GuiOverlay;
import art.arcane.iris.engine.IrisComplex; import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.render.RenderType; import art.arcane.iris.engine.framework.render.RenderType;
import art.arcane.iris.modded.ModdedDimensionManager;
import art.arcane.iris.modded.ModdedIrisLog;
import net.minecraft.server.MinecraftServer; import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.phys.Vec3; import net.minecraft.world.phys.Vec3;
import java.awt.Desktop; import java.awt.Desktop;
@@ -37,6 +38,8 @@ import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer; import java.util.function.Consumer;
public final class ModdedVisionOverlay implements GuiOverlay { public final class ModdedVisionOverlay implements GuiOverlay {
@@ -80,11 +83,14 @@ public final class ModdedVisionOverlay implements GuiOverlay {
@Override @Override
public void teleport(double worldX, double worldZ) { public void teleport(double worldX, double worldZ) {
int blockX = (int) worldX; int blockX = (int) Math.floor(worldX);
int blockZ = (int) worldZ; int blockZ = (int) Math.floor(worldZ);
server.execute(() -> { server.execute(() -> {
ServerPlayer player = opener == null ? null : server.getPlayerList().getPlayer(opener); ServerPlayer player = opener == null ? null : server.getPlayerList().getPlayer(opener);
if (player == null) { if (player == null) {
if (opener != null) {
return;
}
List<ServerPlayer> players = level.players(); List<ServerPlayer> players = level.players();
if (players.isEmpty()) { if (players.isEmpty()) {
return; return;
@@ -92,8 +98,24 @@ public final class ModdedVisionOverlay implements GuiOverlay {
player = players.get(0); player = players.get(0);
} }
int surfaceY = engine.getMinHeight() + engine.getHeight(blockX, blockZ, false) + 2; int surfaceY = engine.getMinHeight() + engine.getHeight(blockX, blockZ, false) + 2;
int safeY = Math.max(surfaceY, level.getHeight(Heightmap.Types.MOTION_BLOCKING, blockX, blockZ) + 1); CompletableFuture<Boolean> teleport = ModdedDimensionManager.teleportAsync(
player.teleportTo(level, blockX + 0.5D, safeY, blockZ + 0.5D, java.util.Set.of(), player.getYRot(), player.getXRot(), false); player,
server,
level,
blockX + 0.5D,
surfaceY,
blockZ + 0.5D,
System.nanoTime() + TimeUnit.SECONDS.toNanos(10L));
UUID playerId = player.getUUID();
teleport.whenComplete((success, failure) -> {
if (failure != null) {
ModdedIrisLog.error("Iris Vision teleport failed for {} at {},{}",
playerId, blockX, blockZ, failure);
} else if (!Boolean.TRUE.equals(success)) {
ModdedIrisLog.warn("Iris Vision teleport did not complete for {} at {},{}",
playerId, blockX, blockZ);
}
});
}); });
} }
@@ -0,0 +1,27 @@
package art.arcane.iris.modded;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class ModdedDimensionManagerTeleportTest {
@Test
public void blockCoordinatesUseFloorAcrossZeroAndChunkBoundaries() {
assertEquals(0, ModdedTeleportBounds.blockCoordinate(0.99D));
assertEquals(-1, ModdedTeleportBounds.blockCoordinate(-0.01D));
assertEquals(-17, ModdedTeleportBounds.blockCoordinate(-16.01D));
assertEquals(16, ModdedTeleportBounds.blockCoordinate(16D));
}
@Test
public void teleportYReservesSupportFeetAndHeadSpace() {
assertEquals(-63, ModdedTeleportBounds.clampY(-64, 320, -100));
assertEquals(318, ModdedTeleportBounds.clampY(-64, 320, 400));
assertEquals(72, ModdedTeleportBounds.clampY(-64, 320, 72));
}
@Test(expected = IllegalArgumentException.class)
public void teleportYRejectsWorldsWithoutThreeVerticalBlocks() {
ModdedTeleportBounds.clampY(0, 2, 1);
}
}
@@ -0,0 +1,91 @@
package art.arcane.iris.modded;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertTrue;
public class ModdedDynamicLevelLifecycleContractTest {
private static final String SOURCE_ROOT_PROPERTY = "iris.moddedCommonSources";
@Test
public void sharedManagerPublishesDynamicLoadAndUnloadAroundRegistration() throws IOException {
String manager = commonSource("ModdedDimensionManager.java");
String loader = commonSource("ModdedLoader.java");
String injection = method(manager, "private static Handle inject(");
String removal = method(manager, "public static boolean remove(");
String injectionRollback = method(manager, "private static void rollbackInjection(");
String removalRollback = method(manager, "private static void rollbackRemoval(");
assertTrue(loader.contains("void fireDynamicLevelLoad(MinecraftServer server, ServerLevel level);"));
assertTrue(loader.contains("void fireDynamicLevelUnload(MinecraftServer server, ServerLevel level);"));
assertBefore(injection, "serverAccess.putLevelIfAbsent(server, key, level);",
"fireDynamicLevelLoad(server, level);");
assertBefore(removal, "fireDynamicLevelUnload(server, level);",
"serverAccess.removeLevel(server, key);");
assertTrue(injectionRollback.contains("fireDynamicLevelUnload(server, level);"));
assertTrue(removalRollback.contains("fireDynamicLevelLoad(server, level);"));
}
@Test
public void everyLoaderUsesItsNativeLifecycleBus() throws IOException {
String fabric = loaderSource("fabric", "art/arcane/iris/fabric/FabricModdedLoader.java");
String forge = loaderSource("forge", "art/arcane/iris/forge/ForgeModdedLoader.java");
String neoForge = loaderSource("neoforge", "art/arcane/iris/neoforge/NeoForgeModdedLoader.java");
assertTrue(fabric.contains("ServerLevelEvents.LOAD.invoker().onLevelLoad(server, level);"));
assertTrue(fabric.contains("ServerLevelEvents.UNLOAD.invoker().onLevelUnload(server, level);"));
assertTrue(forge.contains("LevelEvent.Load.BUS.post(new LevelEvent.Load(level));"));
assertTrue(forge.contains("LevelEvent.Unload.BUS.post(new LevelEvent.Unload(level));"));
assertTrue(neoForge.contains("NeoForge.EVENT_BUS.post(new LevelEvent.Load(level));"));
assertTrue(neoForge.contains("NeoForge.EVENT_BUS.post(new LevelEvent.Unload(level));"));
}
private static String commonSource(String file) throws IOException {
return Files.readString(commonRoot().resolve("art/arcane/iris/modded").resolve(file))
.replace("\r\n", "\n");
}
private static String loaderSource(String loader, String relative) throws IOException {
Path adaptersRoot = commonRoot().getParent().getParent().getParent().getParent();
return Files.readString(adaptersRoot.resolve(loader).resolve("src/main/java").resolve(relative))
.replace("\r\n", "\n");
}
private static Path commonRoot() {
String root = System.getProperty(SOURCE_ROOT_PROPERTY);
if (root == null || root.isBlank()) {
throw new IllegalStateException("Missing test property " + SOURCE_ROOT_PROPERTY);
}
return Path.of(root);
}
private static String method(String source, String signature) {
int start = source.indexOf(signature);
if (start < 0) {
throw new IllegalArgumentException("Missing source method " + signature);
}
int open = source.indexOf('{', start);
int depth = 0;
for (int index = open; index < source.length(); index++) {
char current = source.charAt(index);
if (current == '{') {
depth++;
} else if (current == '}' && --depth == 0) {
return source.substring(start, index + 1);
}
}
throw new IllegalArgumentException("Unclosed source method " + signature);
}
private static void assertBefore(String source, String first, String second) {
int firstIndex = source.indexOf(first);
int secondIndex = source.indexOf(second);
assertTrue("Missing source token " + first, firstIndex >= 0);
assertTrue("Missing source token " + second, secondIndex >= 0);
assertTrue(first + " must precede " + second, firstIndex < secondIndex);
}
}
@@ -65,7 +65,7 @@ public class ModdedLifecycleFailureContractTest {
String reinjection = method(source, "private static void reinjectPersistentDimensions("); String reinjection = method(source, "private static void reinjectPersistentDimensions(");
String failure = catchBlock(reinjection); String failure = catchBlock(reinjection);
assertTrue(failure.contains("LOGGER.error(")); assertTrue(failure.contains("ModdedIrisLog.error("));
assertFalse(failure.contains("e.toString()")); assertFalse(failure.contains("e.toString()"));
assertFalse(failure.contains("throw new IllegalStateException(")); assertFalse(failure.contains("throw new IllegalStateException("));
assertTrue(reinjection.contains("injected++;")); assertTrue(reinjection.contains("injected++;"));
@@ -152,7 +152,7 @@ public class ModdedLifecycleFailureContractTest {
assertBefore(stop, "\"world engines\"", "\"dimension manager\""); assertBefore(stop, "\"world engines\"", "\"dimension manager\"");
assertBefore(stop, "\"server state\"", "if (failure != null)"); assertBefore(stop, "\"server state\"", "if (failure != null)");
assertFalse(stop.contains("throw")); assertFalse(stop.contains("throw"));
assertTrue(stop.contains("LOGGER.error(\"Iris modded shutdown completed with failures\", failure);")); assertTrue(stop.contains("ModdedIrisLog.error(\"Iris modded shutdown completed with failures\", failure);"));
String runStage = method(source, "private static Throwable runStopStage("); String runStage = method(source, "private static Throwable runStopStage(");
assertTrue(runStage.contains("catch (Throwable stageFailure)")); assertTrue(runStage.contains("catch (Throwable stageFailure)"));
@@ -207,21 +207,21 @@ public class ModdedLifecycleFailureContractTest {
String bootstrapSource = source("ModdedEngineBootstrap.java"); String bootstrapSource = source("ModdedEngineBootstrap.java");
String unload = method(bootstrapSource, "public static void levelUnloaded(ServerLevel level)"); String unload = method(bootstrapSource, "public static void levelUnloaded(ServerLevel level)");
String failure = catchBlock(unload); String failure = catchBlock(unload);
assertTrue(failure.contains("LOGGER.error(")); assertTrue(failure.contains("ModdedIrisLog.error("));
assertTrue(failure.contains("throw ")); assertTrue(failure.contains("throw "));
String managerSource = source("ModdedDimensionManager.java"); String managerSource = source("ModdedDimensionManager.java");
String remove = method(managerSource, "public static boolean remove(MinecraftServer server, String dimensionId, boolean wipeStorage)"); String remove = method(managerSource, "public static boolean remove(MinecraftServer server, String dimensionId, boolean wipeStorage)");
assertTrue(remove.contains("ModdedWorldEngines.evictOrThrow(level);")); assertTrue(remove.contains("ModdedWorldEngines.evictOrThrow(level);"));
assertFalse(remove.contains("ModdedWorldEngines.evict(level);")); assertFalse(remove.contains("ModdedWorldEngines.evict(level);"));
assertTrue(remove.contains("generatorUnbound = true;")); assertTrue(remove.contains("unloadEventStarted = true;"));
assertTrue(remove.contains("rollbackRemoval(server, serverAccess, key, level, generator, generatorUnbound, e);")); assertTrue(remove.contains("rollbackRemoval(server, serverAccess, key, level, generator, unloadEventStarted, e);"));
String rollback = method(managerSource, "private static void rollbackRemoval("); String rollback = method(managerSource, "private static void rollbackRemoval(");
assertTrue(rollback.contains("serverAccess.hasLevel(server, key)")); assertTrue(rollback.contains("serverAccess.hasLevel(server, key)"));
assertTrue(rollback.contains("generator.bindLevel(level);")); assertTrue(rollback.contains("generator.bindLevel(level);"));
assertTrue(rollback.contains("failure.addSuppressed(rollbackFailure);")); assertTrue(rollback.contains("failure.addSuppressed(rollbackFailure);"));
assertTrue(rollback.contains("LOGGER.error(")); assertTrue(rollback.contains("ModdedIrisLog.error("));
} }
@Test @Test
@@ -45,6 +45,14 @@ public class ModdedPlatformPathsTest {
public void invalidateLevelCache(MinecraftServer server) { public void invalidateLevelCache(MinecraftServer server) {
} }
@Override
public void fireDynamicLevelLoad(MinecraftServer server, ServerLevel level) {
}
@Override
public void fireDynamicLevelUnload(MinecraftServer server, ServerLevel level) {
}
@Override @Override
public boolean clientEnvironment() { public boolean clientEnvironment() {
return false; return false;
@@ -186,7 +186,7 @@ public class IrisModdedStructureCommandTest {
assertTrue(command.contains("ModdedLocateCommands.registeredStructureUnavailableMessage(")); assertTrue(command.contains("ModdedLocateCommands.registeredStructureUnavailableMessage("));
assertTrue(command.contains("engine.getData().getStructureLoader().getPossibleKeys()")); assertTrue(command.contains("engine.getData().getStructureLoader().getPossibleKeys()"));
assertTrue(command.contains("IrisStructureLocator.hasLocatableEditablePlacement(engine, key)")); assertTrue(command.contains("IrisStructureLocator.hasLocatableEditablePlacement(engine, key)"));
assertTrue(command.contains("LOGGER.info(\"[Iris goto unregistered] [{}] {} - {}\"")); assertTrue(command.contains("ModdedIrisLog.info(\"[Iris goto unregistered] [{}] {} - {}\""));
assertTrue(command.contains("UNREGISTERED(\"unregistered\")")); assertTrue(command.contains("UNREGISTERED(\"unregistered\")"));
assertFalse(command.contains("DatapackIngestService")); assertFalse(command.contains("DatapackIngestService"));
} }
@@ -0,0 +1,89 @@
package art.arcane.iris.modded.command;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class ModdedStudioTeleportContractTest {
private static final String SOURCE_ROOT_PROPERTY = "iris.moddedCommonSources";
@Test
public void studioOpenSerializesReplacementAndWaitsForTheNativePath() throws IOException {
String source = source("command/ModdedStudioCommands.java");
String open = method(source, "private static int open(");
String execute = method(source, "private static void executeStudioOpen(");
assertTrue(open.contains("TRANSITIONS.submit("));
assertFalse(open.contains("orTimeout("));
assertFalse(open.contains("deadlineNanos"));
assertBefore(execute, "replaceExistingStudio(", "ModdedDimensionManager.create(");
assertBefore(execute, "ModdedDimensionManager.create(", "ModdedDimensionManager.teleportAsync(");
assertFalse(execute.contains("deadlineNanos"));
assertFalse(source.contains("player.teleportTo(studio"));
}
@Test
public void studioTeleportAndVisionShareWarmFutureSemantics() throws IOException {
String studio = source("command/ModdedStudioCommands.java");
String vision = source("command/ModdedVisionOverlay.java");
String dimensions = source("ModdedDimensionManager.java");
assertTrue(studio.contains("private static CompletableFuture<Void> teleportToStudio("));
assertTrue(studio.contains("ModdedDimensionManager.teleportAsync("));
assertTrue(dimensions.contains("TELEPORT_WARM_RADIUS = 0"));
assertTrue(dimensions.contains("addTicketAndLoadWithRadius(\n"
+ " TELEPORT_WARM_TICKET,\n"
+ " chunkPos,\n"
+ " TELEPORT_WARM_RADIUS)"));
assertTrue(dimensions.contains("removeTicketWithRadius(\n"
+ " TELEPORT_WARM_TICKET,\n"
+ " chunkPos,\n"
+ " TELEPORT_WARM_RADIUS)"));
assertTrue(vision.contains("Math.floor(worldX)"));
assertTrue(vision.contains("Math.floor(worldZ)"));
assertTrue(vision.contains("if (opener != null)"));
assertTrue(vision.contains("ModdedDimensionManager.teleportAsync("));
assertFalse(vision.contains("level.getHeight("));
assertFalse(vision.contains("player.teleportTo("));
}
private static String source(String relative) throws IOException {
String root = System.getProperty(SOURCE_ROOT_PROPERTY);
if (root == null || root.isBlank()) {
throw new IllegalStateException("Missing test property " + SOURCE_ROOT_PROPERTY);
}
return Files.readString(Path.of(root).resolve("art/arcane/iris/modded").resolve(relative))
.replace("\r\n", "\n");
}
private static String method(String source, String signature) {
int start = source.indexOf(signature);
if (start < 0) {
throw new IllegalArgumentException("Missing source method " + signature);
}
int open = source.indexOf('{', start);
int depth = 0;
for (int index = open; index < source.length(); index++) {
char current = source.charAt(index);
if (current == '{') {
depth++;
} else if (current == '}' && --depth == 0) {
return source.substring(start, index + 1);
}
}
throw new IllegalArgumentException("Unclosed source method " + signature);
}
private static void assertBefore(String source, String first, String second) {
int firstIndex = source.indexOf(first);
int secondIndex = source.indexOf(second);
assertTrue("Missing source token " + first, firstIndex >= 0);
assertTrue("Missing source token " + second, secondIndex >= 0);
assertTrue(first + " must precede " + second, firstIndex < secondIndex);
}
}
@@ -0,0 +1,70 @@
package art.arcane.iris.modded.command;
import org.junit.Test;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class ModdedStudioTransitionQueueTest {
@Test
public void sameOwnerTransitionsRunInSubmissionOrder() {
ModdedStudioTransitionQueue queue = new ModdedStudioTransitionQueue();
UUID owner = UUID.randomUUID();
CompletableFuture<Void> firstGate = new CompletableFuture<>();
AtomicInteger starts = new AtomicInteger();
CompletableFuture<Void> first = queue.submit(owner, () -> {
starts.incrementAndGet();
return firstGate;
});
CompletableFuture<Void> second = queue.submit(owner, () -> {
starts.incrementAndGet();
return CompletableFuture.completedFuture(null);
});
assertEquals(1, starts.get());
assertFalse(second.isDone());
firstGate.complete(null);
assertTrue(first.isDone());
assertTrue(second.isDone());
assertEquals(2, starts.get());
}
@Test
public void differentOwnersDoNotBlockEachOther() {
ModdedStudioTransitionQueue queue = new ModdedStudioTransitionQueue();
CompletableFuture<Void> firstGate = new CompletableFuture<>();
CompletableFuture<Void> first = queue.submit(UUID.randomUUID(), () -> firstGate);
CompletableFuture<Void> second = queue.submit(
UUID.randomUUID(),
() -> CompletableFuture.completedFuture(null));
assertFalse(first.isDone());
assertTrue(second.isDone());
}
@Test
public void failedTransitionDoesNotPoisonTheOwnerQueue() {
ModdedStudioTransitionQueue queue = new ModdedStudioTransitionQueue();
UUID owner = UUID.randomUUID();
CompletableFuture<Void> failure = CompletableFuture.failedFuture(
new IllegalStateException("expected"));
AtomicInteger starts = new AtomicInteger();
CompletableFuture<Void> first = queue.submit(owner, () -> failure);
CompletableFuture<Void> second = queue.submit(owner, () -> {
starts.incrementAndGet();
return CompletableFuture.completedFuture(null);
});
assertTrue(first.isCompletedExceptionally());
assertTrue(second.isDone());
assertEquals(1, starts.get());
}
}
@@ -27,6 +27,7 @@ import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.BlockState;
import net.neoforged.neoforge.common.NeoForge; import net.neoforged.neoforge.common.NeoForge;
import net.neoforged.neoforge.event.level.LevelEvent;
import net.neoforged.neoforge.event.level.block.BreakBlockEvent; import net.neoforged.neoforge.event.level.block.BreakBlockEvent;
import net.neoforged.fml.ModList; import net.neoforged.fml.ModList;
import net.neoforged.fml.loading.FMLEnvironment; import net.neoforged.fml.loading.FMLEnvironment;
@@ -77,6 +78,16 @@ public final class NeoForgeModdedLoader implements ModdedLoader {
server.markWorldsDirty(); server.markWorldsDirty();
} }
@Override
public void fireDynamicLevelLoad(MinecraftServer server, ServerLevel level) {
NeoForge.EVENT_BUS.post(new LevelEvent.Load(level));
}
@Override
public void fireDynamicLevelUnload(MinecraftServer server, ServerLevel level) {
NeoForge.EVENT_BUS.post(new LevelEvent.Unload(level));
}
@Override @Override
public boolean clientEnvironment() { public boolean clientEnvironment() {
return FMLEnvironment.getDist().isClient(); return FMLEnvironment.getDist().isClient();
+72 -4
View File
@@ -4,6 +4,8 @@ import org.gradle.api.tasks.Copy
import org.gradle.api.tasks.compile.JavaCompile import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.jvm.tasks.Jar import org.gradle.jvm.tasks.Jar
import org.gradle.jvm.toolchain.JavaLanguageVersion import org.gradle.jvm.toolchain.JavaLanguageVersion
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
/* /*
* Iris is a World Generator for Minecraft Bukkit Servers * Iris is a World Generator for Minecraft Bukkit Servers
@@ -73,6 +75,7 @@ String bukkitArtifactName = irisArtifactName('CraftBukkit', bukkitMinecraftRange
String fabricArtifactName = irisArtifactName('Fabric', "${minecraftVersion}+${loaderDisplayVersion(fabricLoaderVersion)}") String fabricArtifactName = irisArtifactName('Fabric', "${minecraftVersion}+${loaderDisplayVersion(fabricLoaderVersion)}")
String forgeArtifactName = irisArtifactName('Forge', "${minecraftVersion}+${loaderDisplayVersion(forgeVersion)}") String forgeArtifactName = irisArtifactName('Forge', "${minecraftVersion}+${loaderDisplayVersion(forgeVersion)}")
String neoForgeArtifactName = irisArtifactName('NeoForge', "${minecraftVersion}+${loaderDisplayVersion(neoForgeVersion)}") String neoForgeArtifactName = irisArtifactName('NeoForge', "${minecraftVersion}+${loaderDisplayVersion(neoForgeVersion)}")
long maximumBukkitArtifactBytes = 7_000_000L
apply plugin: ApiGenerator apply plugin: ApiGenerator
// Where `buildAll` drops the per-platform jars for a local test server. Use the approved sibling // Where `buildAll` drops the per-platform jars for a local test server. Use the approved sibling
@@ -145,6 +148,16 @@ nmsBindings.each { key, value ->
def included = configurations.create('included') def included = configurations.create('included')
def jarJar = configurations.create('jarJar') def jarJar = configurations.create('jarJar')
def bukkitLanguagesDirectory = layout.buildDirectory.dir('generated/bukkit-languages')
Set<String> sharedNonEnglishBukkitMessages = [
'iris.desktop.vision.fps',
'iris.desktop.vision.tiles',
'iris.bukkit.runtime.commandpack.message_2',
'iris.bukkit.commanddatapack.message_2',
'iris.runtime.chunk_job.bossbar.progress',
'iris.runtime.studio.action.progress',
'iris.runtime.chunk_job.action.progress'
] as Set
dependencies { dependencies {
nmsBindings.keySet().each { key -> nmsBindings.keySet().each { key ->
add('included', project(path: ":adapters:bukkit:nms:${key}", configuration: 'runtimeElements')) add('included', project(path: ":adapters:bukkit:nms:${key}", configuration: 'runtimeElements'))
@@ -154,14 +167,66 @@ dependencies {
add('jarJar', project(':core:agent')) add('jarJar', project(':core:agent'))
} }
def prepareBukkitLanguages = tasks.register('prepareBukkitLanguages') {
inputs.files(fileTree(project(':core').file('src/main/resources/languages')) {
include('*.json')
})
outputs.dir(bukkitLanguagesDirectory)
doLast {
File outputDirectory = bukkitLanguagesDirectory.get().asFile
delete(outputDirectory)
outputDirectory.mkdirs()
List<File> sources = fileTree(project(':core').file('src/main/resources/languages')) {
include('*.json')
}.files.sort { File left, File right -> left.name <=> right.name }
List<Map<String, Object>> catalogs = sources.collect { File source ->
(Map<String, Object>) new JsonSlurper().parse(source)
}
Map<String, Object> referenceMessages = (Map<String, Object>) catalogs.first().get('messages')
List<String> messageIds = referenceMessages.keySet()
.findAll { String key -> !key.startsWith('iris.modded.') }
.sort()
Set<String> sharedFallbackIds = new LinkedHashSet<>(messageIds)
catalogs.each { Map<String, Object> catalog ->
Map<String, Object> messages = (Map<String, Object>) catalog.get('messages')
if (!messages.keySet().containsAll(messageIds)) {
throw new GradleException("Bundled locale ${catalog.get('locale')} is missing Bukkit messages")
}
sharedFallbackIds.removeIf { String key -> messages.get(key) != referenceMessages.get(key) }
}
sharedFallbackIds.removeAll(sharedNonEnglishBukkitMessages)
sources.eachWithIndex { File source, int index ->
Map<String, Object> catalog = catalogs.get(index)
Map<String, Object> messages = (Map<String, Object>) catalog.get('messages')
List<Object> compactMessages = messageIds.collect { String key ->
sharedFallbackIds.contains(key) ? null : messages.get(key)
}
new File(outputDirectory, source.name).setText(
JsonOutput.toJson([catalog.get('locale'), compactMessages]),
'UTF-8')
}
}
}
tasks.named('jar', Jar).configure { tasks.named('jar', Jar).configure {
inputs.files(included) inputs.files(included)
duplicatesStrategy = DuplicatesStrategy.EXCLUDE duplicatesStrategy = DuplicatesStrategy.EXCLUDE
from(jarJar, provider { included.resolve().collect { zipTree(it) } }) includeEmptyDirs = false
from(jarJar)
from(provider { included.resolve().collect { zipTree(it) } }) {
exclude('languages/**')
}
from(prepareBukkitLanguages) {
into('languages')
}
doFirst { doFirst {
delete(layout.buildDirectory.file("libs/Iris-${project.version}.jar")) delete(layout.buildDirectory.file("libs/Iris-${project.version}.jar"))
} }
archiveFileName.set(bukkitArtifactName) archiveFileName.set(bukkitArtifactName)
doLast {
JarCompactor.compact(archiveFile.get().asFile)
}
} }
tasks.register('iris', Copy) { tasks.register('iris', Copy) {
@@ -537,10 +602,8 @@ List<String> requiredBukkitArtifactEntries = [
'art/arcane/iris/core/lifecycle/WorldLifecycleStaging.class', 'art/arcane/iris/core/lifecycle/WorldLifecycleStaging.class',
'art/arcane/iris/util/simd/VectorSimdKernels.class', 'art/arcane/iris/util/simd/VectorSimdKernels.class',
'art/arcane/iris/util/simd/VectorNoiseKernels2D.class', 'art/arcane/iris/util/simd/VectorNoiseKernels2D.class',
'art/arcane/iris/util/paralithic/functions/Function.class',
'art/arcane/iris/util/project/agent/Agent.class', 'art/arcane/iris/util/project/agent/Agent.class',
'art/arcane/iris/util/common/misc/getHardware.class', 'art/arcane/iris/util/common/misc/getHardware.class',
'art/arcane/iris/util/caffeine/cache/Caffeine.class',
'art/arcane/volmlib/util/io/JarScanner.class', 'art/arcane/volmlib/util/io/JarScanner.class',
'art/arcane/volmlib/util/director/runtime/DirectorRuntimeEngine.class', 'art/arcane/volmlib/util/director/runtime/DirectorRuntimeEngine.class',
'art/arcane/volmlib/util/mantle/io/Lz4IOWorkerCodecSupport.class', 'art/arcane/volmlib/util/mantle/io/Lz4IOWorkerCodecSupport.class',
@@ -553,7 +616,12 @@ tasks.register('verifyBukkitArtifact') {
File artifact = layout.buildDirectory.file("libs/${bukkitArtifactName}").get().asFile File artifact = layout.buildDirectory.file("libs/${bukkitArtifactName}").get().asFile
inputs.file(artifact) inputs.file(artifact)
doLast { doLast {
BukkitArtifactVerifier.verify(artifact, requiredBukkitArtifactEntries, 17, 400, 16) BukkitArtifactVerifier.verify(
artifact,
requiredBukkitArtifactEntries,
17,
16,
maximumBukkitArtifactBytes)
logger.lifecycle("Verified ${artifact.name} packaging and class reference graph") logger.lifecycle("Verified ${artifact.name} packaging and class reference graph")
} }
} }
@@ -21,9 +21,7 @@ public final class BukkitArtifactVerifier {
// class under one of these is a NoClassDefFoundError waiting for the right code path. // class under one of these is a NoClassDefFoundError waiting for the right code path.
private static final List<String> SHIPPED_PREFIXES = List.of( private static final List<String> SHIPPED_PREFIXES = List.of(
"art/arcane/iris/", "art/arcane/iris/",
"art/arcane/volmlib/", "art/arcane/volmlib/"
"com/google/gson/",
"com/googlecode/concurrentlinkedhashmap/"
); );
// Relocation targets for the libraries slimjar downloads and relocates at runtime. Compiled // Relocation targets for the libraries slimjar downloads and relocates at runtime. Compiled
// references to them are correct and the classes are correctly absent from the jar. // references to them are correct and the classes are correctly absent from the jar.
@@ -38,9 +36,12 @@ public final class BukkitArtifactVerifier {
"art/arcane/iris/util/aether/", "art/arcane/iris/util/aether/",
"art/arcane/iris/util/guice/", "art/arcane/iris/util/guice/",
"art/arcane/iris/util/dom4j/", "art/arcane/iris/util/dom4j/",
"art/arcane/iris/util/jaxen/" "art/arcane/iris/util/jaxen/",
"art/arcane/iris/util/gson/",
"art/arcane/iris/util/lru/",
"art/arcane/iris/util/caffeine/",
"art/arcane/iris/util/paralithic/"
); );
private static final String CAFFEINE_CACHE_PACKAGE = "art/arcane/iris/util/caffeine/cache/";
private static final String MATTER_SLICE_PACKAGE = "art/arcane/volmlib/util/matter/slices/"; private static final String MATTER_SLICE_PACKAGE = "art/arcane/volmlib/util/matter/slices/";
private static final String LANGUAGE_DIRECTORY = "languages/"; private static final String LANGUAGE_DIRECTORY = "languages/";
@@ -48,10 +49,14 @@ public final class BukkitArtifactVerifier {
} }
public static void verify(File artifact, List<String> requiredEntries, int minimumLocales, public static void verify(File artifact, List<String> requiredEntries, int minimumLocales,
int minimumCaffeineFactories, int minimumMatterSlices) { int minimumMatterSlices, long maximumArtifactBytes) {
if (!artifact.isFile()) { if (!artifact.isFile()) {
throw new GradleException("Missing Bukkit Iris artifact: " + artifact.getAbsolutePath()); throw new GradleException("Missing Bukkit Iris artifact: " + artifact.getAbsolutePath());
} }
if (artifact.length() > maximumArtifactBytes) {
throw new GradleException(artifact.getName() + " is " + artifact.length()
+ " bytes; Bukkit artifacts must not exceed " + maximumArtifactBytes + " bytes");
}
try (JarFile jar = new JarFile(artifact)) { try (JarFile jar = new JarFile(artifact)) {
for (String requiredEntry : requiredEntries) { for (String requiredEntry : requiredEntries) {
@@ -65,7 +70,6 @@ public final class BukkitArtifactVerifier {
Set<String> shippedClasses = new LinkedHashSet<>(); Set<String> shippedClasses = new LinkedHashSet<>();
int locales = 0; int locales = 0;
int caffeineFactories = 0;
int matterSlices = 0; int matterSlices = 0;
Enumeration<JarEntry> entries = jar.entries(); Enumeration<JarEntry> entries = jar.entries();
while (entries.hasMoreElements()) { while (entries.hasMoreElements()) {
@@ -84,9 +88,6 @@ public final class BukkitArtifactVerifier {
String internalName = name.substring(0, name.length() - ".class".length()); String internalName = name.substring(0, name.length() - ".class".length());
shippedClasses.add(internalName); shippedClasses.add(internalName);
if (isGeneratedCaffeineFactory(internalName)) {
caffeineFactories++;
}
if (internalName.startsWith(MATTER_SLICE_PACKAGE)) { if (internalName.startsWith(MATTER_SLICE_PACKAGE)) {
matterSlices++; matterSlices++;
} }
@@ -96,14 +97,6 @@ public final class BukkitArtifactVerifier {
throw new GradleException(artifact.getName() + " ships " + locales + " locale files; expected at least " throw new GradleException(artifact.getName() + " ships " + locales + " locale files; expected at least "
+ minimumLocales); + minimumLocales);
} }
// Caffeine picks its cache and node implementation with MethodHandles.Lookup.findClass on a
// name built from the builder's feature flags. Nothing references these statically, so only a
// population check can tell that an exclude or minimize() ate them.
if (caffeineFactories < minimumCaffeineFactories) {
throw new GradleException(artifact.getName() + " ships " + caffeineFactories
+ " generated Caffeine cache classes; expected at least " + minimumCaffeineFactories
+ ". Caffeine resolves these by name and cannot survive static pruning");
}
// Matter.read() resolves slice types from the canonical name stored in the payload. // Matter.read() resolves slice types from the canonical name stored in the payload.
if (matterSlices < minimumMatterSlices) { if (matterSlices < minimumMatterSlices) {
throw new GradleException(artifact.getName() + " ships " + matterSlices throw new GradleException(artifact.getName() + " ships " + matterSlices
@@ -148,23 +141,6 @@ public final class BukkitArtifactVerifier {
} }
} }
private static boolean isGeneratedCaffeineFactory(String internalName) {
if (!internalName.startsWith(CAFFEINE_CACHE_PACKAGE)) {
return false;
}
String simpleName = internalName.substring(CAFFEINE_CACHE_PACKAGE.length());
if (simpleName.isEmpty() || simpleName.indexOf('/') >= 0) {
return false;
}
for (int i = 0; i < simpleName.length(); i++) {
if (simpleName.charAt(i) < 'A' || simpleName.charAt(i) > 'Z') {
return false;
}
}
return true;
}
private static byte[] readEntryBytes(JarFile jar, JarEntry entry) throws IOException { private static byte[] readEntryBytes(JarFile jar, JarEntry entry) throws IOException {
try (InputStream input = jar.getInputStream(entry)) { try (InputStream input = jar.getInputStream(entry)) {
return input.readAllBytes(); return input.readAllBytes();
+97
View File
@@ -0,0 +1,97 @@
import org.gradle.api.GradleException;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
public final class JarCompactor {
private static final int BUFFER_BYTES = 64 * 1024;
private JarCompactor() {
}
public static void compact(File artifact) {
if (artifact == null || !artifact.isFile()) {
throw new GradleException("Cannot compact missing jar artifact: " + artifact);
}
Path source = artifact.toPath();
Path temporary = null;
try {
temporary = Files.createTempFile(source.getParent(), artifact.getName(), ".compact");
rewrite(source, temporary);
replace(temporary, source);
} catch (IOException exception) {
if (temporary != null) {
try {
Files.deleteIfExists(temporary);
} catch (IOException cleanupFailure) {
exception.addSuppressed(cleanupFailure);
}
}
throw new GradleException("Unable to compact jar artifact " + artifact.getAbsolutePath(), exception);
}
}
private static void rewrite(Path source, Path destination) throws IOException {
byte[] buffer = new byte[BUFFER_BYTES];
try (InputStream rawInput = new BufferedInputStream(Files.newInputStream(source));
ZipInputStream input = new ZipInputStream(rawInput);
OutputStream rawOutput = new BufferedOutputStream(Files.newOutputStream(destination));
ZipOutputStream output = new ZipOutputStream(rawOutput)) {
output.setLevel(9);
ZipEntry entry;
while ((entry = input.getNextEntry()) != null) {
if (entry.isDirectory()) {
continue;
}
ZipEntry compacted = copyMetadata(entry);
output.putNextEntry(compacted);
int read;
while ((read = input.read(buffer)) >= 0) {
if (read > 0) {
output.write(buffer, 0, read);
}
}
output.closeEntry();
}
}
}
private static ZipEntry copyMetadata(ZipEntry source) {
ZipEntry target = new ZipEntry(source.getName());
target.setMethod(ZipEntry.DEFLATED);
if (source.getTime() >= 0L) {
target.setTime(source.getTime());
}
if (source.getComment() != null) {
target.setComment(source.getComment());
}
if (source.getExtra() != null) {
target.setExtra(source.getExtra());
}
return target;
}
private static void replace(Path temporary, Path source) throws IOException {
try {
Files.move(
temporary,
source,
StandardCopyOption.ATOMIC_MOVE,
StandardCopyOption.REPLACE_EXISTING);
} catch (AtomicMoveNotSupportedException exception) {
Files.move(temporary, source, StandardCopyOption.REPLACE_EXISTING);
}
}
}
@@ -30,7 +30,6 @@ public class BukkitArtifactVerifierTest {
NMS_BINDING + ".class" NMS_BINDING + ".class"
); );
private static final int LOCALES = 2; private static final int LOCALES = 2;
private static final int CAFFEINE_FACTORIES = 2;
private static final int MATTER_SLICES = 1; private static final int MATTER_SLICES = 1;
@Rule @Rule
@@ -40,7 +39,7 @@ public class BukkitArtifactVerifierTest {
public void acceptsCompleteArtifact() throws Exception { public void acceptsCompleteArtifact() throws Exception {
File artifact = createArtifact(validEntries()); File artifact = createArtifact(validEntries());
BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, CAFFEINE_FACTORIES, MATTER_SLICES); BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, MATTER_SLICES, Long.MAX_VALUE);
} }
@Test @Test
@@ -50,8 +49,7 @@ public class BukkitArtifactVerifierTest {
File artifact = createArtifact(entries); File artifact = createArtifact(entries);
GradleException failure = assertThrows(GradleException.class, GradleException failure = assertThrows(GradleException.class,
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, CAFFEINE_FACTORIES, () -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, MATTER_SLICES, Long.MAX_VALUE));
MATTER_SLICES));
assertTrue(failure.getMessage().contains(PLUGIN_DESCRIPTOR)); assertTrue(failure.getMessage().contains(PLUGIN_DESCRIPTOR));
} }
@@ -62,8 +60,7 @@ public class BukkitArtifactVerifierTest {
File artifact = createArtifact(entries); File artifact = createArtifact(entries);
GradleException failure = assertThrows(GradleException.class, GradleException failure = assertThrows(GradleException.class,
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, CAFFEINE_FACTORIES, () -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, MATTER_SLICES, Long.MAX_VALUE));
MATTER_SLICES));
assertTrue(failure.getMessage().contains(SLIMJAR_DEPENDENCIES)); assertTrue(failure.getMessage().contains(SLIMJAR_DEPENDENCIES));
} }
@@ -74,8 +71,7 @@ public class BukkitArtifactVerifierTest {
File artifact = createArtifact(entries); File artifact = createArtifact(entries);
GradleException failure = assertThrows(GradleException.class, GradleException failure = assertThrows(GradleException.class,
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, CAFFEINE_FACTORIES, () -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, MATTER_SLICES, Long.MAX_VALUE));
MATTER_SLICES));
assertTrue(failure.getMessage().contains(SLIMJAR_RESOLUTIONS)); assertTrue(failure.getMessage().contains(SLIMJAR_RESOLUTIONS));
} }
@@ -86,8 +82,7 @@ public class BukkitArtifactVerifierTest {
File artifact = createArtifact(entries); File artifact = createArtifact(entries);
GradleException failure = assertThrows(GradleException.class, GradleException failure = assertThrows(GradleException.class,
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, CAFFEINE_FACTORIES, () -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, MATTER_SLICES, Long.MAX_VALUE));
MATTER_SLICES));
assertTrue(failure.getMessage().contains("art/arcane/volmlib/util/noise/CNG")); assertTrue(failure.getMessage().contains("art/arcane/volmlib/util/noise/CNG"));
} }
@@ -96,21 +91,31 @@ public class BukkitArtifactVerifierTest {
Map<String, byte[]> entries = validEntries(); Map<String, byte[]> entries = validEntries();
entries.put("art/arcane/iris/Consumer.class", entries.put("art/arcane/iris/Consumer.class",
classReferencing("art/arcane/iris/Consumer", "art/arcane/iris/util/kyori/adventure/text/Component")); classReferencing("art/arcane/iris/Consumer", "art/arcane/iris/util/kyori/adventure/text/Component"));
entries.put("art/arcane/iris/GsonConsumer.class",
classReferencing("art/arcane/iris/GsonConsumer", "art/arcane/iris/util/gson/Gson"));
entries.put("art/arcane/iris/LruConsumer.class",
classReferencing("art/arcane/iris/LruConsumer", "art/arcane/iris/util/lru/ConcurrentLinkedHashMap"));
entries.put("art/arcane/iris/CaffeineConsumer.class",
classReferencing("art/arcane/iris/CaffeineConsumer", "art/arcane/iris/util/caffeine/cache/Caffeine"));
entries.put("art/arcane/iris/ParalithicConsumer.class",
classReferencing("art/arcane/iris/ParalithicConsumer", "art/arcane/iris/util/paralithic/functions/Function"));
File artifact = createArtifact(entries); File artifact = createArtifact(entries);
BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, CAFFEINE_FACTORIES, MATTER_SLICES); BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, MATTER_SLICES, Long.MAX_VALUE);
} }
@Test @Test
public void rejectsStrippedCaffeineFactories() throws Exception { public void rejectsArtifactAboveConfiguredSize() throws Exception {
Map<String, byte[]> entries = validEntries(); File artifact = createArtifact(validEntries());
entries.remove("art/arcane/iris/util/caffeine/cache/SSMS.class");
File artifact = createArtifact(entries);
GradleException failure = assertThrows(GradleException.class, GradleException failure = assertThrows(GradleException.class,
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, CAFFEINE_FACTORIES, () -> BukkitArtifactVerifier.verify(
MATTER_SLICES)); artifact,
assertTrue(failure.getMessage().contains("generated Caffeine cache classes")); REQUIRED_ENTRIES,
LOCALES,
MATTER_SLICES,
artifact.length() - 1L));
assertTrue(failure.getMessage().contains("must not exceed"));
} }
@Test @Test
@@ -120,8 +125,7 @@ public class BukkitArtifactVerifierTest {
File artifact = createArtifact(entries); File artifact = createArtifact(entries);
GradleException failure = assertThrows(GradleException.class, GradleException failure = assertThrows(GradleException.class,
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, CAFFEINE_FACTORIES, () -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, MATTER_SLICES, Long.MAX_VALUE));
MATTER_SLICES));
assertTrue(failure.getMessage().contains("locale files")); assertTrue(failure.getMessage().contains("locale files"));
} }
@@ -132,8 +136,7 @@ public class BukkitArtifactVerifierTest {
File artifact = createArtifact(entries); File artifact = createArtifact(entries);
GradleException failure = assertThrows(GradleException.class, GradleException failure = assertThrows(GradleException.class,
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, CAFFEINE_FACTORIES, () -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, MATTER_SLICES, Long.MAX_VALUE));
MATTER_SLICES));
assertTrue(failure.getMessage().contains("Matter slice types")); assertTrue(failure.getMessage().contains("Matter slice types"));
} }
@@ -144,7 +147,7 @@ public class BukkitArtifactVerifierTest {
classAnnotatedWith("art/arcane/iris/Annotated", "com/google/errorprone/annotations/CanIgnoreReturnValue")); classAnnotatedWith("art/arcane/iris/Annotated", "com/google/errorprone/annotations/CanIgnoreReturnValue"));
File artifact = createArtifact(entries); File artifact = createArtifact(entries);
BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, CAFFEINE_FACTORIES, MATTER_SLICES); BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, MATTER_SLICES, Long.MAX_VALUE);
} }
private Map<String, byte[]> validEntries() { private Map<String, byte[]> validEntries() {
@@ -159,10 +162,6 @@ public class BukkitArtifactVerifierTest {
entries.put("art/arcane/volmlib/util/noise/CNG.class", emptyClass("art/arcane/volmlib/util/noise/CNG")); entries.put("art/arcane/volmlib/util/noise/CNG.class", emptyClass("art/arcane/volmlib/util/noise/CNG"));
entries.put("art/arcane/volmlib/util/matter/slices/BlockMatter.class", entries.put("art/arcane/volmlib/util/matter/slices/BlockMatter.class",
emptyClass("art/arcane/volmlib/util/matter/slices/BlockMatter")); emptyClass("art/arcane/volmlib/util/matter/slices/BlockMatter"));
entries.put("art/arcane/iris/util/caffeine/cache/SSMS.class",
emptyClass("art/arcane/iris/util/caffeine/cache/SSMS"));
entries.put("art/arcane/iris/util/caffeine/cache/SSLMS.class",
emptyClass("art/arcane/iris/util/caffeine/cache/SSLMS"));
return entries; return entries;
} }
@@ -0,0 +1,39 @@
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.File;
import java.io.FileOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertNull;
public class JarCompactorTest {
@Rule
public final TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void preservesFilesAndOmitsDirectoryEntries() throws Exception {
File artifact = temporaryFolder.newFile("artifact.jar");
byte[] content = "Iris artifact content".getBytes(StandardCharsets.UTF_8);
try (JarOutputStream output = new JarOutputStream(new FileOutputStream(artifact))) {
output.putNextEntry(new JarEntry("example/"));
output.closeEntry();
output.putNextEntry(new JarEntry("example/value.txt"));
output.write(content);
output.closeEntry();
}
JarCompactor.compact(artifact);
try (JarFile jar = new JarFile(artifact)) {
assertNull(jar.getJarEntry("example/"));
assertArrayEquals(content, jar.getInputStream(
jar.getJarEntry("example/value.txt")).readAllBytes());
}
}
}
+148 -6
View File
@@ -86,12 +86,11 @@ dependencies {
implementation(volmLibCoordinate) { implementation(volmLibCoordinate) {
transitive = false transitive = false
} }
implementation(libs.gson)
implementation(libs.lru)
implementation(libs.caffeine)
implementation(libs.paralithic)
// Dynamically Loaded // Dynamically Loaded
slim(libs.gson)
slim(libs.lru)
slim(libs.caffeine)
slim(libs.paralithic)
slim(libs.paperlib) slim(libs.paperlib)
slim(libs.adventure.api) slim(libs.adventure.api)
slim(libs.adventure.minimessage) slim(libs.adventure.minimessage)
@@ -154,6 +153,8 @@ slimJar {
] ]
relocate('com.dfsek.paralithic', "${lib}.paralithic") relocate('com.dfsek.paralithic', "${lib}.paralithic")
relocate('com.google.gson', "${lib}.gson")
relocate('com.googlecode.concurrentlinkedhashmap', "${lib}.lru")
relocate('io.papermc.lib', "${lib}.paper") relocate('io.papermc.lib', "${lib}.paper")
relocate('net.kyori', "${lib}.kyori") relocate('net.kyori', "${lib}.kyori")
relocate('org.bstats', "${lib}.metrics") relocate('org.bstats', "${lib}.metrics")
@@ -285,9 +286,149 @@ List<String> supersededVolmLibPackages = [
'art/arcane/volmlib/util/director/visual/**', 'art/arcane/volmlib/util/director/visual/**',
'art/arcane/volmlib/util/value/**', 'art/arcane/volmlib/util/value/**',
'art/arcane/volmlib/util/api/**', 'art/arcane/volmlib/util/api/**',
'art/arcane/volmlib/util/entity/**' 'art/arcane/volmlib/util/entity/**',
'art/arcane/volmlib/util/config/**',
'art/arcane/volmlib/util/reflect/**',
'art/arcane/volmlib/util/documentation/**',
'art/arcane/iris/core/report/**',
'art/arcane/iris/util/common/inventorygui/**',
'art/arcane/iris/util/common/board/**',
'art/arcane/volmlib/integration/VaultEconomy*.class'
] ]
List<String> unusedVolmLibClasses = [
'art/arcane/volmlib/util/nbt/io/ParseException',
'art/arcane/volmlib/util/nbt/io/SNBTDeserializer',
'art/arcane/volmlib/util/nbt/io/SNBTParser',
'art/arcane/volmlib/util/nbt/io/SNBTSerializer',
'art/arcane/volmlib/util/nbt/io/SNBTUtil',
'art/arcane/volmlib/util/nbt/io/SNBTWriter',
'art/arcane/volmlib/util/nbt/io/StringPointer',
'art/arcane/volmlib/util/io/StringDeserializer',
'art/arcane/volmlib/util/io/StringSerializer',
'art/arcane/volmlib/util/json/EnumType',
'art/arcane/volmlib/util/json/HTTP',
'art/arcane/volmlib/util/json/HTTPTokener',
'art/arcane/volmlib/util/json/JSONML',
'art/arcane/volmlib/util/json/JSONStringer',
'art/arcane/volmlib/util/json/JSONWriter',
'art/arcane/volmlib/util/json/XML',
'art/arcane/volmlib/util/json/XMLTokener',
'art/arcane/volmlib/util/hud/HudBid',
'art/arcane/volmlib/util/hud/HudBidder',
'art/arcane/volmlib/util/hud/HudLocalLedger',
'art/arcane/volmlib/util/hud/HudTitleClaim',
'art/arcane/volmlib/util/hud/HudTitleService',
'art/arcane/volmlib/util/scheduling/GroupedExecutor',
'art/arcane/volmlib/util/scheduling/SchedulerRuntime',
'art/arcane/volmlib/util/scheduling/SchedulerUtils',
'art/arcane/volmlib/util/scheduling/TaskExecutor',
'art/arcane/volmlib/util/parallel/StreamUtilsSupport',
'art/arcane/volmlib/util/parallel/SyncExecutorSupport',
'art/arcane/volmlib/util/cache/ByteBitCache',
'art/arcane/volmlib/util/cache/DataBitCache',
'art/arcane/volmlib/util/cache/FloatBitCache',
'art/arcane/volmlib/util/cache/IntBitCache',
'art/arcane/volmlib/util/cache/ShortBitCache',
'art/arcane/volmlib/util/cache/UByteBitCache',
'art/arcane/volmlib/util/data/ChunkCache',
'art/arcane/volmlib/util/data/ComplexCache',
'art/arcane/volmlib/util/data/IrisBiomeStorage',
'art/arcane/volmlib/util/data/NibbleArray',
'art/arcane/volmlib/util/data/NibbleDataPalette',
'art/arcane/volmlib/util/data/Recycler',
'art/arcane/volmlib/util/data/VanillaBiomeMap',
'art/arcane/volmlib/util/data/Writable',
'art/arcane/volmlib/util/data/base/ComplexCacheBase',
'art/arcane/volmlib/util/director/DirectorNodeBase',
'art/arcane/volmlib/util/director/DirectorParameterBase',
'art/arcane/volmlib/util/director/theme/DirectorThemes',
'art/arcane/volmlib/util/interpolation/InterpolationMethod',
'art/arcane/volmlib/util/interpolation/IrisInterpolation',
'art/arcane/volmlib/util/math/INode',
'art/arcane/volmlib/util/math/IrisMathHelper',
'art/arcane/volmlib/util/math/KochanekBartelsInterpolation',
'art/arcane/volmlib/util/math/PathInterpolation',
'art/arcane/volmlib/util/math/Spiral',
'art/arcane/volmlib/util/board/BoardManager',
'art/arcane/volmlib/util/board/BoardUpdateTask',
'art/arcane/volmlib/util/bukkit/Placeholders',
'art/arcane/volmlib/util/hunk/HunkFactory',
'art/arcane/volmlib/util/hunk/storage/ArrayHunk',
'art/arcane/volmlib/util/hunk/storage/AtomicDoubleHunk',
'art/arcane/volmlib/util/hunk/storage/AtomicIntegerHunk',
'art/arcane/volmlib/util/hunk/storage/AtomicLongHunk',
'art/arcane/volmlib/util/hunk/storage/SynchronizedArrayHunk',
'art/arcane/volmlib/util/hunk/view/BiomeForceBridge',
'art/arcane/volmlib/util/hunk/view/BiomeGridForceSupport',
'art/arcane/volmlib/util/hunk/view/BiomeGridHunkHolder',
'art/arcane/volmlib/util/hunk/view/BiomeGridHunkView',
'art/arcane/volmlib/util/hunk/view/ChunkDataHunkHolder',
'art/arcane/volmlib/util/hunk/view/RotatedXHunkView',
'art/arcane/volmlib/util/hunk/view/RotatedYHunkView',
'art/arcane/volmlib/util/hunk/view/RotatedZHunkView',
'art/arcane/volmlib/util/exceptions/MissingDimensionException',
'art/arcane/volmlib/util/function/Supplier2',
'art/arcane/volmlib/util/function/Supplier3',
'art/arcane/volmlib/util/hud/HudPriority',
'art/arcane/volmlib/util/interpolation/InterpolationMethod3D',
'art/arcane/volmlib/util/interpolation/InterpolationType',
'art/arcane/volmlib/util/inventorygui/UIRainbowDecorator',
'art/arcane/volmlib/util/io/Converter',
'art/arcane/volmlib/util/matter/MatterPlacer',
'art/arcane/volmlib/util/plugin/SplashScreenSupport',
'art/arcane/volmlib/util/scheduling/Contained',
'art/arcane/volmlib/util/scheduling/IrisLock',
'art/arcane/volmlib/util/scheduling/QueueExecutor',
'art/arcane/volmlib/util/scheduling/S',
'art/arcane/volmlib/util/scheduling/SBase',
'art/arcane/volmlib/util/scheduling/SlidingWindowRateLimiter',
'art/arcane/volmlib/util/scheduling/Switch',
'art/arcane/volmlib/util/scheduling/ThreadMonitor',
'art/arcane/volmlib/util/cache/ByteCache',
'art/arcane/volmlib/util/cache/ChunkCache2DSimple',
'art/arcane/volmlib/util/cache/IntCache',
'art/arcane/volmlib/util/cache/ShortCache',
'art/arcane/volmlib/util/cache/UByteCache',
'art/arcane/volmlib/util/cache/WorldCache2DSimple',
'art/arcane/volmlib/util/data/CuboidException',
'art/arcane/volmlib/util/data/DUTF',
'art/arcane/volmlib/util/data/Heafty',
'art/arcane/volmlib/util/data/InvertedBiomeGrid',
'art/arcane/volmlib/util/data/Shrinkwrap',
'art/arcane/volmlib/util/data/WeightMap',
'art/arcane/volmlib/util/data/WeightedRandom',
'art/arcane/volmlib/util/director/compat/BukkitDirectorContext',
'art/arcane/volmlib/util/director/handlers/base/DummyHandlerBase',
'art/arcane/volmlib/util/director/handlers/base/OptionalWorldHandlerBase',
'art/arcane/volmlib/util/parallel/BurstedHunk',
'art/arcane/volmlib/util/parallel/GridLockSupport',
'art/arcane/volmlib/util/parallel/NoopGridLockSupport',
'art/arcane/volmlib/util/bukkit/ChunkPositionSet',
'art/arcane/volmlib/util/bukkit/Events',
'art/arcane/volmlib/util/json/SingleCollectionTypeFactory',
'art/arcane/volmlib/util/collection/StateList',
'art/arcane/iris/core/pack/IrisPack',
'art/arcane/iris/core/pack/IrisPackRepository',
'art/arcane/iris/core/jobs/DownloadJob',
'art/arcane/iris/core/jobs/JobCollection',
'art/arcane/iris/core/jobs/ParallelQueueJob',
'art/arcane/iris/core/jobs/QueueJob',
'art/arcane/iris/engine/framework/placer/WorldObjectPlacer',
'art/arcane/iris/engine/object/IPostBlockAccess',
'art/arcane/iris/engine/object/IrisPotionEffect',
'art/arcane/iris/engine/platform/DummyChunkGenerator',
'art/arcane/iris/util/common/math/VectorMath',
'art/arcane/iris/util/common/parallel/GridLock',
'art/arcane/iris/util/common/reflect/W',
'art/arcane/iris/util/slimjar/BuildConstants',
'art/arcane/iris/util/slimjar/relocation/meta/AttributeMetaMediator',
'art/arcane/iris/util/slimjar/relocation/meta/AttributeMetaMediatorFactory'
]
List<String> unusedVolmLibClassEntries = unusedVolmLibClasses.collectMany { String classPath ->
[classPath + '.class', classPath + '$*.class']
}
// Annotation-only artifacts pulled in transitively by Gson and Caffeine. Their types appear solely // Annotation-only artifacts pulled in transitively by Gson and Caffeine. Their types appear solely
// in annotation attributes, which the JVM skips silently when the type is absent, so nothing loads // in annotation attributes, which the JVM skips silently when the type is absent, so nothing loads
// or links against them at runtime. // or links against them at runtime.
@@ -315,6 +456,7 @@ tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.Shadow
relocate('io.github.slimjar', "${lib}.slimjar") relocate('io.github.slimjar', "${lib}.slimjar")
exclude('modules/loader-agent.isolated-jar') exclude('modules/loader-agent.isolated-jar')
exclude(supersededVolmLibPackages) exclude(supersededVolmLibPackages)
exclude(unusedVolmLibClassEntries)
exclude(annotationOnlyArtifacts) exclude(annotationOnlyArtifacts)
exclude(dependencyBuildMetadata) exclude(dependencyBuildMetadata)
from(embeddedAgentJar.map { it.archiveFile }) { from(embeddedAgentJar.map { it.archiveFile }) {
@@ -39,7 +39,6 @@ import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KSet; import art.arcane.volmlib.util.collection.KSet;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.misc.ServerProperties; import art.arcane.iris.util.common.misc.ServerProperties;
import art.arcane.iris.util.common.plugin.VolmitSender; import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J; import art.arcane.iris.util.common.scheduling.J;
@@ -90,6 +89,8 @@ public class ServerConfigurator {
private static final Object DATAPACK_INSTALL_LOCK = new Object(); private static final Object DATAPACK_INSTALL_LOCK = new Object();
private static final String CODE_WORKSPACE_SUFFIX = ".code-workspace"; private static final String CODE_WORKSPACE_SUFFIX = ".code-workspace";
private static final String COMPILER_INPUT_FINGERPRINT_CACHE = "datapack-compiler-input-fingerprint"; private static final String COMPILER_INPUT_FINGERPRINT_CACHE = "datapack-compiler-input-fingerprint";
static final String POST_COMPILE_RESTART_WARNING = "Iris installed updated datapack registry entries; "
+ "restart the server before creating worlds or opening Studios.";
private static final int FINGERPRINT_BUFFER_BYTES = 64 * 1024; private static final int FINGERPRINT_BUFFER_BYTES = 64 * 1024;
private static volatile boolean loadedDatapackRuntimeReady; private static volatile boolean loadedDatapackRuntimeReady;
private static volatile String loadedDatapackCompilerInputFingerprint = ""; private static volatile String loadedDatapackCompilerInputFingerprint = "";
@@ -419,13 +420,18 @@ public class ServerConfigurator {
&& !reusableRuntimeFingerprint( && !reusableRuntimeFingerprint(
loadedDatapackCompilerInputFingerprint, loadedDatapackCompilerInputFingerprint,
current); current);
boolean loadedRegistryRestartRequired = fullInstall
&& loadedCompilerInputsChanged
&& currentRegistryRequiresRestart();
if (!current.isEmpty() && current.equals(cached)) { if (!current.isEmpty() && current.equals(cached)) {
IrisLogging.debug("Data packs unchanged, skipping install."); IrisLogging.debug("Data packs unchanged, skipping install.");
DatapackInstallResult result = fullInstall && loadedCompilerInputsChanged DatapackInstallResult result = loadedRegistryRestartRequired
? DatapackInstallResult.restartRequiredResult() ? DatapackInstallResult.restartRequiredResult()
: resultForUnchangedFingerprint(fullInstall, reapply); : resultForUnchangedFingerprint(fullInstall, reapply);
if (result.restartRequired()) { if (result.restartRequired()) {
requireDatapackRestart(); requireDatapackRestart();
} else if (result.succeeded()) {
loadedDatapackCompilerInputFingerprint = current;
} }
reportTiming(timingConsumer, "datapack_install_if_changed_total", totalStart); reportTiming(timingConsumer, "datapack_install_if_changed_total", totalStart);
return result; return result;
@@ -435,7 +441,7 @@ public class ServerConfigurator {
resolveDataFixer(), resolveDataFixer(),
fullInstall, fullInstall,
reapply); reapply);
if (fullInstall && loadedCompilerInputsChanged && result.succeeded()) { if (loadedRegistryRestartRequired && result.succeeded()) {
result = DatapackInstallResult.restartRequiredResult(); result = DatapackInstallResult.restartRequiredResult();
} }
if (result.restartRequired()) { if (result.restartRequired()) {
@@ -444,6 +450,7 @@ public class ServerConfigurator {
reportTiming(timingConsumer, "datapack_compile_publish", compileStart); reportTiming(timingConsumer, "datapack_compile_publish", compileStart);
if (result.succeeded() && !result.restartRequired()) { if (result.succeeded() && !result.restartRequired()) {
writeCompilerInputFingerprintCache(cacheFile.toPath(), current); writeCompilerInputFingerprintCache(cacheFile.toPath(), current);
loadedDatapackCompilerInputFingerprint = current;
} }
reportTiming(timingConsumer, "datapack_install_if_changed_total", totalStart); reportTiming(timingConsumer, "datapack_install_if_changed_total", totalStart);
return result; return result;
@@ -456,6 +463,22 @@ public class ServerConfigurator {
IrisWorldStorage.levelRoot().toPath()); IrisWorldStorage.levelRoot().toPath());
} }
private static boolean currentRegistryRequiresRestart() {
try {
Map<String, String> currentRequirements = IrisDatapackCompiler.computeRegistryRequirements(
collectCompilerPackRoots(),
resolveDataFixer());
return runtimeRequiresRegistryRestart(
loadedDatapackRegistryRequirements,
currentRequirements);
} catch (IOException | RuntimeException exception) {
IrisLogging.reportError(
"Unable to compare loaded Iris datapack registry requirements.",
exception);
return true;
}
}
private static List<IrisGeneratorBinding> collectConfiguredLevelStemBindings() throws IOException { private static List<IrisGeneratorBinding> collectConfiguredLevelStemBindings() throws IOException {
File levelRoot = IrisWorldStorage.levelRoot(); File levelRoot = IrisWorldStorage.levelRoot();
String levelId = levelRoot.getName(); String levelId = levelRoot.getName();
@@ -545,6 +568,19 @@ public class ServerConfigurator {
return true; return true;
} }
static boolean runtimeRequiresRegistryRestart(
Map<String, String> loadedRequirements,
Map<String, String> currentRequirements
) {
if (currentRequirements == null) {
return true;
}
if (currentRequirements.isEmpty()) {
return false;
}
return !loadedRegistrySatisfies(loadedRequirements, currentRequirements);
}
static boolean reusableRuntimeFingerprint(String loadedFingerprint, String currentFingerprint) { static boolean reusableRuntimeFingerprint(String loadedFingerprint, String currentFingerprint) {
return loadedFingerprint != null return loadedFingerprint != null
&& !loadedFingerprint.isBlank() && !loadedFingerprint.isBlank()
@@ -961,31 +997,29 @@ public class ServerConfigurator {
private static boolean verifyDataPacksPost() { private static boolean verifyDataPacksPost() {
try (Stream<IrisData> stream = allPacks()) { try (Stream<IrisData> stream = allPacks()) {
boolean bad = stream return verifyDataPacksPost(stream);
.map(data -> {
IrisLogging.debug("Checking Pack: " + data.getDataFolder().getPath());
ResourceLoader<IrisDimension> loader = data.getDimensionLoader();
return loader.loadAll(loader.getPossibleKeys())
.stream()
.filter(Objects::nonNull)
.map(ServerConfigurator::verifyDataPackInstalled)
.toList()
.contains(false);
})
.toList()
.contains(true);
if (!bad) {
return false;
}
} }
}
static boolean verifyDataPacksPost(Stream<IrisData> packs) {
boolean bad = Objects.requireNonNull(packs, "Iris packs")
.map(data -> {
IrisLogging.debug("Checking Pack: " + data.getDataFolder().getPath());
ResourceLoader<IrisDimension> loader = data.getDimensionLoader();
return loader.loadAll(loader.getPossibleKeys())
.stream()
.filter(Objects::nonNull)
.map(dimension -> verifyDataPackInstalled(dimension, false))
.toList()
.contains(false);
})
.toList()
.contains(true);
if (!bad) {
return false;
}
if (INMS.get().supportsDataPacks()) { if (INMS.get().supportsDataPacks()) {
// Three sentences, no rules: a separator carries the record's severity too, so a box drawn IrisLogging.warn(POST_COMPILE_RESTART_WARNING);
// out of equals signs became three more [SEVERE] lines saying nothing.
IrisLogging.error(C.ITALIC + "You need to restart your server to properly generate custom biomes.");
IrisLogging.error(C.ITALIC + "By continuing, Iris will use backup biomes in place of the custom biomes.");
IrisLogging.error(C.UNDERLINE + "Restart the server before generating.");
for (Player i : Bukkit.getOnlinePlayers()) { for (Player i : Bukkit.getOnlinePlayers()) {
if (i.isOp() || i.hasPermission("iris.all")) { if (i.isOp() || i.hasPermission("iris.all")) {
@@ -1054,6 +1088,10 @@ public class ServerConfigurator {
} }
public static boolean verifyDataPackInstalled(IrisDimension dimension) { public static boolean verifyDataPackInstalled(IrisDimension dimension) {
return verifyDataPackInstalled(dimension, true);
}
private static boolean verifyDataPackInstalled(IrisDimension dimension, boolean reportRuntimeFailure) {
KSet<String> keys = new KSet<>(); KSet<String> keys = new KSet<>();
boolean warn = false; boolean warn = false;
@@ -1082,17 +1120,21 @@ public class ServerConfigurator {
Object o = INMS.get().getCustomBiomeBaseFor(i); Object o = INMS.get().getCustomBiomeBaseFor(i);
if (o == null) { if (o == null) {
IrisLogging.warn("The Biome " + i + " is not registered on the server."); if (reportRuntimeFailure) {
IrisLogging.warn("The Biome " + i + " is not registered on the server.");
}
warn = true; warn = true;
} }
} }
if (INMS.get().missingDimensionTypes(dimension.getDimensionTypeKey())) { if (INMS.get().missingDimensionTypes(dimension.getDimensionTypeKey())) {
IrisLogging.warn("The Dimension Type for " + dimension.getLoadFile() + " is not registered on the server."); if (reportRuntimeFailure) {
IrisLogging.warn("The Dimension Type for " + dimension.getLoadFile() + " is not registered on the server.");
}
warn = true; warn = true;
} }
if (warn) { if (warn && reportRuntimeFailure) {
IrisLogging.error("The Pack " + key + " is INCAPABLE of generating custom biomes"); IrisLogging.error("The Pack " + key + " is INCAPABLE of generating custom biomes");
IrisLogging.error("If not done automatically, restart your server before generating with this pack!"); IrisLogging.error("If not done automatically, restart your server before generating with this pack!");
} }
@@ -31,6 +31,7 @@ import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent; import java.awt.event.WindowEvent;
import java.util.Locale; import java.util.Locale;
import java.util.Set; import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
@@ -63,7 +64,7 @@ public final class GuiHost {
default void unregisterHotloadHook(Runnable onHotload) { default void unregisterHotloadHook(Runnable onHotload) {
} }
default GuiOverlay overlayFor(Engine engine) { default GuiOverlay overlayFor(Engine engine, UUID openerId) {
return null; return null;
} }
} }
@@ -73,6 +73,7 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.UUID;
public final class VisionGUI extends JPanel implements MouseWheelListener, KeyListener, MouseMotionListener, MouseInputListener { public final class VisionGUI extends JPanel implements MouseWheelListener, KeyListener, MouseMotionListener, MouseInputListener {
private static final long serialVersionUID = 2094606939770332040L; private static final long serialVersionUID = 2094606939770332040L;
@@ -107,6 +108,7 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi
private static final double KEYBOARD_ZOOM_FACTOR = 1.189207115002721D; private static final double KEYBOARD_ZOOM_FACTOR = 1.189207115002721D;
private final JFrame hostFrame; private final JFrame hostFrame;
private final UUID openerId;
private final VisionRenderController controller; private final VisionRenderController controller;
private final Runnable hotloadHook; private final Runnable hotloadHook;
private final Timer resizeTimer; private final Timer resizeTimer;
@@ -147,11 +149,12 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi
private boolean controlsUpdating; private boolean controlsUpdating;
private boolean closed; private boolean closed;
private VisionGUI(JFrame hostFrame, Engine engine) { private VisionGUI(JFrame hostFrame, Engine engine, UUID openerId) {
this.hostFrame = Objects.requireNonNull(hostFrame, "hostFrame"); this.hostFrame = Objects.requireNonNull(hostFrame, "hostFrame");
this.engine = Objects.requireNonNull(engine, "engine"); this.engine = Objects.requireNonNull(engine, "engine");
this.openerId = openerId;
this.renderer = new IrisRenderer(engine); this.renderer = new IrisRenderer(engine);
this.overlay = GuiHost.get().overlayFor(engine); this.overlay = GuiHost.get().overlayFor(engine, openerId);
this.controller = new VisionRenderController(this::repaint); this.controller = new VisionRenderController(this::repaint);
this.hotloadHook = () -> EventQueue.invokeLater(this::refreshContent); this.hotloadHook = () -> EventQueue.invokeLater(this::refreshContent);
this.notifications = new LinkedHashMap<>(); this.notifications = new LinkedHashMap<>();
@@ -201,14 +204,14 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi
notificationTimer.start(); notificationTimer.start();
} }
public static void launch(Engine engine) { public static void launch(Engine engine, UUID openerId) {
EventQueue.invokeLater(() -> createAndShowGUI(engine)); EventQueue.invokeLater(() -> createAndShowGUI(engine, openerId));
} }
private static void createAndShowGUI(Engine engine) { private static void createAndShowGUI(Engine engine, UUID openerId) {
JFrame frame = new JFrame(IrisLanguage.plain(DesktopUiMessages.VISION_TITLE)); JFrame frame = new JFrame(IrisLanguage.plain(DesktopUiMessages.VISION_TITLE));
GuiHost.prepareFrame(frame); GuiHost.prepareFrame(frame);
VisionGUI vision = new VisionGUI(frame, engine); VisionGUI vision = new VisionGUI(frame, engine, openerId);
frame.getContentPane().setBackground(BACKGROUND); frame.getContentPane().setBackground(BACKGROUND);
frame.setLayout(new BorderLayout()); frame.setLayout(new BorderLayout());
frame.add(buildToolbar(vision), BorderLayout.NORTH); frame.add(buildToolbar(vision), BorderLayout.NORTH);
@@ -739,7 +742,7 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi
} }
engine = reacquired; engine = reacquired;
renderer = new IrisRenderer(reacquired); renderer = new IrisRenderer(reacquired);
overlay = GuiHost.get().overlayFor(reacquired); overlay = GuiHost.get().overlayFor(reacquired, openerId);
contentRevision++; contentRevision++;
captureHeightRange(); captureHeightRange();
return true; return true;
@@ -758,7 +761,7 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi
return; return;
} }
renderer = new IrisRenderer(engine); renderer = new IrisRenderer(engine);
overlay = GuiHost.get().overlayFor(engine); overlay = GuiHost.get().overlayFor(engine, openerId);
contentRevision++; contentRevision++;
captureHeightRange(); captureHeightRange();
requestRender(); requestRender();
@@ -56,6 +56,10 @@ public final class IrisLanguage {
private static final Pattern LOCALE_NAME = Pattern.compile("[A-Za-z0-9_-]+"); private static final Pattern LOCALE_NAME = Pattern.compile("[A-Za-z0-9_-]+");
private static final Pattern LEGACY_COLOR = Pattern.compile("(?i)\\u00a7[0-9A-FK-ORX]"); private static final Pattern LEGACY_COLOR = Pattern.compile("(?i)\\u00a7[0-9A-FK-ORX]");
private static final MessageCatalog CATALOG = IrisMessages.catalog(); private static final MessageCatalog CATALOG = IrisMessages.catalog();
private static final List<String> BUKKIT_MESSAGE_IDS = CATALOG.ids().stream()
.filter(id -> !id.startsWith("iris.modded."))
.sorted()
.toList();
private static final LocalizationManager MANAGER = new LocalizationManager( private static final LocalizationManager MANAGER = new LocalizationManager(
LocalizationCandidate.english(CATALOG, PluralSelector.oneOther()) LocalizationCandidate.english(CATALOG, PluralSelector.oneOther())
); );
@@ -369,8 +373,11 @@ public final class IrisLanguage {
} }
} }
private static LocaleOverlay parseOverlay(String source, String locale, String raw) { static LocaleOverlay parseOverlay(String source, String locale, String raw) {
JsonElement parsed = JsonParser.parseString(raw == null || raw.isBlank() ? "{}" : raw); JsonElement parsed = JsonParser.parseString(raw == null || raw.isBlank() ? "{}" : raw);
if (parsed.isJsonArray()) {
return parseCompactBukkitOverlay(source, locale, parsed.getAsJsonArray());
}
if (!parsed.isJsonObject()) { if (!parsed.isJsonObject()) {
throw new IllegalArgumentException("Locale source is not a JSON object: " + source); throw new IllegalArgumentException("Locale source is not a JSON object: " + source);
} }
@@ -398,6 +405,49 @@ public final class IrisLanguage {
return builder.build(); return builder.build();
} }
private static LocaleOverlay parseCompactBukkitOverlay(
String source,
String locale,
JsonArray root
) {
if (root.size() != 2 || !root.get(0).isJsonPrimitive() || !root.get(1).isJsonArray()) {
throw new IllegalArgumentException("Compact locale source is invalid: " + source);
}
if (!locale.equals(normalizeLocale(root.get(0).getAsString()))) {
throw new IllegalArgumentException("Locale source declares a different locale than its file: " + source);
}
JsonArray messages = root.get(1).getAsJsonArray();
if (messages.size() != BUKKIT_MESSAGE_IDS.size()) {
throw new IllegalArgumentException("Compact locale message count is invalid: " + source);
}
LocaleOverlay.Builder builder = LocaleOverlay.builder(source, locale);
for (int i = 0; i < messages.size(); i++) {
JsonElement value = messages.get(i);
if (value == null || value.isJsonNull()) {
continue;
}
appendCompactMessage(builder, BUKKIT_MESSAGE_IDS.get(i), value);
}
return builder.build();
}
private static void appendCompactMessage(
LocaleOverlay.Builder builder,
String key,
JsonElement value
) {
MessageKey definition = CATALOG.key(key);
if (value.isJsonObject() && definition instanceof PluralKey) {
builder.plural(key, readPlural(key, value.getAsJsonObject()));
} else if (value.isJsonArray()) {
builder.lines(key, readLines(key, value.getAsJsonArray()));
} else if (value.isJsonPrimitive() && value.getAsJsonPrimitive().isString()) {
builder.text(key, value.getAsString());
} else {
throw new IllegalArgumentException("Compact locale value has an invalid shape: " + key);
}
}
private static void appendMessages(LocaleOverlay.Builder builder, JsonObject object, String prefix) { private static void appendMessages(LocaleOverlay.Builder builder, JsonObject object, String prefix) {
for (Map.Entry<String, JsonElement> entry : object.entrySet()) { for (Map.Entry<String, JsonElement> entry : object.entrySet()) {
String key = prefix.isEmpty() ? entry.getKey() : prefix + "." + entry.getKey(); String key = prefix.isEmpty() ? entry.getKey() : prefix + "." + entry.getKey();
@@ -13,7 +13,7 @@ import java.util.List;
import java.util.Set; import java.util.Set;
final class PackRiverValidator { final class PackRiverValidator {
private static final Set<String> WATER_MODES = Set.of("SEA_LEVEL", "TERRACED"); private static final Set<String> WATER_MODES = Set.of("FIXED", "TERRACED");
private static final Set<String> TERMINAL_MODES = Set.of("SUPPRESS", "DRY_CHANNEL", "SINKHOLE_GROTTO"); private static final Set<String> TERMINAL_MODES = Set.of("SUPPRESS", "DRY_CHANNEL", "SINKHOLE_GROTTO");
private static final Set<String> ROUTING_POLICIES = Set.of("ALLOW", "AVOID", "BLOCK"); private static final Set<String> ROUTING_POLICIES = Set.of("ALLOW", "AVOID", "BLOCK");
private static final Set<String> CAVE_MODES = Set.of( private static final Set<String> CAVE_MODES = Set.of(
@@ -91,7 +91,7 @@ final class PackRiverValidator {
validateTerrain(packFolder, path + ".terrain", terrain, errors, warnings); validateTerrain(packFolder, path + ".terrain", terrain, errors, warnings);
} }
if (water != null) { if (water != null) {
validateWater(path + ".water", water, errors); validateWater(path + ".water", water, context.dimension(), errors);
} }
if (biomes != null) { if (biomes != null) {
validateBiomePools( validateBiomePools(
@@ -107,7 +107,15 @@ final class PackRiverValidator {
boolean sinkholeTerminal = terrain != null boolean sinkholeTerminal = terrain != null
&& "SINKHOLE_GROTTO".equals(stringValue(terrain, "terminalMode", "DRY_CHANNEL")); && "SINKHOLE_GROTTO".equals(stringValue(terrain, "terminalMode", "DRY_CHANNEL"));
if (caves != null) { if (caves != null) {
validateCaves(packFolder, path + ".caves", caves, sinkholeTerminal, errors, warnings); validateCaves(
packFolder,
path + ".caves",
caves,
context.dimension(),
sinkholeTerminal,
errors,
warnings
);
} }
if (topology != null && terrain != null) { if (topology != null && terrain != null) {
@@ -168,6 +176,7 @@ final class PackRiverValidator {
validateStyledRange(packFolder, terrain, "bankWidth", path, 0D, 2048D, errors, warnings); validateStyledRange(packFolder, terrain, "bankWidth", path, 0D, 2048D, errors, warnings);
validateStyledRange(packFolder, terrain, "depth", path, 1D, 512D, errors, warnings); validateStyledRange(packFolder, terrain, "depth", path, 1D, 512D, errors, warnings);
validateStyledRange(packFolder, terrain, "tunnelWidthMultiplier", path, 1D, 8D, errors, warnings); validateStyledRange(packFolder, terrain, "tunnelWidthMultiplier", path, 1D, 8D, errors, warnings);
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "channelRadiusBonus", 0D, 64D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "maxChannelWidth", 1D, 2048D, errors); PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "maxChannelWidth", 1D, 2048D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "maxBankWidth", 0D, 2048D, errors); PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "maxBankWidth", 0D, 2048D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "maxDepth", 1D, 512D, errors); PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "maxDepth", 1D, 512D, errors);
@@ -211,18 +220,50 @@ final class PackRiverValidator {
} }
} }
private static void validateWater(String path, JSONObject water, List<String> errors) { private static void validateWater(
String path,
JSONObject water,
JSONObject dimension,
List<String> errors
) {
PackJsonFieldChecks.validateOptionalEnum(path, water, "mode", WATER_MODES, errors); PackJsonFieldChecks.validateOptionalEnum(path, water, "mode", WATER_MODES, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, water, "poolLength", 8, 4096, errors); PackJsonFieldChecks.validateOptionalIntegerRange(path, water, "poolLength", 8, 4096, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, water, "maximumPoolRise", 0, 64, errors); PackJsonFieldChecks.validateOptionalIntegerRange(path, water, "maximumPoolRise", 0, 64, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, water, "dropHeight", 1, 32, errors); PackJsonFieldChecks.validateOptionalIntegerRange(path, water, "dropHeight", 1, 32, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, water, "fluidHeight", -2048, 2048, errors);
validateFluidPalette(path, water, errors);
String mode = stringValue(water, "mode", "SEA_LEVEL"); String mode = stringValue(water, "mode", "FIXED");
int fluidHeight = integerValue(water, "fluidHeight", 63);
int maximumPoolRise = integerValue(water, "maximumPoolRise", 4); int maximumPoolRise = integerValue(water, "maximumPoolRise", 4);
int dropHeight = integerValue(water, "dropHeight", 1); int dropHeight = integerValue(water, "dropHeight", 1);
if ("TERRACED".equals(mode) && dropHeight > maximumPoolRise) { if ("TERRACED".equals(mode) && dropHeight > maximumPoolRise) {
errors.add(path + ".dropHeight must not exceed maximumPoolRise in TERRACED mode."); errors.add(path + ".dropHeight must not exceed maximumPoolRise in TERRACED mode.");
} }
JSONObject dimensionHeight = dimension.optJSONObject("dimensionHeight");
int minimumHeight = dimensionHeight == null ? -64 : integerValue(dimensionHeight, "min", -64);
int maximumHeight = dimensionHeight == null ? 320 : integerValue(dimensionHeight, "max", 320);
if (fluidHeight < minimumHeight || fluidHeight > maximumHeight) {
errors.add(path + ".fluidHeight must remain inside dimensionHeight.");
}
if ("TERRACED".equals(mode) && fluidHeight + maximumPoolRise > maximumHeight) {
errors.add(path + ".fluidHeight plus maximumPoolRise must remain inside dimensionHeight.");
}
}
private static void validateFluidPalette(String path, JSONObject water, List<String> errors) {
if (!water.has("fluidPalette")) {
return;
}
Object rawPalette = water.opt("fluidPalette");
if (!(rawPalette instanceof JSONObject palette)) {
errors.add(path + ".fluidPalette must be an object.");
return;
}
Object rawBlocks = palette.opt("palette");
if (!(rawBlocks instanceof JSONArray blocks) || blocks.length() < 1) {
errors.add(path + ".fluidPalette.palette must contain at least one fluid block.");
}
} }
private static void validateTopologyComplexity( private static void validateTopologyComplexity(
@@ -345,9 +386,11 @@ final class PackRiverValidator {
PackJsonFieldChecks.validateOptionalDoubleRange( PackJsonFieldChecks.validateOptionalDoubleRange(
wormPath, worm, "depthMultiplier", 0.125D, 8D, errors); wormPath, worm, "depthMultiplier", 0.125D, 8D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange( PackJsonFieldChecks.validateOptionalDoubleRange(
wormPath, worm, "bodyWavelength", 32D, 16384D, errors); wormPath, worm, "bodyWavelength", 8D, 16384D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange( PackJsonFieldChecks.validateOptionalDoubleRange(
wormPath, worm, "bodyDetailWavelength", 32D, 16384D, errors); wormPath, worm, "bodyDetailWavelength", 8D, 16384D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(
wormPath, worm, "bodyDetailInfluence", 0D, 1D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange( PackJsonFieldChecks.validateOptionalDoubleRange(
wormPath, worm, "widthVariation", 0D, 0.875D, errors); wormPath, worm, "widthVariation", 0D, 0.875D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange( PackJsonFieldChecks.validateOptionalDoubleRange(
@@ -452,6 +495,7 @@ final class PackRiverValidator {
} }
private static void validateCaves(File packFolder, String path, JSONObject caves, private static void validateCaves(File packFolder, String path, JSONObject caves,
JSONObject dimension,
boolean forceGeneratedGrotto, boolean forceGeneratedGrotto,
List<String> errors, List<String> warnings) { List<String> errors, List<String> warnings) {
PackJsonFieldChecks.validateOptionalEnum(path, caves, "mode", CAVE_MODES, errors); PackJsonFieldChecks.validateOptionalEnum(path, caves, "mode", CAVE_MODES, errors);
@@ -473,6 +517,19 @@ final class PackRiverValidator {
validateNoiseChance(packFolder, caves, "entry", path, errors); validateNoiseChance(packFolder, caves, "entry", path, errors);
validateStyle(packFolder, caves, "grottoShapeStyle", path, errors); validateStyle(packFolder, caves, "grottoShapeStyle", path, errors);
validateStyle(packFolder, caves, "grottoWarpStyle", path, errors); validateStyle(packFolder, caves, "grottoWarpStyle", path, errors);
JSONObject deepPools = caves.has("deepPools")
? requireObject(caves, "deepPools", path + ".deepPools", errors)
: null;
if (deepPools != null) {
validateDeepPools(
packFolder,
path + ".deepPools",
deepPools,
dimension,
errors,
warnings
);
}
String mode = stringValue(caves, "mode", "SEALED"); String mode = stringValue(caves, "mode", "SEALED");
if ("SEALED".equals(mode) && !forceGeneratedGrotto) { if ("SEALED".equals(mode) && !forceGeneratedGrotto) {
@@ -505,6 +562,86 @@ final class PackRiverValidator {
} }
} }
private static void validateDeepPools(
File packFolder,
String path,
JSONObject deepPools,
JSONObject dimension,
List<String> errors,
List<String> warnings
) {
PackJsonFieldChecks.validateOptionalBoolean(path, deepPools, "enabled", errors);
PackJsonFieldChecks.validateOptionalIntegerRange(
path, deepPools, "minimumSpacing", 16, 4096, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(
path, deepPools, "maximumPerReach", 0, 16, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(
path, deepPools, "minimumFluidY", -2048, 2048, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(
path, deepPools, "maximumFluidY", -2048, 2048, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(
path, deepPools, "searchRadius", 0, 256, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(
path, deepPools, "searchAttempts", 1, 64, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(
path, deepPools, "horizontalRadius", 2, 128, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(
path, deepPools, "verticalRadius", 2, 64, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(
path, deepPools, "dryHeadroom", 1, 63, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(
path, deepPools, "shapeVariation", 0D, 0.75D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(
path, deepPools, "warpStrength", 0D, 64D, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(
path, deepPools, "maximumVolume", 64, 1048576, errors);
validateNoiseChance(packFolder, deepPools, "reach", path, errors);
validateStyle(packFolder, deepPools, "shapeStyle", path, errors);
validateStyle(packFolder, deepPools, "warpStyle", path, errors);
validateFluidPalette(path, deepPools, errors);
int minimumFluidY = integerValue(deepPools, "minimumFluidY", -224);
int maximumFluidY = integerValue(deepPools, "maximumFluidY", -104);
int searchRadius = integerValue(deepPools, "searchRadius", 16);
int horizontalRadius = integerValue(deepPools, "horizontalRadius", 18);
int verticalRadius = integerValue(deepPools, "verticalRadius", 8);
int dryHeadroom = integerValue(deepPools, "dryHeadroom", 4);
int maximumVolume = integerValue(deepPools, "maximumVolume", 32768);
if (minimumFluidY > maximumFluidY) {
errors.add(path + ".minimumFluidY must not exceed maximumFluidY.");
}
if (dryHeadroom >= verticalRadius) {
errors.add(path + ".dryHeadroom must be smaller than verticalRadius.");
}
if (searchRadius + horizontalRadius > 128) {
errors.add(path + ".searchRadius plus horizontalRadius must not exceed 128 blocks.");
}
long minimumVolume = grottoVolume(horizontalRadius, verticalRadius);
if (minimumVolume > maximumVolume) {
errors.add(path + ".maximumVolume must be at least " + minimumVolume
+ " to contain the base deep-pool chamber.");
}
if (!booleanValue(deepPools, "enabled", false)) {
return;
}
JSONObject dimensionHeight = dimension.optJSONObject("dimensionHeight");
int minimumHeight = dimensionHeight == null ? -64 : integerValue(dimensionHeight, "min", -64);
int maximumHeight = dimensionHeight == null ? 320 : integerValue(dimensionHeight, "max", 320);
int lowestBoundaryY = minimumFluidY - (verticalRadius * 2 - dryHeadroom) - 1;
int highestBoundaryY = maximumFluidY + dryHeadroom + 1;
if (lowestBoundaryY <= minimumHeight || highestBoundaryY >= maximumHeight) {
errors.add(path + " fluid range and chamber envelope must remain inside dimensionHeight.");
}
int maximumPerReach = integerValue(deepPools, "maximumPerReach", 1);
double reachChance = noiseChanceValue(deepPools, "reach", 1D / 3D);
if (maximumPerReach == 0 || reachChance == 0D) {
warnings.add(path + " is enabled but its reach gate cannot accept any pools.");
}
}
private static void validateGrotto(String path, JSONObject caves, List<String> errors) { private static void validateGrotto(String path, JSONObject caves, List<String> errors) {
int throatRadius = integerValue(caves, "throatRadius", 2); int throatRadius = integerValue(caves, "throatRadius", 2);
int dryHeadroom = integerValue(caves, "dryHeadroom", 4); int dryHeadroom = integerValue(caves, "dryHeadroom", 4);
@@ -65,7 +65,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
private static final AtomicInteger BOOST_HOLDERS = new AtomicInteger(); private static final AtomicInteger BOOST_HOLDERS = new AtomicInteger();
private static final int ADAPTIVE_SLOW_REQUEST_STEP = 3; private static final int ADAPTIVE_SLOW_REQUEST_STEP = 3;
private static final int ADAPTIVE_RECOVERY_INTERVAL = 8; private static final int ADAPTIVE_RECOVERY_INTERVAL = 8;
private static final long CLOSE_DRAIN_TIMEOUT_SECONDS = 60L; private static final long CLOSE_DRAIN_WARNING_SECONDS = 60L;
private static final long FLUSH_TIMEOUT_SECONDS = 120L; private static final long FLUSH_TIMEOUT_SECONDS = 120L;
private final World world; private final World world;
private final IrisRuntimeSchedulerMode runtimeSchedulerMode; private final IrisRuntimeSchedulerMode runtimeSchedulerMode;
@@ -773,17 +773,13 @@ public class AsyncPregenMethod implements PregeneratorMethod {
// A stop request interrupts the pregen worker; shield the drain and flush so chunks still hit disk. // A stop request interrupts the pregen worker; shield the drain and flush so chunks still hit disk.
boolean interrupted = Thread.interrupted(); boolean interrupted = Thread.interrupted();
try { try {
boolean drained = false; interrupted |= awaitDrain(
try { semaphore,
drained = semaphore.tryAcquire(threads, CLOSE_DRAIN_TIMEOUT_SECONDS, TimeUnit.SECONDS); threads,
} catch (InterruptedException e) { CLOSE_DRAIN_WARNING_SECONDS,
interrupted = true; TimeUnit.SECONDS,
} () -> IrisLogging.warn("Async pregen is still draining outstanding chunks. " + metricsSnapshot())
);
if (!drained) {
IrisLogging.warn("Async pregen close did not drain in " + CLOSE_DRAIN_TIMEOUT_SECONDS
+ "s, continuing degraded. " + metricsSnapshot());
}
flushAllRemainingChunks(); flushAllRemainingChunks();
executor.shutdown(); executor.shutdown();
@@ -797,6 +793,26 @@ public class AsyncPregenMethod implements PregeneratorMethod {
} }
} }
static boolean awaitDrain(
Semaphore semaphore,
int permits,
long warningInterval,
TimeUnit timeUnit,
Runnable onWait
) {
boolean interrupted = false;
while (true) {
try {
if (semaphore.tryAcquire(permits, warningInterval, timeUnit)) {
return interrupted;
}
onWait.run();
} catch (InterruptedException e) {
interrupted = true;
}
}
}
private boolean isCancelled() { private boolean isCancelled() {
return closing.get() || Thread.currentThread().isInterrupted(); return closing.get() || Thread.currentThread().isInterrupted();
} }
@@ -84,19 +84,35 @@ public class IrisProject {
long seed, long seed,
StudioOpenCoordinator.StudioOpenKind openKind, StudioOpenCoordinator.StudioOpenKind openKind,
Consumer<World> onDone Consumer<World> onDone
) throws IrisException {
return open(sender, seed, openKind, onDone, System.nanoTime());
}
public CompletableFuture<StudioOpenCoordinator.StudioOpenResult> open(
VolmitSender sender,
long seed,
StudioOpenCoordinator.StudioOpenKind openKind,
Consumer<World> onDone,
long requestedAtNanos
) throws IrisException { ) throws IrisException {
if (isOpen()) { if (isOpen()) {
return close().thenCompose(ignored -> openInternal(sender, seed, openKind, onDone)); return close().thenCompose(ignored -> openInternal(
sender,
seed,
openKind,
onDone,
requestedAtNanos));
} }
return openInternal(sender, seed, openKind, onDone); return openInternal(sender, seed, openKind, onDone, requestedAtNanos);
} }
private CompletableFuture<StudioOpenCoordinator.StudioOpenResult> openInternal( private CompletableFuture<StudioOpenCoordinator.StudioOpenResult> openInternal(
VolmitSender sender, VolmitSender sender,
long seed, long seed,
StudioOpenCoordinator.StudioOpenKind openKind, StudioOpenCoordinator.StudioOpenKind openKind,
Consumer<World> onDone Consumer<World> onDone,
long requestedAtNanos
) { ) {
AtomicReference<String> stage = new AtomicReference<>("Queued"); AtomicReference<String> stage = new AtomicReference<>("Queued");
AtomicReference<Double> progress = new AtomicReference<>(0.01D); AtomicReference<Double> progress = new AtomicReference<>(0.01D);
@@ -114,7 +130,8 @@ public class IrisProject {
} }
progress.set(Math.max(0D, Math.min(0.99D, update.progress()))); progress.set(Math.max(0D, Math.min(0.99D, update.progress())));
}, },
onDone onDone,
requestedAtNanos
) )
); );
StudioOpenProgressReporter.startStudioOpenReporter(sender, stage, progress, complete, failed); StudioOpenProgressReporter.startStudioOpenReporter(sender, stage, progress, complete, failed);
@@ -14,6 +14,7 @@ import art.arcane.iris.core.project.IrisProject;
import art.arcane.iris.core.project.IrisCodeWorkspace; import art.arcane.iris.core.project.IrisCodeWorkspace;
import art.arcane.iris.core.tools.IrisCreator; import art.arcane.iris.core.tools.IrisCreator;
import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.platform.BukkitChunkGenerator; import art.arcane.iris.engine.platform.BukkitChunkGenerator;
import art.arcane.iris.engine.platform.PlatformChunkGenerator; import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.util.common.plugin.VolmitSender; import art.arcane.iris.util.common.plugin.VolmitSender;
@@ -98,16 +99,11 @@ public final class StudioOpenCoordinator {
public CompletableFuture<Boolean> teleportPlayerToProject( public CompletableFuture<Boolean> teleportPlayerToProject(
IrisProject project, IrisProject project,
Player player, Player player
AtomicBoolean admission,
long deadlineNanos
) { ) {
if (project == null || player == null) { if (project == null || player == null) {
return CompletableFuture.completedFuture(false); return CompletableFuture.completedFuture(false);
} }
AtomicBoolean activeAdmission = Objects.requireNonNull(
admission,
"Studio teleport admission");
PlatformChunkGenerator provider = project.getActiveProvider(); PlatformChunkGenerator provider = project.getActiveProvider();
if (provider == null) { if (provider == null) {
return CompletableFuture.failedFuture(new IllegalStateException( return CompletableFuture.failedFuture(new IllegalStateException(
@@ -123,10 +119,6 @@ public final class StudioOpenCoordinator {
return CompletableFuture.failedFuture(new IllegalStateException( return CompletableFuture.failedFuture(new IllegalStateException(
"Studio entry point could not be resolved.")); "Studio entry point could not be resolved."));
} }
if (System.nanoTime() >= deadlineNanos
|| !activeAdmission.compareAndSet(true, false)) {
return studioTeleportDeadlineFailure("native teleport delegation");
}
CompletableFuture<Boolean> teleport = project.getActiveOpenKind() == StudioOpenKind.STANDARD CompletableFuture<Boolean> teleport = project.getActiveOpenKind() == StudioOpenKind.STANDARD
? WorldRuntimeControlService.get().teleportInMode(player, entry, GameMode.SPECTATOR) ? WorldRuntimeControlService.get().teleportInMode(player, entry, GameMode.SPECTATOR)
: WorldRuntimeControlService.get().teleport(player, entry); : WorldRuntimeControlService.get().teleport(player, entry);
@@ -134,25 +126,12 @@ public final class StudioOpenCoordinator {
return CompletableFuture.failedFuture(new IllegalStateException( return CompletableFuture.failedFuture(new IllegalStateException(
"Studio native teleport returned no completion future.")); "Studio native teleport returned no completion future."));
} }
long remainingNanos = deadlineNanos - System.nanoTime();
if (remainingNanos <= 0L) {
teleport.completeExceptionally(new TimeoutException(
"Studio teleport deadline expired before native teleport completion."));
return teleport;
}
teleport.orTimeout(remainingNanos, TimeUnit.NANOSECONDS);
return teleport; return teleport;
} }
private <T> CompletableFuture<T> studioTeleportDeadlineFailure(String stageName) {
return CompletableFuture.failedFuture(new TimeoutException(
"Studio teleport deadline expired before " + stageName + "."));
}
private void executeOpen(StudioOpenRequest request, CompletableFuture<StudioOpenResult> future) { private void executeOpen(StudioOpenRequest request, CompletableFuture<StudioOpenResult> future) {
World world = null; World world = null;
PlatformChunkGenerator provider = null; PlatformChunkGenerator provider = null;
CompletableFuture<Boolean> nativeTeleportFuture = null;
try { try {
long openStart = System.nanoTime(); long openStart = System.nanoTime();
long t = openStart; long t = openStart;
@@ -181,6 +160,10 @@ public final class StudioOpenCoordinator {
if (provider == null) { if (provider == null) {
throw new IllegalStateException("Studio runtime provider is unavailable for world \"" + request.worldName() + "\"."); throw new IllegalStateException("Studio runtime provider is unavailable for world \"" + request.worldName() + "\".");
} }
World entryWorld = world;
PlatformChunkGenerator entryProvider = provider;
CompletableFuture<Void> entryBootstrap = J.afut(
() -> endStudioEntryBootstrap(entryWorld, entryProvider));
updateStage(request, "apply_world_rules", 0.72D); updateStage(request, "apply_world_rules", 0.72D);
final World rulesWorld = world; final World rulesWorld = world;
@@ -204,9 +187,15 @@ public final class StudioOpenCoordinator {
t = logStudioPhase(request, "resolve_entry_anchor", t, openStart); t = logStudioPhase(request, "resolve_entry_anchor", t, openStart);
updateStage(request, "prepare_structure_rings", 0.79D); updateStage(request, "prepare_structure_rings", 0.79D);
endStudioEntryBootstrap(world, provider); entryBootstrap.get(STUDIO_STRUCTURE_ACTIVATION_TIMEOUT_SECONDS, TimeUnit.SECONDS);
t = logStudioPhase(request, "prepare_structure_rings", t, openStart); t = logStudioPhase(request, "prepare_structure_rings", t, openStart);
updateStage(request, "prepare_generation_caches", 0.88D);
if (entryProvider.getEngine() instanceof IrisEngine irisEngine) {
irisEngine.awaitGenerationCacheWarm();
}
t = logStudioPhase(request, "prepare_generation_caches", t, openStart);
Location entryLocation = entryAnchor; Location entryLocation = entryAnchor;
if (request.openKind().teleportThroughStandardEntry() if (request.openKind().teleportThroughStandardEntry()
@@ -219,7 +208,7 @@ public final class StudioOpenCoordinator {
} }
Boolean teleported; Boolean teleported;
try { try {
nativeTeleportFuture = WorldRuntimeControlService.get().teleportInMode( CompletableFuture<Boolean> nativeTeleportFuture = WorldRuntimeControlService.get().teleportInMode(
player, player,
entryLocation, entryLocation,
GameMode.SPECTATOR); GameMode.SPECTATOR);
@@ -227,15 +216,20 @@ public final class StudioOpenCoordinator {
throw new IllegalStateException( throw new IllegalStateException(
"Studio native teleport returned no completion future."); "Studio native teleport returned no completion future.");
} }
teleported = nativeTeleportFuture.get(60L, TimeUnit.SECONDS); teleported = nativeTeleportFuture.get();
} catch (TimeoutException e) { } catch (ExecutionException e) {
nativeTeleportFuture.completeExceptionally(e); Throwable failure = unwrapFailure(e);
throw new IllegalStateException("Studio teleport timed out — destination region may still be generating."); throw new IllegalStateException("Studio teleport failed.", failure);
} }
if (!Boolean.TRUE.equals(teleported)) { if (!Boolean.TRUE.equals(teleported)) {
throw new IllegalStateException("Studio teleport did not complete successfully."); throw new IllegalStateException("Studio teleport did not complete successfully.");
} }
t = logStudioPhase(request, "teleport_standard_entry", t, openStart); t = logStudioPhase(request, "teleport_standard_entry", t, openStart);
IrisLogging.info("Studio player %s arrived in %dms: dimension=%s world=%s",
player.getName(),
elapsedMillis(request.requestedAtNanos()),
request.dimensionKey(),
world.getName());
} }
updateStage(request, "finalize_open", 1.00D); updateStage(request, "finalize_open", 1.00D);
@@ -861,10 +855,14 @@ public final class StudioOpenCoordinator {
StudioOpenKind openKind, StudioOpenKind openKind,
boolean retainOnFailure, boolean retainOnFailure,
Consumer<StudioOpenProgress> progressConsumer, Consumer<StudioOpenProgress> progressConsumer,
Consumer<World> onDone Consumer<World> onDone,
long requestedAtNanos
) { ) {
public StudioOpenRequest { public StudioOpenRequest {
openKind = Objects.requireNonNull(openKind, "Studio open kind"); openKind = Objects.requireNonNull(openKind, "Studio open kind");
if (requestedAtNanos <= 0L) {
throw new IllegalArgumentException("Studio request time must be positive.");
}
} }
public static StudioOpenRequest studioProject( public static StudioOpenRequest studioProject(
@@ -874,6 +872,25 @@ public final class StudioOpenCoordinator {
StudioOpenKind openKind, StudioOpenKind openKind,
Consumer<StudioOpenProgress> progressConsumer, Consumer<StudioOpenProgress> progressConsumer,
Consumer<World> onDone Consumer<World> onDone
) {
return studioProject(
project,
sender,
seed,
openKind,
progressConsumer,
onDone,
System.nanoTime());
}
public static StudioOpenRequest studioProject(
IrisProject project,
VolmitSender sender,
long seed,
StudioOpenKind openKind,
Consumer<StudioOpenProgress> progressConsumer,
Consumer<World> onDone,
long requestedAtNanos
) { ) {
String playerName = sender != null && sender.isPlayer() && sender.player() != null ? sender.player().getName() : null; String playerName = sender != null && sender.isPlayer() && sender.player() != null ? sender.player().getName() : null;
return new StudioOpenRequest( return new StudioOpenRequest(
@@ -886,7 +903,8 @@ public final class StudioOpenCoordinator {
openKind, openKind,
false, false,
progressConsumer, progressConsumer,
onDone onDone,
requestedAtNanos
); );
} }
} }
@@ -10,6 +10,7 @@ import art.arcane.iris.core.service.BoardSVC;
import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.platform.BukkitChunkGenerator; import art.arcane.iris.engine.platform.BukkitChunkGenerator;
import art.arcane.iris.engine.platform.PlatformChunkGenerator; import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.util.common.scheduling.J; import art.arcane.iris.util.common.scheduling.J;
import io.papermc.lib.PaperLib; import io.papermc.lib.PaperLib;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
@@ -41,10 +42,13 @@ import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
public final class WorldRuntimeControlService { public final class WorldRuntimeControlService {
private static final int STUDIO_ENTRY_VIEW_DISTANCE = 2;
private static final int MAX_SAFE_ENTRY_HORIZONTAL_RADIUS = 15; private static final int MAX_SAFE_ENTRY_HORIZONTAL_RADIUS = 15;
private static final int MAX_SAFE_ENTRY_VERTICAL_SEARCH = 64; private static final int SAFE_ENTRY_UPWARD_ALLOWANCE = 32;
private static final double BLOCK_CENTER = 0.5D; private static final double BLOCK_CENTER = 0.5D;
private static final double COLLISION_EPSILON = 0.000001D; private static final double COLLISION_EPSILON = 0.000001D;
private static final Method PLAYER_GET_VIEW_DISTANCE_METHOD = findPlayerMethod("getViewDistance");
private static final Method PLAYER_SET_VIEW_DISTANCE_METHOD = findPlayerMethod("setViewDistance", int.class);
private static final Set<Material> UNSAFE_ENTRY_MATERIALS = Set.of( private static final Set<Material> UNSAFE_ENTRY_MATERIALS = Set.of(
Material.CACTUS, Material.CACTUS,
Material.CAMPFIRE, Material.CAMPFIRE,
@@ -302,7 +306,7 @@ public final class WorldRuntimeControlService {
CompletableFuture<Location> future = new CompletableFuture<>(); CompletableFuture<Location> future = new CompletableFuture<>();
boolean scheduled = J.runRegion(world, chunkX, chunkZ, () -> { boolean scheduled = J.runRegion(world, chunkX, chunkZ, () -> {
try { try {
future.complete(findTopSafeLocation(world, source)); future.complete(findTopSafeLocationWithTicket(world, source, chunkX, chunkZ));
} catch (Throwable t) { } catch (Throwable t) {
future.completeExceptionally(t); future.completeExceptionally(t);
} }
@@ -314,6 +318,22 @@ public final class WorldRuntimeControlService {
return future; return future;
} }
private static Location findTopSafeLocationWithTicket(
World world,
Location source,
int chunkX,
int chunkZ
) {
boolean ticketAdded = world.addPluginChunkTicket(chunkX, chunkZ, BukkitPlatform.plugin());
try {
return findTopSafeLocation(world, source);
} finally {
if (ticketAdded) {
world.removePluginChunkTicket(chunkX, chunkZ, BukkitPlatform.plugin());
}
}
}
public CompletableFuture<Boolean> teleport(Player player, Location location) { public CompletableFuture<Boolean> teleport(Player player, Location location) {
return scheduleTeleport(player, location, null); return scheduleTeleport(player, location, null);
} }
@@ -350,6 +370,7 @@ public final class WorldRuntimeControlService {
CompletableFuture<Boolean> future = new CompletableFuture<>(); CompletableFuture<Boolean> future = new CompletableFuture<>();
GameModeRestore modeRestore = new GameModeRestore(player); GameModeRestore modeRestore = new GameModeRestore(player);
PlayerViewDistanceRestore viewDistanceRestore = new PlayerViewDistanceRestore(player);
AtomicReference<CompletableFuture<Boolean>> activeTeleport = new AtomicReference<>(); AtomicReference<CompletableFuture<Boolean>> activeTeleport = new AtomicReference<>();
future.whenComplete((success, failure) -> { future.whenComplete((success, failure) -> {
if (Boolean.TRUE.equals(success)) { if (Boolean.TRUE.equals(success)) {
@@ -359,6 +380,7 @@ public final class WorldRuntimeControlService {
if (teleport != null && !teleport.isDone()) { if (teleport != null && !teleport.isDone()) {
teleport.cancel(false); teleport.cancel(false);
} }
viewDistanceRestore.restore();
modeRestore.restore(); modeRestore.restore();
}); });
boolean scheduled = J.runEntity(player, () -> { boolean scheduled = J.runEntity(player, () -> {
@@ -368,7 +390,9 @@ public final class WorldRuntimeControlService {
} }
if (gameMode != null) { if (gameMode != null) {
modeRestore.apply(gameMode); modeRestore.apply(gameMode);
viewDistanceRestore.apply();
if (future.isDone()) { if (future.isDone()) {
viewDistanceRestore.restore();
modeRestore.restore(); modeRestore.restore();
return; return;
} }
@@ -393,6 +417,7 @@ public final class WorldRuntimeControlService {
if (Boolean.TRUE.equals(success)) { if (Boolean.TRUE.equals(success)) {
if (future.complete(true)) { if (future.complete(true)) {
viewDistanceRestore.restore();
J.runEntity(player, () -> IrisServices.get(BoardSVC.class).updatePlayer(player)); J.runEntity(player, () -> IrisServices.get(BoardSVC.class).updatePlayer(player));
} }
return; return;
@@ -479,6 +504,7 @@ public final class WorldRuntimeControlService {
z, z,
minimumFloorY, minimumFloorY,
maximumFloorY, maximumFloorY,
source.getBlockY() - 1,
yaw, yaw,
pitch pitch
); );
@@ -498,13 +524,14 @@ public final class WorldRuntimeControlService {
int z, int z,
int minimumFloorY, int minimumFloorY,
int maximumFloorY, int maximumFloorY,
int preferredFloorY,
float yaw, float yaw,
float pitch float pitch
) { ) {
int highestY = world.getHighestBlockYAt(x, z, HeightMap.MOTION_BLOCKING_NO_LEAVES); int highestY = world.getHighestBlockYAt(x, z, HeightMap.MOTION_BLOCKING_NO_LEAVES);
int startingFloorY = Math.max(minimumFloorY, Math.min(maximumFloorY, highestY)); int preferredMaximumY = Math.min(maximumFloorY, preferredFloorY + SAFE_ENTRY_UPWARD_ALLOWANCE);
int lowestFloorY = Math.max(minimumFloorY, startingFloorY - MAX_SAFE_ENTRY_VERTICAL_SEARCH + 1); int startingFloorY = Math.max(minimumFloorY, Math.min(preferredMaximumY, highestY));
for (int floorY = startingFloorY; floorY >= lowestFloorY; floorY--) { for (int floorY = startingFloorY; floorY >= minimumFloorY; floorY--) {
Block floor = world.getBlockAt(x, floorY, z); Block floor = world.getBlockAt(x, floorY, z);
if (!isSafeFloor(floor)) { if (!isSafeFloor(floor)) {
continue; continue;
@@ -777,6 +804,14 @@ public final class WorldRuntimeControlService {
return method.invoke(instance); return method.invoke(instance);
} }
private static Method findPlayerMethod(String methodName, Class<?>... parameterTypes) {
try {
return Player.class.getMethod(methodName, parameterTypes);
} catch (NoSuchMethodException ignored) {
return null;
}
}
@FunctionalInterface @FunctionalInterface
interface TeleportExecutor { interface TeleportExecutor {
CompletableFuture<Boolean> teleport(Player player, Location location); CompletableFuture<Boolean> teleport(Player player, Location location);
@@ -822,4 +857,56 @@ public final class WorldRuntimeControlService {
} }
} }
} }
private static final class PlayerViewDistanceRestore {
private final Player player;
private final AtomicBoolean changed;
private final AtomicBoolean restored;
private int previousViewDistance;
private PlayerViewDistanceRestore(Player player) {
this.player = player;
changed = new AtomicBoolean(false);
restored = new AtomicBoolean(false);
}
private void apply() {
if (PLAYER_GET_VIEW_DISTANCE_METHOD == null || PLAYER_SET_VIEW_DISTANCE_METHOD == null) {
return;
}
try {
Object currentValue = PLAYER_GET_VIEW_DISTANCE_METHOD.invoke(player);
if (!(currentValue instanceof Number)) {
return;
}
int currentViewDistance = ((Number) currentValue).intValue();
if (currentViewDistance <= STUDIO_ENTRY_VIEW_DISTANCE) {
return;
}
PLAYER_SET_VIEW_DISTANCE_METHOD.invoke(player, STUDIO_ENTRY_VIEW_DISTANCE);
previousViewDistance = currentViewDistance;
changed.set(true);
} catch (ReflectiveOperationException failure) {
IrisLogging.reportError("Failed to apply a temporary player view distance for a studio teleport.", failure);
}
}
private void restore() {
if (!changed.get() || !restored.compareAndSet(false, true)) {
return;
}
Runnable restoration = () -> {
try {
PLAYER_SET_VIEW_DISTANCE_METHOD.invoke(player, previousViewDistance);
} catch (ReflectiveOperationException failure) {
IrisLogging.reportError("Failed to restore a player's view distance after a studio teleport.", failure);
}
};
if (!J.runEntity(player, restoration)) {
IrisLogging.reportError(
"Failed to restore a player's view distance after a studio teleport.",
new IllegalStateException("The player entity scheduler rejected the view-distance restoration."));
}
}
}
} }
@@ -83,7 +83,6 @@ import java.util.UUID;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@@ -95,7 +94,6 @@ import art.arcane.volmlib.util.localization.MessageArgument;
public class StudioSVC implements IrisService { public class StudioSVC implements IrisService {
public static final String WORKSPACE_NAME = "packs"; public static final String WORKSPACE_NAME = "packs";
private static final long DOWNLOAD_SHUTDOWN_POLL_SECONDS = 15L; private static final long DOWNLOAD_SHUTDOWN_POLL_SECONDS = 15L;
private static final long STUDIO_PLAYER_TELEPORT_TIMEOUT_SECONDS = 10L;
private static final Pattern PROJECT_NAME = Pattern.compile("[a-z0-9_-]+"); private static final Pattern PROJECT_NAME = Pattern.compile("[a-z0-9_-]+");
private static final AtomicCache<Integer> counter = new AtomicCache<>(); private static final AtomicCache<Integer> counter = new AtomicCache<>();
private final StudioTransitionQueue studioTransitions = new StudioTransitionQueue(); private final StudioTransitionQueue studioTransitions = new StudioTransitionQueue();
@@ -478,24 +476,14 @@ public class StudioSVC implements IrisService {
public CompletableFuture<Boolean> teleportToActiveProject(Player player) { public CompletableFuture<Boolean> teleportToActiveProject(Player player) {
Player target = Objects.requireNonNull(player, "Studio teleport player"); Player target = Objects.requireNonNull(player, "Studio teleport player");
AtomicBoolean admission = new AtomicBoolean(true); return studioTransitions.submit(() -> {
long deadlineNanos = System.nanoTime()
+ TimeUnit.SECONDS.toNanos(STUDIO_PLAYER_TELEPORT_TIMEOUT_SECONDS);
CompletableFuture<Boolean> transition = studioTransitions.submit(() -> {
IrisProject project = activeProject; IrisProject project = activeProject;
if (project == null || !project.isOpen()) { if (project == null || !project.isOpen()) {
return CompletableFuture.failedFuture(new IllegalStateException( return CompletableFuture.failedFuture(new IllegalStateException(
"No active Studio project is available for teleport.")); "No active Studio project is available for teleport."));
} }
return StudioOpenCoordinator.get().teleportPlayerToProject( return StudioOpenCoordinator.get().teleportPlayerToProject(project, target);
project,
target,
admission,
deadlineNanos);
}); });
transition.orTimeout(STUDIO_PLAYER_TELEPORT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
transition.whenComplete((ignored, failure) -> admission.set(false));
return transition;
} }
public void open(VolmitSender sender, String dimm) { public void open(VolmitSender sender, String dimm) {
@@ -565,13 +553,20 @@ public class StudioSVC implements IrisService {
StudioOpenCoordinator.StudioOpenKind openKind, StudioOpenCoordinator.StudioOpenKind openKind,
Consumer<World> onDone Consumer<World> onDone
) throws IrisException { ) throws IrisException {
long requestedAtNanos = System.nanoTime();
if (reportPackAdmissionFailure(sender, dimm) != null) { if (reportPackAdmissionFailure(sender, dimm) != null) {
return; return;
} }
StudioOpenCoordinator.StudioOpenKind requiredOpenKind = Objects.requireNonNull( StudioOpenCoordinator.StudioOpenKind requiredOpenKind = Objects.requireNonNull(
openKind, openKind,
"Studio open kind"); "Studio open kind");
studioTransitions.submit(() -> replaceActiveProject(sender, seed, dimm, requiredOpenKind, onDone)) studioTransitions.submit(() -> replaceActiveProject(
sender,
seed,
dimm,
requiredOpenKind,
onDone,
requestedAtNanos))
.whenComplete((ignored, throwable) -> { .whenComplete((ignored, throwable) -> {
if (throwable == null) { if (throwable == null) {
return; return;
@@ -591,6 +586,7 @@ public class StudioSVC implements IrisService {
Runnable beforeOpen, Runnable beforeOpen,
Consumer<World> onDone Consumer<World> onDone
) { ) {
long requestedAtNanos = System.nanoTime();
BrokenPackException failure = reportPackAdmissionFailure(sender, dimension); BrokenPackException failure = reportPackAdmissionFailure(sender, dimension);
if (failure != null) { if (failure != null) {
return CompletableFuture.failedFuture(failure); return CompletableFuture.failedFuture(failure);
@@ -601,7 +597,8 @@ public class StudioSVC implements IrisService {
dimension, dimension,
Objects.requireNonNull(openKind, "Studio open kind"), Objects.requireNonNull(openKind, "Studio open kind"),
Objects.requireNonNull(beforeOpen, "Studio before-open callback"), Objects.requireNonNull(beforeOpen, "Studio before-open callback"),
Objects.requireNonNull(onDone, "Studio open completion callback"))); Objects.requireNonNull(onDone, "Studio open completion callback"),
requestedAtNanos));
} }
private CompletableFuture<StudioOpenCoordinator.StudioOpenResult> replaceActiveProjectTracked( private CompletableFuture<StudioOpenCoordinator.StudioOpenResult> replaceActiveProjectTracked(
@@ -610,7 +607,8 @@ public class StudioSVC implements IrisService {
String dimension, String dimension,
StudioOpenCoordinator.StudioOpenKind openKind, StudioOpenCoordinator.StudioOpenKind openKind,
Runnable beforeOpen, Runnable beforeOpen,
Consumer<World> onDone Consumer<World> onDone,
long requestedAtNanos
) { ) {
return closeActiveProject().thenCompose(closeResult -> { return closeActiveProject().thenCompose(closeResult -> {
if (closeResult == null) { if (closeResult == null) {
@@ -621,7 +619,13 @@ public class StudioSVC implements IrisService {
return CompletableFuture.failedFuture(closeResult.failureCause()); return CompletableFuture.failedFuture(closeResult.failureCause());
} }
beforeOpen.run(); beforeOpen.run();
return beginStudioOpenTracked(sender, seed, dimension, openKind, onDone); return beginStudioOpenTracked(
sender,
seed,
dimension,
openKind,
onDone,
requestedAtNanos);
}); });
} }
@@ -630,13 +634,14 @@ public class StudioSVC implements IrisService {
long seed, long seed,
String dimension, String dimension,
StudioOpenCoordinator.StudioOpenKind openKind, StudioOpenCoordinator.StudioOpenKind openKind,
Consumer<World> onDone Consumer<World> onDone,
long requestedAtNanos
) { ) {
IrisProject project = new IrisProject(new File(getWorkspaceFolder(), dimension)); IrisProject project = new IrisProject(new File(getWorkspaceFolder(), dimension));
activeProject = project; activeProject = project;
CompletableFuture<StudioOpenCoordinator.StudioOpenResult> opening; CompletableFuture<StudioOpenCoordinator.StudioOpenResult> opening;
try { try {
opening = project.open(sender, seed, openKind, onDone); opening = project.open(sender, seed, openKind, onDone, requestedAtNanos);
} catch (IrisException exception) { } catch (IrisException exception) {
if (activeProject == project) { if (activeProject == project) {
activeProject = null; activeProject = null;
@@ -663,7 +668,8 @@ public class StudioSVC implements IrisService {
long seed, long seed,
String dimension, String dimension,
StudioOpenCoordinator.StudioOpenKind openKind, StudioOpenCoordinator.StudioOpenKind openKind,
Consumer<World> onDone Consumer<World> onDone,
long requestedAtNanos
) { ) {
return closeActiveProjectForReplacement(sender).handle((closeResult, closeThrowable) -> { return closeActiveProjectForReplacement(sender).handle((closeResult, closeThrowable) -> {
if (closeThrowable != null) { if (closeThrowable != null) {
@@ -691,7 +697,13 @@ public class StudioSVC implements IrisService {
} }
return true; return true;
}).thenCompose(closed -> closed }).thenCompose(closed -> closed
? beginStudioOpen(sender, seed, dimension, openKind, onDone) ? beginStudioOpen(
sender,
seed,
dimension,
openKind,
onDone,
requestedAtNanos)
: CompletableFuture.completedFuture(null)); : CompletableFuture.completedFuture(null));
} }
@@ -700,7 +712,8 @@ public class StudioSVC implements IrisService {
long seed, long seed,
String dimension, String dimension,
StudioOpenCoordinator.StudioOpenKind openKind, StudioOpenCoordinator.StudioOpenKind openKind,
Consumer<World> onDone Consumer<World> onDone,
long requestedAtNanos
) { ) {
IrisProject project = new IrisProject(new File(getWorkspaceFolder(), dimension)); IrisProject project = new IrisProject(new File(getWorkspaceFolder(), dimension));
activeProject = project; activeProject = project;
@@ -710,7 +723,8 @@ public class StudioSVC implements IrisService {
sender, sender,
seed, seed,
openKind, openKind,
onDone); onDone,
requestedAtNanos);
} catch (IrisException e) { } catch (IrisException e) {
if (activeProject == project) { if (activeProject == project) {
activeProject = null; activeProject = null;
@@ -306,6 +306,9 @@ public class IrisCreator {
world = J.sfut(() -> INMS.get().createWorldAsync(wc, request)) world = J.sfut(() -> INMS.get().createWorldAsync(wc, request))
.thenCompose(Function.identity()) .thenCompose(Function.identity())
.get(WORLD_CREATE_TIMEOUT_SECONDS, TimeUnit.SECONDS); .get(WORLD_CREATE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
if (!studio && !benchmark) {
awaitInitialSpawnPreparation(access, name);
}
} catch (Throwable e) { } catch (Throwable e) {
done.set(true); done.set(true);
cancelRepeatingTask(createProgressTask); cancelRepeatingTask(createProgressTask);
@@ -603,6 +606,21 @@ public class IrisCreator {
return taskId; return taskId;
} }
static void awaitInitialSpawnPreparation(
PlatformChunkGenerator generator,
String worldName
) throws InterruptedException, ExecutionException, TimeoutException {
CompletableFuture<Void> initialSpawnReady = Objects.requireNonNull(
generator.getInitialSpawnReady(),
"Initial spawn preparation future");
try {
initialSpawnReady.get(WORLD_CREATE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
} catch (TimeoutException failure) {
throw new TimeoutException("Initial spawn preparation timed out for world \""
+ worldName + "\".");
}
}
private AtomicInteger startPregenProgressReporter( private AtomicInteger startPregenProgressReporter(
AtomicDouble progress, AtomicDouble progress,
AtomicBoolean done, AtomicBoolean done,
@@ -29,10 +29,14 @@ import art.arcane.iris.engine.object.IrisDecorationPart;
import art.arcane.iris.engine.object.IrisDecorator; import art.arcane.iris.engine.object.IrisDecorator;
import art.arcane.iris.engine.object.IrisGenerator; import art.arcane.iris.engine.object.IrisGenerator;
import art.arcane.iris.engine.object.IrisInterpolator; import art.arcane.iris.engine.object.IrisInterpolator;
import art.arcane.iris.engine.object.IrisMaterialPalette;
import art.arcane.iris.engine.object.IrisRegion; import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisRiverCaves;
import art.arcane.iris.engine.object.IrisRiverDeepPools;
import art.arcane.iris.engine.object.IrisRiverNetwork;
import art.arcane.iris.engine.object.IrisRiverOverride; import art.arcane.iris.engine.object.IrisRiverOverride;
import art.arcane.iris.engine.object.IrisRiverRoutingPolicy; import art.arcane.iris.engine.object.IrisRiverRoutingPolicy;
import art.arcane.iris.engine.object.IrisRiverWaterMode; import art.arcane.iris.engine.object.IrisRiverWater;
import art.arcane.iris.engine.object.IrisShapedGeneratorStyle; import art.arcane.iris.engine.object.IrisShapedGeneratorStyle;
import art.arcane.iris.engine.river.runtime.IrisRiverRuntime; import art.arcane.iris.engine.river.runtime.IrisRiverRuntime;
import art.arcane.iris.engine.river.runtime.IrisRiverRuntimeContext; import art.arcane.iris.engine.river.runtime.IrisRiverRuntimeContext;
@@ -40,6 +44,7 @@ import art.arcane.iris.engine.river.runtime.IrisRiverSurfaceSample;
import art.arcane.iris.engine.river.RiverRouteState; import art.arcane.iris.engine.river.RiverRouteState;
import art.arcane.iris.engine.river.RiverSample; import art.arcane.iris.engine.river.RiverSample;
import art.arcane.iris.engine.river.RiverSection; import art.arcane.iris.engine.river.RiverSection;
import art.arcane.iris.engine.river.cave.RiverCaveFluidKind;
import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.PlatformBiome; import art.arcane.iris.spi.PlatformBiome;
@@ -66,6 +71,7 @@ import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.IdentityHashMap; import java.util.IdentityHashMap;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
@@ -132,6 +138,8 @@ public class IrisComplex implements DataProvider {
private ProceduralStream<IrisDecorator> shoreSurfaceDecoration; private ProceduralStream<IrisDecorator> shoreSurfaceDecoration;
private ProceduralStream<PlatformBlockState> rockStream; private ProceduralStream<PlatformBlockState> rockStream;
private ProceduralStream<PlatformBlockState> fluidStream; private ProceduralStream<PlatformBlockState> fluidStream;
private ProceduralStream<PlatformBlockState> riverFluidStream;
private ProceduralStream<PlatformBlockState> riverDeepPoolFluidStream;
private IrisBiome focusBiome; private IrisBiome focusBiome;
private IrisRegion focusRegion; private IrisRegion focusRegion;
private Map<IrisInterpolator, IdentityHashMap<IrisBiome, GeneratorBounds>> generatorBounds; private Map<IrisInterpolator, IdentityHashMap<IrisBiome, GeneratorBounds>> generatorBounds;
@@ -213,6 +221,29 @@ public class IrisComplex implements DataProvider {
.select(engine.getDimension().getRockPalette().getBlockData(data)); .select(engine.getDimension().getRockPalette().getBlockData(data));
fluidStream = engine.getDimension().getFluidPalette().getLayerGenerator(rng.nextParallelRNG(78), data).stream() fluidStream = engine.getDimension().getFluidPalette().getLayerGenerator(rng.nextParallelRNG(78), data).stream()
.select(engine.getDimension().getFluidPalette().getBlockData(data)); .select(engine.getDimension().getFluidPalette().getBlockData(data));
IrisRiverNetwork configuredRivers = engine.getDimension().getRivers();
IrisRiverWater configuredRiverWater = configuredRivers == null ? null : configuredRivers.getWater();
riverFluidStream = configuredRivers != null && configuredRivers.isEnabled()
? configuredFluidStream(
Objects.requireNonNull(configuredRiverWater).getFluidPalette(),
rng.nextParallelRNG(79),
"River water"
)
: fluidStream;
IrisRiverCaves configuredRiverCaves = configuredRivers == null ? null : configuredRivers.getCaves();
IrisRiverDeepPools configuredDeepPools = configuredRiverCaves == null
? null
: configuredRiverCaves.getDeepPools();
riverDeepPoolFluidStream = configuredRivers != null
&& configuredRivers.isEnabled()
&& configuredDeepPools != null
&& configuredDeepPools.isEnabled()
? configuredFluidStream(
configuredDeepPools.getFluidPalette(),
rng.nextParallelRNG(80),
"River deep-pool"
)
: riverFluidStream;
regionStyleStream = engine.getDimension().getRegionStyle().create(rng.nextParallelRNG(883), getData()).stream() regionStyleStream = engine.getDimension().getRegionStyle().create(rng.nextParallelRNG(883), getData()).stream()
.zoom(engine.getDimension().getRegionZoom()); .zoom(engine.getDimension().getRegionZoom());
regionIdentityStream = regionStyleStream.fit(Integer.MIN_VALUE, Integer.MAX_VALUE); regionIdentityStream = regionStyleStream.fit(Integer.MIN_VALUE, Integer.MAX_VALUE);
@@ -303,16 +334,16 @@ public class IrisComplex implements DataProvider {
.cache2D("naturalTrueBiomeStream", engine, cacheSize); .cache2D("naturalTrueBiomeStream", engine, cacheSize);
if (engine.getDimension().getRivers() != null && engine.getDimension().getRivers().isEnabled()) { if (engine.getDimension().getRivers() != null && engine.getDimension().getRivers().isEnabled()) {
ProceduralStream<Boolean> naturalOceanStream = createNaturalOceanStream( ProceduralStream<Boolean> naturalOceanStream = createNaturalOceanStream(
naturalHeightStream,
bridgeStream, bridgeStream,
focusBiome, focusBiome
fluidHeight,
engine.getDimension().getRivers().getWater().getMode()
).cache2D("naturalOceanStream", engine, cacheSize); ).cache2D("naturalOceanStream", engine, cacheSize);
int riverFluidHeight = engine.getDimension().getRivers().getWater().getFluidHeight()
- engine.getDimension().getMinHeight();
riverRuntime = new IrisRiverRuntime(new IrisRiverRuntimeContext( riverRuntime = new IrisRiverRuntime(new IrisRiverRuntimeContext(
engine.getSeedManager().getBodies(), engine.getSeedManager().getBodies(),
engine.getDimension().getRivers(), engine.getDimension().getRivers(),
data, data,
riverFluidHeight,
(int) Math.round(fluidHeight), (int) Math.round(fluidHeight),
IrisEngineMantle.isRiverHydrologyEnabled(engine.getDimension()), IrisEngineMantle.isRiverHydrologyEnabled(engine.getDimension()),
IrisEngineMantle.isRiverCaveHydrologyEnabled(engine.getDimension()), IrisEngineMantle.isRiverCaveHydrologyEnabled(engine.getDimension()),
@@ -458,23 +489,49 @@ public class IrisComplex implements DataProvider {
} }
static ProceduralStream<Boolean> createNaturalOceanStream( static ProceduralStream<Boolean> createNaturalOceanStream(
ProceduralStream<Double> naturalHeightStream,
ProceduralStream<InferredType> bridgeStream, ProceduralStream<InferredType> bridgeStream,
IrisBiome focusBiome, IrisBiome focusBiome
double fluidHeight,
IrisRiverWaterMode waterMode
) { ) {
if (focusBiome != null) { if (focusBiome != null) {
boolean ocean = focusBiome.getInferredType() == InferredType.SEA; boolean ocean = focusBiome.getInferredType() == InferredType.SEA;
return ProceduralStream.of((x, z) -> ocean, Interpolated.BOOLEAN); return ProceduralStream.of((x, z) -> ocean, Interpolated.BOOLEAN);
} }
if (waterMode == IrisRiverWaterMode.SEA_LEVEL) { return bridgeStream.convert(type -> type == InferredType.SEA);
return bridgeStream.convert(type -> type == InferredType.SEA); }
private ProceduralStream<PlatformBlockState> configuredFluidStream(
IrisMaterialPalette palette,
RNG fluidRng,
String configurationName
) {
Objects.requireNonNull(palette, configurationName + " fluidPalette must be configured");
KList<PlatformBlockState> blocks = palette.getBlockData(data);
if (blocks.isEmpty()) {
throw new IllegalArgumentException(
configurationName + " fluidPalette must resolve at least one fluid block");
} }
return ProceduralStream.of( for (PlatformBlockState block : blocks) {
(x, z) -> naturalHeightStream.getDouble(x, z) < fluidHeight - 1D, if (block == null || !block.isFluid()) {
Interpolated.BOOLEAN throw new IllegalArgumentException(
); configurationName + " fluidPalette may contain only fluid blocks");
}
}
return palette.getLayerGenerator(fluidRng, data).stream().select(blocks);
}
public PlatformBlockState resolveRiverCaveFluid(RiverCaveFluidKind fluidKind, double x, double z) {
return switch (Objects.requireNonNull(fluidKind)) {
case RIVER -> riverFluidStream.get(x, z);
case DEEP_POOL -> riverDeepPoolFluidStream.get(x, z);
};
}
public PlatformBlockState resolveSurfaceFluid(double x, double z) {
IrisRiverSurfaceSample sample = riverSurfaceStream.get(x, z);
if (sample.river().present() && sample.surfaceFluid()) {
return riverFluidStream.get(x, z);
}
return fluidStream.get(x, z);
} }
public ProceduralStream<IrisBiome> getBiomeStream(InferredType type) { public ProceduralStream<IrisBiome> getBiomeStream(InferredType type) {
@@ -71,6 +71,7 @@ import java.util.Locale;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
@@ -93,6 +94,7 @@ public class IrisEngine implements Engine {
private final ChronoLatch perSecondLatch; private final ChronoLatch perSecondLatch;
private final ChronoLatch perSecondBudLatch; private final ChronoLatch perSecondBudLatch;
private final EngineMetrics metrics; private final EngineMetrics metrics;
private final CompletableFuture<Void> generationCacheWarm;
private final boolean studio; private final boolean studio;
private final AtomicRollingSequence wallClock; private final AtomicRollingSequence wallClock;
@Getter(AccessLevel.NONE) @Getter(AccessLevel.NONE)
@@ -186,6 +188,7 @@ public class IrisEngine implements Engine {
bud = new AtomicInteger(0); bud = new AtomicInteger(0);
buds = new AtomicInteger(0); buds = new AtomicInteger(0);
metrics = new EngineMetrics(32); metrics = new EngineMetrics(32);
generationCacheWarm = new CompletableFuture<>();
cleanLatch = new ChronoLatch(10000); cleanLatch = new ChronoLatch(10000);
generatedLast = new AtomicInteger(0); generatedLast = new AtomicInteger(0);
perSecond = new AtomicDouble(0); perSecond = new AtomicDouble(0);
@@ -232,16 +235,17 @@ public class IrisEngine implements Engine {
runtimeBuilder.publishRuntime(initialRuntime, null); runtimeBuilder.publishRuntime(initialRuntime, null);
IrisLogging.debug("[IrisEngine timing] setupEngine total=" + (M.ms() - _t0) + "ms"); IrisLogging.debug("[IrisEngine timing] setupEngine total=" + (M.ms() - _t0) + "ms");
logStudioInitializationPhase("build_runtime", phaseStartedAt, false); logStudioInitializationPhase("build_runtime", phaseStartedAt, false);
_t0 = M.ms();
phaseStartedAt = System.nanoTime(); phaseStartedAt = System.nanoTime();
if (requiredMode.warmGenerationCaches()) { if (requiredMode.warmGenerationCaches()) {
GenerationCacheWarmer.warm(this); if (requiredMode.studio()) {
startGenerationCacheWarm(phaseStartedAt);
} else {
warmGenerationCaches(phaseStartedAt);
}
} else {
generationCacheWarm.complete(null);
logStudioInitializationPhase("generation_cache_warm", phaseStartedAt, true);
} }
IrisLogging.debug("[IrisEngine timing] cache warm total=" + (M.ms() - _t0) + "ms");
logStudioInitializationPhase(
"generation_cache_warm",
phaseStartedAt,
!requiredMode.warmGenerationCaches());
EngineTickRegistry.registerTicking(this); EngineTickRegistry.registerTicking(this);
} catch (Throwable e) { } catch (Throwable e) {
shutdownSequence.cleanupFailedConstruction(e); shutdownSequence.cleanupFailedConstruction(e);
@@ -250,6 +254,21 @@ public class IrisEngine implements Engine {
IrisLogging.debug("Engine Initialized " + getCacheID()); IrisLogging.debug("Engine Initialized " + getCacheID());
} }
public void awaitGenerationCacheWarm() {
if (generationCacheWarm.isDone() && !generationCacheWarm.isCompletedExceptionally()) {
return;
}
try {
generationCacheWarm.join();
} catch (CompletionException failure) {
throw new IllegalStateException("Iris generation caches did not become ready.", failure.getCause());
}
}
public boolean isGenerationCacheWarmPending() {
return !generationCacheWarm.isDone();
}
private void logStudioInitializationPhase(String phase, long startedAtNanos, boolean skipped) { private void logStudioInitializationPhase(String phase, long startedAtNanos, boolean skipped) {
if (!studio) { if (!studio) {
return; return;
@@ -262,6 +281,32 @@ public class IrisEngine implements Engine {
Boolean.toString(skipped)); Boolean.toString(skipped));
} }
private void startGenerationCacheWarm(long phaseStartedAtNanos) {
if (!backgroundTasks.scheduleTrackedTask(() -> warmGenerationCaches(phaseStartedAtNanos))) {
throw new IllegalStateException("Iris background task admission closed before generation cache warming.");
}
}
private void warmGenerationCaches(long phaseStartedAtNanos) {
long startedAtMillis = M.ms();
try {
GenerationCacheWarmer.warm(this);
generationCacheWarm.complete(null);
} catch (Throwable failure) {
generationCacheWarm.completeExceptionally(failure);
if (failure instanceof Error error) {
throw error;
}
if (failure instanceof RuntimeException runtimeException) {
throw runtimeException;
}
throw new IllegalStateException("Generation cache warming failed.", failure);
} finally {
IrisLogging.debug("[IrisEngine timing] cache warm total=" + (M.ms() - startedAtMillis) + "ms");
logStudioInitializationPhase("generation_cache_warm", phaseStartedAtNanos, false);
}
}
private void verifySeed() { private void verifySeed() {
if (getEngineData().getSeed() != null && getEngineData().getSeed() != target.getWorld().getRawWorldSeed()) { if (getEngineData().getSeed() != null && getEngineData().getSeed() != target.getWorld().getRawWorldSeed()) {
target.getWorld().setRawWorldSeed(getEngineData().getSeed()); target.getWorld().setRawWorldSeed(getEngineData().getSeed());
@@ -323,6 +368,7 @@ public class IrisEngine implements Engine {
@Override @Override
public void generateMatter(int x, int z, boolean multicore, ChunkContext context) { public void generateMatter(int x, int z, boolean multicore, ChunkContext context) {
awaitGenerationCacheWarm();
try (GenerationSessionLease lease = acquireGenerationLease("matter_generate"); try (GenerationSessionLease lease = acquireGenerationLease("matter_generate");
IrisContext.Scope ignored = IrisContext.open(this, lease.sessionId(), context)) { IrisContext.Scope ignored = IrisContext.open(this, lease.sessionId(), context)) {
IrisComplex activeComplex = getComplex(); IrisComplex activeComplex = getComplex();
@@ -574,6 +620,7 @@ public class IrisEngine implements Engine {
@BlockCoordinates @BlockCoordinates
@Override @Override
public void generate(int x, int z, Hunk<PlatformBlockState> vblocks, Hunk<PlatformBiome> vbiomes, boolean multicore) throws WrongEngineBroException { public void generate(int x, int z, Hunk<PlatformBlockState> vblocks, Hunk<PlatformBiome> vbiomes, boolean multicore) throws WrongEngineBroException {
awaitGenerationCacheWarm();
try (GenerationSessionLease lease = acquireGenerationLease("chunk_generate"); try (GenerationSessionLease lease = acquireGenerationLease("chunk_generate");
IrisContext.Scope generationScope = IrisContext.open(this, lease.sessionId(), null)) { IrisContext.Scope generationScope = IrisContext.open(this, lease.sessionId(), null)) {
getEngineData().getStatistics().generatedChunk(); getEngineData().getStatistics().generatedChunk();
@@ -48,7 +48,6 @@ import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@Data @Data
@@ -78,7 +77,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
final WorldBlockDropRouter blockDropRouter = new WorldBlockDropRouter(this); final WorldBlockDropRouter blockDropRouter = new WorldBlockDropRouter(this);
@Getter(AccessLevel.NONE) @Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE) @Setter(AccessLevel.NONE)
final WorldTeleportWarmup teleportWarmup = new WorldTeleportWarmup(this); final WorldTeleportWarmup teleportWarmup = new WorldTeleportWarmup();
private boolean looperStopped; private boolean looperStopped;
private volatile boolean cleanupServiceStopped; private volatile boolean cleanupServiceStopped;
volatile int entityCount = 0; volatile int entityCount = 0;
@@ -193,10 +192,6 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
}; };
} }
AtomicBoolean ignoreTeleport() {
return ignoreTP;
}
@Override @Override
public void onTick() { public void onTick() {
@@ -18,87 +18,53 @@
package art.arcane.iris.engine; package art.arcane.iris.engine;
import art.arcane.iris.core.localization.IrisLanguage; import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.core.localization.RuntimeUiMessages;
import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.iris.util.common.scheduling.jobs.QueueJob;
import art.arcane.volmlib.util.collection.KList;
import io.papermc.lib.PaperLib;
import org.bukkit.Chunk;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.player.PlayerTeleportEvent; import org.bukkit.event.player.PlayerTeleportEvent;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
/**
* Cancels a teleport, loads the destination chunks off the main thread and then replays the teleport
* on the thread that owns the player. The replay sets the manager's ignore flag so the reissued
* teleport is not intercepted again.
*/
final class WorldTeleportWarmup { final class WorldTeleportWarmup {
private final IrisWorldManager manager;
WorldTeleportWarmup(IrisWorldManager manager) {
this.manager = manager;
}
void teleportAsync(PlayerTeleportEvent e) { void teleportAsync(PlayerTeleportEvent e) {
Location destination = e.getTo();
if (destination == null) {
return;
}
Player player = e.getPlayer();
PlayerTeleportEvent.TeleportCause cause = e.getCause();
e.setCancelled(true); e.setCancelled(true);
warmupAreaAsync(e.getPlayer(), e.getTo(), () -> J.runEntity(e.getPlayer(), manager.managedTask( CompletableFuture<Boolean> teleport;
"bukkit_world_manager_teleport", try {
() -> { teleport = BukkitPlatform.teleportAsync(
manager.ignoreTeleport().set(true); player,
e.getPlayer().teleport(e.getTo(), e.getCause()); destination.clone(),
manager.ignoreTeleport().set(false); cause);
}))); } catch (Throwable failure) {
reportFailure(player, destination, failure);
return;
}
if (teleport == null) {
reportFailure(player, destination, new IllegalStateException(
"Async teleport returned no completion future."));
return;
}
teleport.whenComplete((success, failure) -> {
if (failure != null) {
reportFailure(player, destination, failure);
} else if (!Boolean.TRUE.equals(success)) {
reportFailure(player, destination, new IllegalStateException(
"Async teleport did not complete successfully."));
}
});
} }
private void warmupAreaAsync(Player player, Location to, Runnable r) { private void reportFailure(Player player, Location destination, Throwable failure) {
J.a(manager.managedTask("bukkit_world_manager_teleport_warmup", () -> { IrisLogging.error("Async teleport into Iris world failed for " + player.getName()
int viewDistance = 2; + " at " + destination.getBlockX() + ", " + destination.getBlockY() + ", "
KList<Future<Chunk>> futures = new KList<>(); + destination.getBlockZ() + ".");
for (int i = -viewDistance; i <= viewDistance; i++) { IrisLogging.reportError(failure);
for (int j = -viewDistance; j <= viewDistance; j++) {
int finalJ = j;
int finalI = i;
if (to.getWorld().isChunkLoaded((to.getBlockX() >> 4) + i, (to.getBlockZ() >> 4) + j)) {
futures.add(CompletableFuture.completedFuture(null));
continue;
}
futures.add(MultiBurst.burst.completeValue(()
-> PaperLib.getChunkAtAsync(to.getWorld(),
(to.getBlockX() >> 4) + finalI,
(to.getBlockZ() >> 4) + finalJ,
true, false).get()));
}
}
new QueueJob<Future<Chunk>>() {
@Override
public void execute(Future<Chunk> chunkFuture) {
try {
chunkFuture.get();
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
IrisLogging.debug("Chunk warmup interrupted while loading async teleport chunk.");
} catch (ExecutionException ex) {
IrisLogging.reportError(ex);
}
}
@Override
public String getName() {
return IrisLanguage.text(RuntimeUiMessages.JOB_LOADING_CHUNKS);
}
}.queue(futures).execute(new VolmitSender(player), true, r);
}));
} }
} }
@@ -102,7 +102,7 @@ public class IrisDecorantActuator extends EngineAssignedActuator<PlatformBlockSt
} }
if (height < surfaceFluidHeight && PREDICATE_SOLID.test(output.get(i, height, j)) if (height < surfaceFluidHeight && PREDICATE_SOLID.test(output.get(i, height, j))
&& height + 1 < output.getHeight() && B.isWater(output.get(i, height + 1, j))) { && height + 1 < output.getHeight() && B.isFluid(output.get(i, height + 1, j))) {
getSeaSurfaceDecorator().decorate(i, j, getSeaSurfaceDecorator().decorate(i, j,
realX, Math.round(i + 1), Math.round(x + i - 1), realX, Math.round(i + 1), Math.round(x + i - 1),
realZ, Math.round(z + j + 1), Math.round(z + j - 1), realZ, Math.round(z + j + 1), Math.round(z + j - 1),
@@ -92,7 +92,6 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
boolean hideOres = dimension.isHideOresForHiddenOre(); boolean hideOres = dimension.isHideOresForHiddenOre();
ChunkedDataCache<IrisBiome> biomeCache = context.getBiome(); ChunkedDataCache<IrisBiome> biomeCache = context.getBiome();
ChunkedDataCache<IrisRegion> regionCache = context.getRegion(); ChunkedDataCache<IrisRegion> regionCache = context.getRegion();
ChunkedDataCache<PlatformBlockState> fluidCache = context.getFluid();
ChunkedDataCache<PlatformBlockState> rockCache = context.getRock(); ChunkedDataCache<PlatformBlockState> rockCache = context.getRock();
int realX = xf + x; int realX = xf + x;
UpperDimensionContext upperContext = getEngine().getUpperContext(); UpperDimensionContext upperContext = getEngine().getUpperContext();
@@ -118,7 +117,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
} }
int topY = Math.min(hf, chunkHeight - 1); int topY = Math.min(hf, chunkHeight - 1);
PlatformBlockState fluid = fluidCache.get(xf, zf); PlatformBlockState fluid = complex.resolveSurfaceFluid(realX, realZ);
PlatformBlockState rock = rockCache.get(xf, zf); PlatformBlockState rock = rockCache.get(xf, zf);
PlatformBlockState mappedSurfaceBlock = complex.getImageMapRuntime().sampleSurfaceBlock(realX, realZ); PlatformBlockState mappedSurfaceBlock = complex.getImageMapRuntime().sampleSurfaceBlock(realX, realZ);
KList<IrisOreGenerator> biomeSurfaceOres = hideOres ? null : biome.getSurfaceOreGenerators(); KList<IrisOreGenerator> biomeSurfaceOres = hideOres ? null : biome.getSurfaceOreGenerators();
@@ -26,12 +26,16 @@ import art.arcane.iris.core.events.IrisEngineHotloadEvent;
import art.arcane.iris.util.common.format.C; import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.plugin.VolmitSender; import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J; import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.Location;
import org.bukkit.Sound; import org.bukkit.Sound;
import org.bukkit.World;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent; import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent; import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import org.bukkit.event.world.ChunkLoadEvent; import org.bukkit.event.world.ChunkLoadEvent;
import org.bukkit.event.world.ChunkUnloadEvent; import org.bukkit.event.world.ChunkUnloadEvent;
import org.bukkit.event.world.WorldSaveEvent; import org.bukkit.event.world.WorldSaveEvent;
@@ -45,7 +49,6 @@ public abstract class EngineAssignedWorldManager extends EngineAssignedComponent
private boolean listenerRegistered; private boolean listenerRegistered;
private boolean closeRequested; private boolean closeRequested;
private int taskId; private int taskId;
protected AtomicBoolean ignoreTP = new AtomicBoolean(false);
public EngineAssignedWorldManager() { public EngineAssignedWorldManager() {
super(null, null); super(null, null);
@@ -126,6 +129,29 @@ public abstract class EngineAssignedWorldManager extends EngineAssignedComponent
}); });
} }
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void on(PlayerTeleportEvent e) {
if (!BukkitPlatform.isPaperServer()
|| !getEngine().isStudio()
|| e.getCause() != PlayerTeleportEvent.TeleportCause.COMMAND) {
return;
}
Location destination = e.getTo();
World targetWorld = BukkitWorldBinding.world(getTarget().getWorld());
if (destination == null || targetWorld == null || !targetWorld.equals(destination.getWorld())) {
return;
}
int chunkX = destination.getBlockX() >> 4;
int chunkZ = destination.getBlockZ() >> 4;
if (targetWorld.isChunkLoaded(chunkX, chunkZ)) {
return;
}
runManagerTask("bukkit_world_manager_teleport_event", () -> teleportAsync(e));
}
@EventHandler @EventHandler
public void on(ChunkLoadEvent e) { public void on(ChunkLoadEvent e) {
runManagerTask("bukkit_world_manager_chunk_load", () -> { runManagerTask("bukkit_world_manager_chunk_load", () -> {
@@ -38,6 +38,19 @@ public interface MantleComponent extends Comparable<MantleComponent> {
return 0; return 0;
} }
default int getInputRadius(
int targetChunkX,
int targetChunkZ,
int invocationChunkRadius,
ChunkContext context
) {
return getInputRadius();
}
default boolean isInputGenerationLazy() {
return false;
}
default IrisData getData() { default IrisData getData() {
return getEngineMantle().getData(); return getEngineMantle().getData();
} }
@@ -66,9 +66,21 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
private final AtomicReferenceArray<MantleChunk<Matter>> window; private final AtomicReferenceArray<MantleChunk<Matter>> window;
public MantleWriter(EngineMantle engineMantle, Mantle<Matter> mantle, int x, int z, int radius, boolean multicore) { public MantleWriter(EngineMantle engineMantle, Mantle<Matter> mantle, int x, int z, int radius, boolean multicore) {
this(engineMantle, mantle, x, z, radius, radius * 2, multicore);
}
public MantleWriter(
EngineMantle engineMantle,
Mantle<Matter> mantle,
int x,
int z,
int prefetchRadius,
int accessRadius,
boolean multicore
) {
this.engineMantle = engineMantle; this.engineMantle = engineMantle;
this.mantle = mantle; this.mantle = mantle;
this.radius = radius * 2; this.radius = accessRadius;
this.x = x; this.x = x;
this.z = z; this.z = z;
// Every coordinate acquireChunk accepts lives in this window, so a flat array replaces the // Every coordinate acquireChunk accepts lives in this window, so a flat array replaces the
@@ -78,7 +90,10 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
final boolean foliaMaintenance = J.isFolia() final boolean foliaMaintenance = J.isFolia()
&& WorldMaintenance.isWorldMaintenanceActive(engineMantle.getEngine().getWorld().identity()); && WorldMaintenance.isWorldMaintenanceActive(engineMantle.getEngine().getWorld().identity());
final int parallelism = foliaMaintenance ? 1 : (multicore ? Runtime.getRuntime().availableProcessors() / 2 : 4); final int parallelism = resolvePrefetchParallelism(
foliaMaintenance,
multicore,
Runtime.getRuntime().availableProcessors());
if (foliaMaintenance && IrisSettings.get().getGeneral().isDebug()) { if (foliaMaintenance && IrisSettings.get().getGeneral().isDebug()) {
IrisLogging.info("MantleWriter using sequential chunk prefetch for maintenance regen at " + x + "," + z + "."); IrisLogging.info("MantleWriter using sequential chunk prefetch for maintenance regen at " + x + "," + z + ".");
} }
@@ -86,10 +101,10 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
// prefetch must release the permits already pinned into the window here. // prefetch must release the permits already pinned into the window here.
try { try {
mantle.getChunks( mantle.getChunks(
x - radius, x - prefetchRadius,
x + radius, x + prefetchRadius,
z - radius, z - prefetchRadius,
z + radius, z + prefetchRadius,
parallelism, parallelism,
this::storePrefetchedChunk this::storePrefetchedChunk
); );
@@ -99,6 +114,16 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
} }
} }
static int resolvePrefetchParallelism(boolean foliaMaintenance, boolean multicore, int availableProcessors) {
if (foliaMaintenance) {
return 1;
}
if (!multicore) {
return 4;
}
return Math.max(1, availableProcessors / 2);
}
private static Set<IrisPosition> getBallooned(Set<IrisPosition> vset, double radius) { private static Set<IrisPosition> getBallooned(Set<IrisPosition> vset, double radius) {
Set<IrisPosition> returnset = new HashSet<>(); Set<IrisPosition> returnset = new HashSet<>();
int ceilrad = (int) Math.ceil(radius); int ceilrad = (int) Math.ceil(radius);
@@ -4,6 +4,7 @@ import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.util.common.parallel.MultiBurst; import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.iris.util.project.context.ChunkContext; import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.volmlib.util.documentation.ChunkCoordinates; import art.arcane.volmlib.util.documentation.ChunkCoordinates;
import art.arcane.volmlib.util.mantle.flag.MantleFlag; import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import art.arcane.volmlib.util.mantle.runtime.Mantle; import art.arcane.volmlib.util.mantle.runtime.Mantle;
@@ -43,18 +44,31 @@ public interface MatterGenerator {
return; return;
} }
int writeRadius = getRadius(); MatterGenerationPlan generationPlan = resolveGenerationPlan(x, z, context);
MatterPassPlan[] passPlans = generationPlan.passPlans();
if (passPlans.length == 0) {
return;
}
int prefetchRadius = passPlans[0].passChunkRadius();
LongOpenHashSet partialChunks = new LongOpenHashSet(); LongOpenHashSet partialChunks = new LongOpenHashSet();
try (MantleWriter writer = new MantleWriter(getEngine().getMantle(), getMantle(), x, z, writeRadius, multicore)) { try (MantleWriter writer = new MantleWriter(
getEngine().getMantle(),
getMantle(),
x,
z,
prefetchRadius,
generationPlan.writerAccessRadius(),
multicore)) {
// Every task launched below writes through this writer. They must all be finished before // Every task launched below writes through this writer. They must all be finished before
// close() releases the cached chunks, even when a pass throws, or detached pool threads // close() releases the cached chunks, even when a pass throws, or detached pool threads
// write into released chunks. // write into released chunks.
List<MatterComponentTask> outstandingTasks = null; List<MatterComponentTask> outstandingTasks = null;
try { try {
for (MantlePass pass : getComponents()) { for (MatterPassPlan passPlan : passPlans) {
int passRadius = pass.passChunkRadius(); MantlePass pass = passPlan.pass();
int passRadius = passPlan.passChunkRadius();
List<MantleComponent> passComponents = pass.components(); List<MantleComponent> passComponents = pass.components();
MantleComponent[] enabledComponents = new MantleComponent[passComponents.size()]; MantleComponent[] enabledComponents = new MantleComponent[passComponents.size()];
int[] componentPassRadii = new int[passComponents.size()]; int[] componentPassRadii = new int[passComponents.size()];
@@ -63,7 +77,7 @@ public interface MatterGenerator {
if (component.isEnabled()) { if (component.isEnabled()) {
// A component must cover its own reach plus every later pass' reach, or a // A component must cover its own reach plus every later pass' reach, or a
// later pass reads this component's data from chunks it never wrote. // later pass reads this component's data from chunks it never wrote.
int componentReach = component.getRadius() + pass.downstreamBlockRadius(); int componentReach = component.getRadius() + passPlan.downstreamBlockRadius();
componentPassRadii[enabledComponentCount] = componentReach > 0 ? Math.ceilDiv(componentReach, 16) : 0; componentPassRadii[enabledComponentCount] = componentReach > 0 ? Math.ceilDiv(componentReach, 16) : 0;
enabledComponents[enabledComponentCount++] = component; enabledComponents[enabledComponentCount++] = component;
} }
@@ -112,7 +126,8 @@ public interface MatterGenerator {
MantleChunk<Matter> chunk = writer.acquireChunk(passX, passZ); MantleChunk<Matter> chunk = writer.acquireChunk(passX, passZ);
if (chunk == null) { if (chunk == null) {
throw new IllegalStateException("Mantle pass chunk " + passX + "," + passZ throw new IllegalStateException("Mantle pass chunk " + passX + "," + passZ
+ " is outside the writer prepared at " + x + "," + z + " with radius " + writeRadius); + " is outside the writer prepared at " + x + "," + z
+ " with radius " + generationPlan.writerAccessRadius());
} }
if (chunk.isFlagged(MantleFlag.PLANNED)) { if (chunk.isFlagged(MantleFlag.PLANNED)) {
@@ -174,8 +189,9 @@ public interface MatterGenerator {
} }
} }
for (int i = -getRealRadius(); i <= getRealRadius(); i++) { int realRadius = passPlans[passPlans.length - 1].passChunkRadius();
for (int j = -getRealRadius(); j <= getRealRadius(); j++) { for (int i = -realRadius; i <= realRadius; i++) {
for (int j = -realRadius; j <= realRadius; j++) {
int realX = x + i; int realX = x + i;
int realZ = z + j; int realZ = z + j;
long realKey = chunkKey(realX, realZ); long realKey = chunkKey(realX, realZ);
@@ -195,6 +211,45 @@ public interface MatterGenerator {
return (((long) x) << 32) ^ (z & 0xffffffffL); return (((long) x) << 32) ^ (z & 0xffffffffL);
} }
private MatterGenerationPlan resolveGenerationPlan(int x, int z, ChunkContext context) {
List<MantlePass> passes = getComponents();
MatterPassPlan[] plans = new MatterPassPlan[passes.size()];
int accessDownstreamBlockRadius = 0;
int generationDownstreamBlockRadius = 0;
for (int passIndex = passes.size() - 1; passIndex >= 0; passIndex--) {
MantlePass pass = passes.get(passIndex);
int passBlockRadius = 0;
int passAccessInputRadius = 0;
int passGenerationInputRadius = 0;
for (MantleComponent component : pass.components()) {
if (!component.isEnabled()) {
continue;
}
int componentRadius = component.getRadius();
passBlockRadius = Math.max(passBlockRadius, componentRadius);
int componentReach = componentRadius + generationDownstreamBlockRadius;
int invocationChunkRadius = componentReach > 0
? Math.ceilDiv(componentReach, 16)
: 0;
int componentInputRadius = component.getInputRadius(x, z, invocationChunkRadius, context);
passAccessInputRadius = Math.max(passAccessInputRadius, componentInputRadius);
if (!component.isInputGenerationLazy()) {
passGenerationInputRadius = Math.max(passGenerationInputRadius, componentInputRadius);
}
}
int accessInvocationRadius = accessDownstreamBlockRadius + passBlockRadius;
int generationInvocationRadius = generationDownstreamBlockRadius + passBlockRadius;
int passChunkRadius = generationInvocationRadius > 0 ? Math.ceilDiv(generationInvocationRadius, 16) : 0;
plans[passIndex] = new MatterPassPlan(pass, passChunkRadius, generationDownstreamBlockRadius);
accessDownstreamBlockRadius = accessInvocationRadius + passAccessInputRadius;
generationDownstreamBlockRadius = generationInvocationRadius + passGenerationInputRadius;
}
int writerChunkRadius = accessDownstreamBlockRadius > 0
? Math.ceilDiv(accessDownstreamBlockRadius, 16)
: 0;
return new MatterGenerationPlan(plans, writerChunkRadius * 2);
}
private MatterComponentTask runComponentAsync( private MatterComponentTask runComponentAsync(
MantleChunk<Matter> chunk, MantleChunk<Matter> chunk,
MantleComponent component, MantleComponent component,
@@ -217,11 +272,21 @@ public interface MatterGenerator {
try { try {
if (DISPATCHER.ownsCurrentThread()) { if (DISPATCHER.ownsCurrentThread()) {
completeComponentTask(future, key, chunk, component, writer, chunkX, chunkZ, context); completeComponentTask(future, key, chunk, component, writer, chunkX, chunkZ, context, IrisContext.get());
return new MatterComponentTask(key, future, null); return new MatterComponentTask(key, future, null);
} }
Future<?> submission = DISPATCHER.submit(() -> completeComponentTask(future, key, chunk, component, writer, chunkX, chunkZ, context)); IrisContext callerContext = IrisContext.get();
Future<?> submission = DISPATCHER.submit(() -> completeComponentTask(
future,
key,
chunk,
component,
writer,
chunkX,
chunkZ,
context,
callerContext));
return new MatterComponentTask(key, future, submission); return new MatterComponentTask(key, future, submission);
} catch (Throwable throwable) { } catch (Throwable throwable) {
IN_FLIGHT_COMPONENTS.remove(key, future); IN_FLIGHT_COMPONENTS.remove(key, future);
@@ -329,10 +394,11 @@ public interface MatterGenerator {
MantleWriter writer, MantleWriter writer,
int chunkX, int chunkX,
int chunkZ, int chunkZ,
ChunkContext context ChunkContext context,
IrisContext callerContext
) { ) {
try { try {
runComponentInline(chunk, component, writer, chunkX, chunkZ, context); runComponentWithContext(chunk, component, writer, chunkX, chunkZ, context, callerContext);
future.complete(null); future.complete(null);
} catch (Throwable throwable) { } catch (Throwable throwable) {
future.completeExceptionally(throwable); future.completeExceptionally(throwable);
@@ -342,6 +408,28 @@ public interface MatterGenerator {
} }
} }
private void runComponentWithContext(
MantleChunk<Matter> chunk,
MantleComponent component,
MantleWriter writer,
int chunkX,
int chunkZ,
ChunkContext context,
IrisContext callerContext
) {
if (callerContext == null) {
runComponentInline(chunk, component, writer, chunkX, chunkZ, context);
return;
}
try (IrisContext.Scope ignored = IrisContext.open(
callerContext.getEngine(),
callerContext.getGenerationSessionId(),
callerContext.getChunkContext())) {
runComponentInline(chunk, component, writer, chunkX, chunkZ, context);
}
}
private void runComponentInline( private void runComponentInline(
MantleChunk<Matter> chunk, MantleChunk<Matter> chunk,
MantleComponent component, MantleComponent component,
@@ -360,6 +448,12 @@ public interface MatterGenerator {
record MatterComponentTask(MatterTaskKey key, CompletableFuture<Void> future, Future<?> submission) { record MatterComponentTask(MatterTaskKey key, CompletableFuture<Void> future, Future<?> submission) {
} }
record MatterPassPlan(MantlePass pass, int passChunkRadius, int downstreamBlockRadius) {
}
record MatterGenerationPlan(MatterPassPlan[] passPlans, int writerAccessRadius) {
}
final class MatterTaskKey { final class MatterTaskKey {
private final Mantle<Matter> mantle; private final Mantle<Matter> mantle;
private final int chunkX; private final int chunkX;
@@ -27,6 +27,7 @@ final class CaveCarveScratch {
final int[] fluidMaxY = new int[256]; final int[] fluidMaxY = new int[256];
final int[] surfaceBreakFloorY = new int[256]; final int[] surfaceBreakFloorY = new int[256];
final boolean[] surfaceBreakColumn = new boolean[256]; final boolean[] surfaceBreakColumn = new boolean[256];
final boolean[] surfaceCeilingColumn = new boolean[256];
final double[] columnThreshold = new double[256]; final double[] columnThreshold = new double[256];
final double[] passThreshold = new double[256]; final double[] passThreshold = new double[256];
final double[] fullWeights = new double[256]; final double[] fullWeights = new double[256];
@@ -62,6 +63,7 @@ final class CaveCarveScratch {
int activeModulesY = Integer.MIN_VALUE; int activeModulesY = Integer.MIN_VALUE;
int activeModuleCount; int activeModuleCount;
double[] verticalEdgeFade = new double[0]; double[] verticalEdgeFade = new double[0];
double[] surfaceClosureThreshold = new double[0];
MatterCavern[] matterByY = new MatterCavern[0]; MatterCavern[] matterByY = new MatterCavern[0];
Matter[] sectionMatter = new Matter[0]; Matter[] sectionMatter = new Matter[0];
MatterSlice<?>[] sectionSlices = new MatterSlice<?>[0]; MatterSlice<?>[] sectionSlices = new MatterSlice<?>[0];
@@ -20,13 +20,15 @@ final class ConfiguredRiverGrottoShape implements RiverCaveGrottoShape {
private final CNG warpY; private final CNG warpY;
private final CNG warpZ; private final CNG warpZ;
private final double warpStrength; private final double warpStrength;
private final double boundaryVariation;
ConfiguredRiverGrottoShape( ConfiguredRiverGrottoShape(
long seed, long seed,
IrisData data, IrisData data,
IrisGeneratorStyle shapeStyle, IrisGeneratorStyle shapeStyle,
IrisGeneratorStyle warpStyle, IrisGeneratorStyle warpStyle,
double warpStrength double warpStrength,
double boundaryVariation
) { ) {
IrisGeneratorStyle resolvedShape = shapeStyle == null IrisGeneratorStyle resolvedShape = shapeStyle == null
? new IrisGeneratorStyle(NoiseStyle.FLAT) ? new IrisGeneratorStyle(NoiseStyle.FLAT)
@@ -39,6 +41,7 @@ final class ConfiguredRiverGrottoShape implements RiverCaveGrottoShape {
warpY = resolvedWarp.createNoCache(new RNG(seed ^ WARP_Y_SALT), data); warpY = resolvedWarp.createNoCache(new RNG(seed ^ WARP_Y_SALT), data);
warpZ = resolvedWarp.createNoCache(new RNG(seed ^ WARP_Z_SALT), data); warpZ = resolvedWarp.createNoCache(new RNG(seed ^ WARP_Z_SALT), data);
this.warpStrength = Math.max(0D, warpStrength); this.warpStrength = Math.max(0D, warpStrength);
this.boundaryVariation = Math.max(0D, Math.min(0.75D, boundaryVariation));
} }
@Override @Override
@@ -60,7 +63,7 @@ final class ConfiguredRiverGrottoShape implements RiverCaveGrottoShape {
double normalized = (warpedX * warpedX / (horizontalRadius * horizontalRadius)) double normalized = (warpedX * warpedX / (horizontalRadius * horizontalRadius))
+ (warpedY * warpedY / (verticalRadius * verticalRadius)) + (warpedY * warpedY / (verticalRadius * verticalRadius))
+ (warpedZ * warpedZ / (horizontalRadius * horizontalRadius)); + (warpedZ * warpedZ / (horizontalRadius * horizontalRadius));
double boundary = shape.fitDouble(-0.2D, 0.2D, worldX, worldY, worldZ); double boundary = shape.fitDouble(-boundaryVariation, boundaryVariation, worldX, worldY, worldZ);
return normalized <= 1D + boundary; return normalized <= 1D + boundary;
} }
} }
@@ -48,6 +48,8 @@ public class IrisCaveCarver3D {
private static final int ADAPTIVE_DEEP_SAMPLE_STEP = 8; private static final int ADAPTIVE_DEEP_SAMPLE_STEP = 8;
private static final double ADAPTIVE_LOCAL_RANGE_SCALE = 0.125D; private static final double ADAPTIVE_LOCAL_RANGE_SCALE = 0.125D;
private static final double ADAPTIVE_DEEP_MARGIN_BOOST = 0.015D; private static final double ADAPTIVE_DEEP_MARGIN_BOOST = 0.015D;
private static final int SURFACE_CEILING_FADE_DEPTH = 12;
private static final double SURFACE_CEILING_SOLID_EPSILON = 0.000001D;
private final Engine engine; private final Engine engine;
private final IrisData data; private final IrisData data;
@@ -250,9 +252,11 @@ public class IrisCaveCarver3D {
int[] fluidMaxY = scratch.fluidMaxY; int[] fluidMaxY = scratch.fluidMaxY;
int[] surfaceBreakFloorY = scratch.surfaceBreakFloorY; int[] surfaceBreakFloorY = scratch.surfaceBreakFloorY;
boolean[] surfaceBreakColumn = scratch.surfaceBreakColumn; boolean[] surfaceBreakColumn = scratch.surfaceBreakColumn;
boolean[] surfaceCeilingColumn = scratch.surfaceCeilingColumn;
double[] columnThreshold = scratch.columnThreshold; double[] columnThreshold = scratch.columnThreshold;
double[] clampedWeights = scratch.clampedColumnWeights; double[] clampedWeights = scratch.clampedColumnWeights;
double[] verticalEdgeFade = prepareVerticalEdgeFadeTable(scratch, minY, maxY); double[] verticalEdgeFade = prepareVerticalEdgeFadeTable(scratch, minY, maxY);
prepareSurfaceClosureThresholdTable(scratch, minY, maxY);
MatterCavern[] matterByY = prepareMatterByYTable(scratch, minY, maxY); MatterCavern[] matterByY = prepareMatterByYTable(scratch, minY, maxY);
prepareSectionCaches(scratch, minY, maxY); prepareSectionCaches(scratch, minY, maxY);
@@ -284,7 +288,8 @@ public class IrisCaveCarver3D {
} else { } else {
columnSurfaceY = engine.getHeight(x, z); columnSurfaceY = engine.getHeight(x, z);
} }
int clearanceTopY = Math.min(maxY, Math.max(minY, columnSurfaceY - surfaceClearance)); int unclampedClearanceTopY = columnSurfaceY - surfaceClearance;
int clearanceTopY = Math.min(maxY, Math.max(minY, unclampedClearanceTopY));
boolean breakColumn = allowSurfaceBreak boolean breakColumn = allowSurfaceBreak
&& surfaceBreakDensity.noiseFastSigned2D(x, z) >= surfaceBreakNoiseThreshold; && surfaceBreakDensity.noiseFastSigned2D(x, z) >= surfaceBreakNoiseThreshold;
int columnTopY = breakColumn int columnTopY = breakColumn
@@ -297,6 +302,7 @@ public class IrisCaveCarver3D {
: Integer.MIN_VALUE; : Integer.MIN_VALUE;
surfaceBreakFloorY[index] = Math.max(minY, columnSurfaceY - surfaceBreakDepth); surfaceBreakFloorY[index] = Math.max(minY, columnSurfaceY - surfaceBreakDepth);
surfaceBreakColumn[index] = breakColumn; surfaceBreakColumn[index] = breakColumn;
surfaceCeilingColumn[index] = !breakColumn && unclampedClearanceTopY <= maxY;
columnThreshold[index] = (thresholdDensity == null columnThreshold[index] = (thresholdDensity == null
? constantThreshold ? constantThreshold
: thresholdDensity.fitDouble(thresholdMin, thresholdMax, x, z)) - thresholdBias; : thresholdDensity.fitDouble(thresholdMin, thresholdMax, x, z)) - thresholdBias;
@@ -487,6 +493,7 @@ public class IrisCaveCarver3D {
localThreshold += surfaceBreakThresholdBoost; localThreshold += surfaceBreakThresholdBoost;
} }
localThreshold -= verticalEdgeFade[y - minY]; localThreshold -= verticalEdgeFade[y - minY];
localThreshold = applySurfaceCeilingFade(scratch, localThreshold, columnIndex, y, minY);
planeThresholdLimit[planeCount] = localThreshold * normalizationFactor; planeThresholdLimit[planeCount] = localThreshold * normalizationFactor;
planeCount++; planeCount++;
} }
@@ -514,7 +521,7 @@ public class IrisCaveCarver3D {
} }
double localThreshold = planeThresholdLimit[planeIndex] * inverseNormalization; double localThreshold = planeThresholdLimit[planeIndex] * inverseNormalization;
MatterCavern matter = resolveMatter(verticalMatter, x0 + localX, y, z0 + localZ, MatterCavern matter = resolveMatter(scratch, verticalMatter, x0 + localX, y, z0 + localZ,
columnIndex, fluidMaxY, localThreshold); columnIndex, fluidMaxY, localThreshold);
writeCavern(cavernSlice, localX, y, localZ, matter, fluidSupportPlan); writeCavern(cavernSlice, localX, y, localZ, matter, fluidSupportPlan);
carved++; carved++;
@@ -531,7 +538,7 @@ public class IrisCaveCarver3D {
int localX = PowerOfTwoCoordinates.unpackLocal16X(columnIndex); int localX = PowerOfTwoCoordinates.unpackLocal16X(columnIndex);
int localZ = columnIndex & 15; int localZ = columnIndex & 15;
double localThreshold = planeThresholdLimit[planeIndex] * inverseNormalization; double localThreshold = planeThresholdLimit[planeIndex] * inverseNormalization;
MatterCavern matter = resolveMatter(verticalMatter, x0 + localX, y, z0 + localZ, MatterCavern matter = resolveMatter(scratch, verticalMatter, x0 + localX, y, z0 + localZ,
columnIndex, fluidMaxY, localThreshold); columnIndex, fluidMaxY, localThreshold);
writeCavern(cavernSlice, localX, y, localZ, matter, fluidSupportPlan); writeCavern(cavernSlice, localX, y, localZ, matter, fluidSupportPlan);
carved++; carved++;
@@ -624,6 +631,7 @@ public class IrisCaveCarver3D {
localThreshold += surfaceBreakThresholdBoost; localThreshold += surfaceBreakThresholdBoost;
} }
localThreshold -= verticalEdgeFade[y - minY]; localThreshold -= verticalEdgeFade[y - minY];
localThreshold = applySurfaceCeilingFade(scratch, localThreshold, columnIndex, y, minY);
planeThresholdLimit[planeCount] = localThreshold * normalizationFactor; planeThresholdLimit[planeCount] = localThreshold * normalizationFactor;
planeCount++; planeCount++;
} }
@@ -662,7 +670,7 @@ public class IrisCaveCarver3D {
} }
double localThreshold = planeThresholdLimit[planeIndex] * inverseNormalization; double localThreshold = planeThresholdLimit[planeIndex] * inverseNormalization;
MatterCavern matter = resolveMatter(verticalMatter, x0 + localX, y, z0 + localZ, MatterCavern matter = resolveMatter(scratch, verticalMatter, x0 + localX, y, z0 + localZ,
columnIndex, fluidMaxY, localThreshold); columnIndex, fluidMaxY, localThreshold);
writeCavern(cavernSlice, localX, y, localZ, matter, fluidSupportPlan); writeCavern(cavernSlice, localX, y, localZ, matter, fluidSupportPlan);
carved++; carved++;
@@ -679,7 +687,7 @@ public class IrisCaveCarver3D {
int localX = PowerOfTwoCoordinates.unpackLocal16X(columnIndex); int localX = PowerOfTwoCoordinates.unpackLocal16X(columnIndex);
int localZ = columnIndex & 15; int localZ = columnIndex & 15;
double localThreshold = planeThresholdLimit[planeIndex] * inverseNormalization; double localThreshold = planeThresholdLimit[planeIndex] * inverseNormalization;
MatterCavern matter = resolveMatter(verticalMatter, x0 + localX, y, z0 + localZ, MatterCavern matter = resolveMatter(scratch, verticalMatter, x0 + localX, y, z0 + localZ,
columnIndex, fluidMaxY, localThreshold); columnIndex, fluidMaxY, localThreshold);
writeCavern(cavernSlice, localX, y, localZ, matter, fluidSupportPlan); writeCavern(cavernSlice, localX, y, localZ, matter, fluidSupportPlan);
carved++; carved++;
@@ -822,6 +830,7 @@ public class IrisCaveCarver3D {
localThreshold += surfaceBreakThresholdBoost; localThreshold += surfaceBreakThresholdBoost;
} }
localThreshold -= verticalEdgeFade[fadeIndex]; localThreshold -= verticalEdgeFade[fadeIndex];
localThreshold = applySurfaceCeilingFade(scratch, localThreshold, index, yy, minY);
if (density > localThreshold) { if (density > localThreshold) {
continue; continue;
} }
@@ -830,7 +839,7 @@ public class IrisCaveCarver3D {
int localZ = tileLocalZ[columnIndex]; int localZ = tileLocalZ[columnIndex];
int worldX = x0 + localX; int worldX = x0 + localX;
int worldZ = z0 + localZ; int worldZ = z0 + localZ;
MatterCavern matter = resolveMatter(verticalMatter, worldX, yy, worldZ, MatterCavern matter = resolveMatter(scratch, verticalMatter, worldX, yy, worldZ,
index, fluidMaxY, localThreshold); index, fluidMaxY, localThreshold);
if (skipExistingCarved) { if (skipExistingCarved) {
if (cavernSlice.get(localX, localY, localZ) == null) { if (cavernSlice.get(localX, localY, localZ) == null) {
@@ -897,23 +906,23 @@ public class IrisCaveCarver3D {
double threshold = columnThreshold[index] + thresholdBoost - ((1D - columnWeight) * thresholdPenalty); double threshold = columnThreshold[index] + thresholdBoost - ((1D - columnWeight) * thresholdPenalty);
for (int y = minY; y <= columnTopY; y += sampleStep) { for (int y = minY; y <= columnTopY; y += sampleStep) {
double localThreshold = threshold; double density = sampleDensityOptimized(scratch, x, y, z);
if (breakColumn && y >= breakFloorY) {
localThreshold += surfaceBreakThresholdBoost;
}
localThreshold -= verticalEdgeFade[y - minY];
if (sampleDensityOptimized(scratch, x, y, z) > localThreshold) {
continue;
}
int carveMaxY = Math.min(columnTopY, y + sampleStep - 1); int carveMaxY = Math.min(columnTopY, y + sampleStep - 1);
for (int yy = y; yy <= carveMaxY; yy++) { for (int yy = y; yy <= carveMaxY; yy++) {
if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaries, index, yy)) { if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaries, index, yy)) {
continue; continue;
} }
double localThreshold = threshold;
if (breakColumn && yy >= breakFloorY) {
localThreshold += surfaceBreakThresholdBoost;
}
localThreshold -= verticalEdgeFade[yy - minY];
localThreshold = applySurfaceCeilingFade(scratch, localThreshold, index, yy, minY);
if (density > localThreshold) {
continue;
}
MatterCavern verticalMatter = matterByY[yy - minY]; MatterCavern verticalMatter = matterByY[yy - minY];
MatterCavern matter = resolveMatter(verticalMatter, x, yy, z, MatterCavern matter = resolveMatter(scratch, verticalMatter, x, yy, z,
index, fluidMaxY, localThreshold); index, fluidMaxY, localThreshold);
MatterSlice<MatterCavern> cavernSlice = resolveCavernSlice(scratch, chunk, PowerOfTwoCoordinates.floorDivPow2(yy, 4)); MatterSlice<MatterCavern> cavernSlice = resolveCavernSlice(scratch, chunk, PowerOfTwoCoordinates.floorDivPow2(yy, 4));
int localY = yy & 15; int localY = yy & 15;
@@ -2191,57 +2200,61 @@ public class IrisCaveCarver3D {
return matterByY; return matterByY;
} }
private MatterCavern resolveMatter(MatterCavern verticalMatter, int x, int y, int z, private MatterCavern resolveMatter(CaveCarveScratch scratch, MatterCavern verticalMatter, int x, int y, int z,
int columnIndex, int[] fluidMaxY, double localThreshold) { int columnIndex, int[] fluidMaxY, double localThreshold) {
if (verticalMatter != carveLava if (verticalMatter != carveLava
&& y <= fluidMaxY[columnIndex] && y <= fluidMaxY[columnIndex]
&& isAquiferCandidate(x, y, z, localThreshold)) { && isAquiferCandidate(scratch, x, y, z, localThreshold)) {
return carveFluid; return carveFluid;
} }
return verticalMatter; return verticalMatter;
} }
private boolean isAquiferCandidate(int x, int y, int z, double localThreshold) { private boolean isAquiferCandidate(CaveCarveScratch scratch, int x, int y, int z, double localThreshold) {
double depthFactor = Math.max(0D, Math.min(1.5D, (fluidHeight - y) / 48D)); double depthFactor = Math.max(0D, Math.min(1.5D, (fluidHeight - y) / 48D));
double cutoff = 0.35D + (depthFactor * 0.2D); double cutoff = 0.35D + (depthFactor * 0.2D);
if (detailDensity.noiseFastSigned3D(x, y * 0.5D, z) <= cutoff) { if (detailDensity.noiseFastSigned3D(x, y * 0.5D, z) <= cutoff) {
return false; return false;
} }
return !fluidRequiresFloor || hasAquiferCupSupport(x, y, z, localThreshold); return !fluidRequiresFloor || hasAquiferCupSupport(scratch, x, y, z, localThreshold);
} }
private boolean hasAquiferCupSupport(int x, int y, int z, double threshold) { private boolean isAquiferCandidate(int x, int y, int z, double localThreshold) {
return isAquiferCandidate(scratchCache.get(), x, y, z, localThreshold);
}
private boolean hasAquiferCupSupport(CaveCarveScratch scratch, int x, int y, int z, double threshold) {
int floorY = Math.max(0, y - 1); int floorY = Math.max(0, y - 1);
int deepFloorY = Math.max(0, y - 2); int deepFloorY = Math.max(0, y - 2);
int aboveY = Math.min(aquiferCeilingY, y + 1); int aboveY = Math.min(aquiferCeilingY, y + 1);
if (!isDensitySolid(x, floorY, z, threshold)) { if (!isDensitySolid(scratch, x, floorY, z, threshold)) {
return false; return false;
} }
if (!isDensitySolid(x, deepFloorY, z, threshold - 0.05D)) { if (!isDensitySolid(scratch, x, deepFloorY, z, threshold - 0.05D)) {
return false; return false;
} }
int support = 0; int support = 0;
if (isDensitySolid(x + 1, y, z, threshold)) { if (isDensitySolid(scratch, x + 1, y, z, threshold)) {
support++; support++;
} }
if (isDensitySolid(x - 1, y, z, threshold)) { if (isDensitySolid(scratch, x - 1, y, z, threshold)) {
support++; support++;
} }
if (isDensitySolid(x, y, z + 1, threshold)) { if (isDensitySolid(scratch, x, y, z + 1, threshold)) {
support++; support++;
} }
if (isDensitySolid(x, y, z - 1, threshold)) { if (isDensitySolid(scratch, x, y, z - 1, threshold)) {
support++; support++;
} }
if (isDensitySolid(x, aboveY, z, threshold)) { if (isDensitySolid(scratch, x, aboveY, z, threshold)) {
support++; support++;
} }
return support >= 4; return support >= 4;
} }
private boolean isDensitySolid(int x, int y, int z, double threshold) { private boolean isDensitySolid(CaveCarveScratch scratch, int x, int y, int z, double threshold) {
return sampleDensityOptimized(x, y, z) > threshold; return sampleDensityOptimized(scratch, x, y, z) > threshold;
} }
private void writeCavern(MatterSlice<MatterCavern> cavernSlice, int localX, int y, int localZ, private void writeCavern(MatterSlice<MatterCavern> cavernSlice, int localX, int y, int localZ,
@@ -2288,6 +2301,52 @@ public class IrisCaveCarver3D {
return (value * 2D) - 1D; return (value * 2D) - 1D;
} }
private double applySurfaceCeilingFade(
CaveCarveScratch scratch,
double threshold,
int columnIndex,
int y,
int minY
) {
if (!scratch.surfaceCeilingColumn[columnIndex]) {
return threshold;
}
int ceilingDistance = scratch.columnMaxY[columnIndex] - y;
if (ceilingDistance < 0 || ceilingDistance >= SURFACE_CEILING_FADE_DEPTH) {
return threshold;
}
double closureThreshold = scratch.surfaceClosureThreshold[y - minY];
if (threshold <= closureThreshold) {
return threshold;
}
double progress = ceilingDistance / (double) SURFACE_CEILING_FADE_DEPTH;
double smooth = progress * progress * (3D - (2D * progress));
return closureThreshold + ((threshold - closureThreshold) * smooth);
}
private void prepareSurfaceClosureThresholdTable(CaveCarveScratch scratch, int minY, int maxY) {
int size = Math.max(0, maxY - minY + 1);
if (scratch.surfaceClosureThreshold.length < size) {
scratch.surfaceClosureThreshold = new double[size];
}
double baseMinimum = -Math.abs(baseWeight) - Math.abs(detailWeight);
for (int y = minY; y <= maxY; y++) {
double minimumDensity = baseMinimum;
for (CaveFieldModuleState module : modules) {
if (y < module.minY || y > module.maxY) {
continue;
}
minimumDensity += Math.min(module.minContribution, module.maxContribution);
}
scratch.surfaceClosureThreshold[y - minY] =
(minimumDensity * inverseNormalization) - SURFACE_CEILING_SOLID_EPSILON;
}
}
private double[] prepareVerticalEdgeFadeTable(CaveCarveScratch scratch, int minY, int maxY) { private double[] prepareVerticalEdgeFadeTable(CaveCarveScratch scratch, int minY, int maxY) {
int size = Math.max(0, maxY - minY + 1); int size = Math.max(0, maxY - minY + 1);
if (scratch.verticalEdgeFade.length < size) { if (scratch.verticalEdgeFade.length < size) {
@@ -474,7 +474,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
private void prefillProfileFieldSamples(int startX, int startZ, IrisComplex complex, BlendScratch blendScratch) { private void prefillProfileFieldSamples(int startX, int startZ, IrisComplex complex, BlendScratch blendScratch) {
fillFieldHeights(complex.getHeightStream(), startX, startZ, blendScratch.fieldSurfaceHeights); fillFieldHeights(complex.getHeightStream(), startX, startZ, blendScratch.fieldSurfaceHeights);
fillFieldHeights(complex.getRiverWaterSurfaceStream(), startX, startZ, blendScratch.fieldFluidHeights); fillFieldHeights(complex.getRiverWaterSurfaceStream(), startX, startZ, blendScratch.fieldFluidHeights);
fillFieldFluidPresence(complex.getFluidStream(), startX, startZ, blendScratch.fieldSurfaceHeights, fillFieldFluidPresence(complex, startX, startZ, blendScratch.fieldSurfaceHeights,
blendScratch.fieldFluidHeights, blendScratch.fieldHasFluid); blendScratch.fieldFluidHeights, blendScratch.fieldHasFluid);
fillFieldObjects(complex.getRegionStream(), startX, startZ, blendScratch.fieldRegions); fillFieldObjects(complex.getRegionStream(), startX, startZ, blendScratch.fieldRegions);
fillFieldObjects(complex.getTrueBiomeStream(), startX, startZ, blendScratch.fieldSurfaceBiomes); fillFieldObjects(complex.getTrueBiomeStream(), startX, startZ, blendScratch.fieldSurfaceBiomes);
@@ -500,7 +500,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
} }
private void fillFieldFluidPresence( private void fillFieldFluidPresence(
ProceduralStream<PlatformBlockState> stream, IrisComplex complex,
int startX, int startX,
int startZ, int startZ,
double[] surfaceHeights, double[] surfaceHeights,
@@ -511,7 +511,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
int worldX = startX + fieldX; int worldX = startX + fieldX;
for (int fieldZ = 0; fieldZ < FIELD_SIZE; fieldZ++) { for (int fieldZ = 0; fieldZ < FIELD_SIZE; fieldZ++) {
int fieldIndex = (fieldX * FIELD_SIZE) + fieldZ; int fieldIndex = (fieldX * FIELD_SIZE) + fieldZ;
target[fieldIndex] = B.isFluid(stream.get(worldX, startZ + fieldZ)) target[fieldIndex] = B.isFluid(complex.resolveSurfaceFluid(worldX, startZ + fieldZ))
&& Math.round(surfaceHeights[fieldIndex]) < Math.round(fluidHeights[fieldIndex]); && Math.round(surfaceHeights[fieldIndex]) < Math.round(fluidHeights[fieldIndex]);
} }
} }
@@ -3,7 +3,7 @@ package art.arcane.iris.engine.mantle.components;
import art.arcane.iris.engine.river.cave.CavePosition; import art.arcane.iris.engine.river.cave.CavePosition;
import art.arcane.iris.engine.river.cave.CaveVoxel; import art.arcane.iris.engine.river.cave.CaveVoxel;
import art.arcane.iris.engine.river.cave.CaveVoxelView; import art.arcane.iris.engine.river.cave.CaveVoxelView;
import art.arcane.iris.engine.river.cave.RiverCaveAction; import art.arcane.iris.engine.river.cave.RiverCaveFluidKind;
import art.arcane.iris.engine.river.cave.RiverCaveHydrology; import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
import art.arcane.iris.engine.data.cache.Cache; import art.arcane.iris.engine.data.cache.Cache;
import art.arcane.iris.engine.object.IrisProceduralBlocks; import art.arcane.iris.engine.object.IrisProceduralBlocks;
@@ -15,8 +15,10 @@ import art.arcane.volmlib.util.mantle.runtime.TectonicPlate;
import art.arcane.volmlib.util.matter.Matter; import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterCavern; import art.arcane.volmlib.util.matter.MatterCavern;
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import java.util.Objects; import java.util.Objects;
import java.util.function.BiConsumer;
final class MantleRiverCaveVoxelView implements MantleRiverHydrologyComponent.TunnelVoxelView { final class MantleRiverCaveVoxelView implements MantleRiverHydrologyComponent.TunnelVoxelView {
private static final int CLOSED_COLUMN = Integer.MAX_VALUE; private static final int CLOSED_COLUMN = Integer.MAX_VALUE;
@@ -26,6 +28,9 @@ final class MantleRiverCaveVoxelView implements MantleRiverHydrologyComponent.Tu
private final int worldHeight; private final int worldHeight;
private final Function2<Integer, Integer, Integer> surfaceHeight; private final Function2<Integer, Integer, Integer> surfaceHeight;
private final Function2<Integer, Integer, PlatformBlockState> compatibleFluid; private final Function2<Integer, Integer, PlatformBlockState> compatibleFluid;
private final RiverCaveFluidKind planningFluidKind;
private final BiConsumer<Integer, Integer> chunkLoader;
private final LongOpenHashSet loadedChunks;
private final Long2IntOpenHashMap openFloorCache; private final Long2IntOpenHashMap openFloorCache;
private final Long2IntOpenHashMap surfaceHeightCache; private final Long2IntOpenHashMap surfaceHeightCache;
@@ -33,12 +38,17 @@ final class MantleRiverCaveVoxelView implements MantleRiverHydrologyComponent.Tu
Mantle<Matter> mantle, Mantle<Matter> mantle,
int worldHeight, int worldHeight,
Function2<Integer, Integer, Integer> surfaceHeight, Function2<Integer, Integer, Integer> surfaceHeight,
Function2<Integer, Integer, PlatformBlockState> compatibleFluid Function2<Integer, Integer, PlatformBlockState> compatibleFluid,
RiverCaveFluidKind planningFluidKind,
BiConsumer<Integer, Integer> chunkLoader
) { ) {
this.mantle = Objects.requireNonNull(mantle); this.mantle = Objects.requireNonNull(mantle);
this.worldHeight = worldHeight; this.worldHeight = worldHeight;
this.surfaceHeight = Objects.requireNonNull(surfaceHeight); this.surfaceHeight = Objects.requireNonNull(surfaceHeight);
this.compatibleFluid = Objects.requireNonNull(compatibleFluid); this.compatibleFluid = Objects.requireNonNull(compatibleFluid);
this.planningFluidKind = Objects.requireNonNull(planningFluidKind);
this.chunkLoader = Objects.requireNonNull(chunkLoader);
loadedChunks = new LongOpenHashSet();
openFloorCache = new Long2IntOpenHashMap(); openFloorCache = new Long2IntOpenHashMap();
openFloorCache.defaultReturnValue(CACHE_MISS); openFloorCache.defaultReturnValue(CACHE_MISS);
surfaceHeightCache = new Long2IntOpenHashMap(); surfaceHeightCache = new Long2IntOpenHashMap();
@@ -52,10 +62,18 @@ final class MantleRiverCaveVoxelView implements MantleRiverHydrologyComponent.Tu
@Override @Override
public CaveVoxel voxelAt(CavePosition position) { public CaveVoxel voxelAt(CavePosition position) {
RiverCaveHydrology hydrology = dataIfPresent(position, RiverCaveHydrology.class);
if (hydrology != null && hydrology.fluidKind() != planningFluidKind) {
return CaveVoxel.INCOMPATIBLE_FLUID;
}
MatterCavern cavern = dataIfPresent(position, MatterCavern.class); MatterCavern cavern = dataIfPresent(position, MatterCavern.class);
if (cavern != null) { if (cavern != null) {
if (cavern.isLava()) { if (cavern.isLava()) {
return CaveVoxel.LAVA; PlatformBlockState expected = compatibleFluid.apply(position.x(), position.z());
return expected != null
&& IrisProceduralBlocks.materialKey(expected).endsWith(":lava")
? CaveVoxel.COMPATIBLE_FLUID
: CaveVoxel.LAVA;
} }
if (cavern.getLiquid() == 1) { if (cavern.getLiquid() == 1) {
return CaveVoxel.COMPATIBLE_FLUID; return CaveVoxel.COMPATIBLE_FLUID;
@@ -71,13 +89,13 @@ final class MantleRiverCaveVoxelView implements MantleRiverHydrologyComponent.Tu
if (!block.isFluid()) { if (!block.isFluid()) {
return CaveVoxel.SOLID; return CaveVoxel.SOLID;
} }
if (IrisProceduralBlocks.materialKey(block).endsWith(":lava")) {
return CaveVoxel.LAVA;
}
PlatformBlockState expected = compatibleFluid.apply(position.x(), position.z()); PlatformBlockState expected = compatibleFluid.apply(position.x(), position.z());
return expected != null if (expected != null
&& IrisProceduralBlocks.materialKey(expected).equals(IrisProceduralBlocks.materialKey(block)) && IrisProceduralBlocks.materialKey(expected).equals(IrisProceduralBlocks.materialKey(block))) {
? CaveVoxel.COMPATIBLE_FLUID return CaveVoxel.COMPATIBLE_FLUID;
}
return IrisProceduralBlocks.materialKey(block).endsWith(":lava")
? CaveVoxel.LAVA
: CaveVoxel.INCOMPATIBLE_FLUID; : CaveVoxel.INCOMPATIBLE_FLUID;
} }
@@ -99,9 +117,8 @@ final class MantleRiverCaveVoxelView implements MantleRiverHydrologyComponent.Tu
} }
@Override @Override
public RiverCaveAction riverActionAt(CavePosition position) { public RiverCaveHydrology riverHydrologyAt(CavePosition position) {
RiverCaveHydrology hydrology = dataIfPresent(position, RiverCaveHydrology.class); return dataIfPresent(position, RiverCaveHydrology.class);
return hydrology == null ? null : hydrology.action();
} }
private int resolveOpenFloor(int x, int z) { private int resolveOpenFloor(int x, int z) {
@@ -131,6 +148,10 @@ final class MantleRiverCaveVoxelView implements MantleRiverHydrologyComponent.Tu
private <T> T dataIfPresent(CavePosition position, Class<T> type) { private <T> T dataIfPresent(CavePosition position, Class<T> type) {
int chunkX = position.x() >> 4; int chunkX = position.x() >> 4;
int chunkZ = position.z() >> 4; int chunkZ = position.z() >> 4;
long chunkKey = Mantle.key(chunkX, chunkZ);
if (loadedChunks.add(chunkKey)) {
chunkLoader.accept(chunkX, chunkZ);
}
TectonicPlate<Matter> plate = mantle.getLoadedRegions().get(Mantle.key(chunkX >> 5, chunkZ >> 5)); TectonicPlate<Matter> plate = mantle.getLoadedRegions().get(Mantle.key(chunkX >> 5, chunkZ >> 5));
if (plate == null || plate.isClosed()) { if (plate == null || plate.isClosed()) {
return null; return null;
@@ -1,17 +1,21 @@
package art.arcane.iris.engine.mantle.components; package art.arcane.iris.engine.mantle.components;
import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.data.cache.Cache;
import art.arcane.iris.engine.mantle.ComponentFlag; import art.arcane.iris.engine.mantle.ComponentFlag;
import art.arcane.iris.engine.mantle.EngineMantle; import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.mantle.IrisMantleComponent; import art.arcane.iris.engine.mantle.IrisMantleComponent;
import art.arcane.iris.engine.mantle.MantleComponent;
import art.arcane.iris.engine.mantle.MantleWriter; import art.arcane.iris.engine.mantle.MantleWriter;
import art.arcane.iris.engine.object.IrisRiverCaveFallback; import art.arcane.iris.engine.object.IrisRiverCaveFallback;
import art.arcane.iris.engine.object.IrisRiverCaveMode; import art.arcane.iris.engine.object.IrisRiverCaveMode;
import art.arcane.iris.engine.object.IrisRiverCaves; import art.arcane.iris.engine.object.IrisRiverCaves;
import art.arcane.iris.engine.object.IrisRiverDeepPools;
import art.arcane.iris.engine.object.IrisRiverExistingFluidPolicy; import art.arcane.iris.engine.object.IrisRiverExistingFluidPolicy;
import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisRiverNetwork; import art.arcane.iris.engine.object.IrisRiverNetwork;
import art.arcane.iris.engine.river.RiverAnchor; import art.arcane.iris.engine.river.RiverAnchor;
import art.arcane.iris.engine.river.RiverNetwork;
import art.arcane.iris.engine.river.RiverRouteState; import art.arcane.iris.engine.river.RiverRouteState;
import art.arcane.iris.engine.river.RiverSample; import art.arcane.iris.engine.river.RiverSample;
import art.arcane.iris.engine.river.RiverSection; import art.arcane.iris.engine.river.RiverSection;
@@ -22,6 +26,7 @@ import art.arcane.iris.engine.river.cave.CaveVoxelPrecondition;
import art.arcane.iris.engine.river.cave.CaveVoxelView; import art.arcane.iris.engine.river.cave.CaveVoxelView;
import art.arcane.iris.engine.river.cave.RiverCaveAction; import art.arcane.iris.engine.river.cave.RiverCaveAction;
import art.arcane.iris.engine.river.cave.RiverCaveContainmentPlanner; import art.arcane.iris.engine.river.cave.RiverCaveContainmentPlanner;
import art.arcane.iris.engine.river.cave.RiverCaveFluidKind;
import art.arcane.iris.engine.river.cave.RiverCaveFluidPolicy; import art.arcane.iris.engine.river.cave.RiverCaveFluidPolicy;
import art.arcane.iris.engine.river.cave.RiverCaveHydrology; import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
import art.arcane.iris.engine.river.cave.RiverCaveMode; import art.arcane.iris.engine.river.cave.RiverCaveMode;
@@ -35,8 +40,13 @@ import art.arcane.iris.engine.river.runtime.IrisRiverTunnelSample;
import art.arcane.iris.util.project.context.ChunkContext; import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.volmlib.util.mantle.flag.MantleFlag; import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import art.arcane.volmlib.util.mantle.flag.ReservedFlag; import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.math.BlockPosition;
import art.arcane.volmlib.util.matter.Matter;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
@@ -47,6 +57,8 @@ import java.util.Set;
@ComponentFlag(ReservedFlag.RIVER_HYDROLOGY) @ComponentFlag(ReservedFlag.RIVER_HYDROLOGY)
public final class MantleRiverHydrologyComponent extends IrisMantleComponent { public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
private static final long CANDIDATE_SALT = 0x6A09E667F3BCC909L; private static final long CANDIDATE_SALT = 0x6A09E667F3BCC909L;
private static final long DEEP_POOL_CANDIDATE_SALT = 0xBB67AE8584CAA73BL;
private static final long DEEP_POOL_POSITION_SALT = 0x3C6EF372FE94F82BL;
static final int PRIORITY = 1; static final int PRIORITY = 1;
private static final int[] FALLBACK_X = {0, 1, -1, 0, 0}; private static final int[] FALLBACK_X = {0, 1, -1, 0, 0};
private static final int[] FALLBACK_Z = {0, 0, 0, 1, -1}; private static final int[] FALLBACK_Z = {0, 0, 0, 1, -1};
@@ -73,6 +85,11 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
return PREREQUISITES; return PREREQUISITES;
} }
@Override
public boolean isInputGenerationLazy() {
return true;
}
@Override @Override
public int getInputRadius() { public int getInputRadius() {
if (!getDimension().isCarvingEnabled() if (!getDimension().isCarvingEnabled()
@@ -87,6 +104,122 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
return inputRadius(runtime.caveSettings(), tunnelHalo(runtime)); return inputRadius(runtime.caveSettings(), tunnelHalo(runtime));
} }
@Override
public int getInputRadius(
int targetChunkX,
int targetChunkZ,
int invocationChunkRadius,
ChunkContext context
) {
if (!getDimension().isCarvingEnabled()
|| getDimension().getRivers() == null
|| !getDimension().getRivers().isEnabled()) {
return 0;
}
if (context == null || context.getComplex() == null) {
return getInputRadius();
}
IrisRiverRuntime runtime = context.getComplex().getRiverRuntime();
if (runtime == null) {
return getInputRadius();
}
IrisRiverCaves caves = runtime.caveSettings();
int tunnelRadius = tunnelHalo(runtime);
int radius = hasRiverFootprint(
runtime,
targetChunkX,
targetChunkZ,
invocationChunkRadius,
tunnelRadius
) ? tunnelRadius : 0;
if (caves.getMode() != IrisRiverCaveMode.SEALED
&& caves.getMaximumPerReach() > 0) {
radius = Math.max(radius, hasAcceptedCaveAnchor(
runtime,
caves,
targetChunkX,
targetChunkZ,
invocationChunkRadius
) ? planningHalo(caves) : 0);
}
IrisRiverDeepPools deepPools = caves.getDeepPools();
if (deepPools != null
&& deepPools.isEnabled()
&& deepPools.getMaximumPerReach() > 0) {
radius = Math.max(radius, hasAcceptedDeepPoolAnchor(
runtime,
deepPools,
targetChunkX,
targetChunkZ,
invocationChunkRadius
) ? deepPoolPlanningHalo(deepPools) : 0);
}
return radius;
}
private static boolean hasRiverFootprint(
IrisRiverRuntime runtime,
int targetChunkX,
int targetChunkZ,
int invocationChunkRadius,
int tunnelRadius
) {
return runtime.hasRiverFootprint(
((targetChunkX - invocationChunkRadius) << 4) - tunnelRadius,
((targetChunkZ - invocationChunkRadius) << 4) - tunnelRadius,
((targetChunkX + invocationChunkRadius + 1) << 4) + tunnelRadius,
((targetChunkZ + invocationChunkRadius + 1) << 4) + tunnelRadius
);
}
private static boolean hasAcceptedCaveAnchor(
IrisRiverRuntime runtime,
IrisRiverCaves caves,
int targetChunkX,
int targetChunkZ,
int invocationChunkRadius
) {
int candidateHalo = candidateHalo(caves);
List<RiverAnchor> anchors = runtime.candidateAnchors(
((targetChunkX - invocationChunkRadius) << 4) - candidateHalo,
((targetChunkZ - invocationChunkRadius) << 4) - candidateHalo,
((targetChunkX + invocationChunkRadius + 1) << 4) + candidateHalo,
((targetChunkZ + invocationChunkRadius + 1) << 4) + candidateHalo,
caves.getMinimumSpacing(),
CANDIDATE_SALT
);
for (RiverAnchor anchor : anchors) {
if (runtime.acceptsCaveAnchor(anchor)) {
return true;
}
}
return false;
}
private static boolean hasAcceptedDeepPoolAnchor(
IrisRiverRuntime runtime,
IrisRiverDeepPools deepPools,
int targetChunkX,
int targetChunkZ,
int invocationChunkRadius
) {
int candidateHalo = deepPoolCandidateHalo(deepPools);
List<RiverAnchor> anchors = runtime.candidateAnchors(
((targetChunkX - invocationChunkRadius) << 4) - candidateHalo,
((targetChunkZ - invocationChunkRadius) << 4) - candidateHalo,
((targetChunkX + invocationChunkRadius + 1) << 4) + candidateHalo,
((targetChunkZ + invocationChunkRadius + 1) << 4) + candidateHalo,
deepPools.getMinimumSpacing(),
DEEP_POOL_CANDIDATE_SALT
);
for (RiverAnchor anchor : anchors) {
if (runtime.acceptsDeepPoolAnchor(anchor)) {
return true;
}
}
return false;
}
@Override @Override
public void generateLayer(MantleWriter writer, int chunkX, int chunkZ, ChunkContext context) { public void generateLayer(MantleWriter writer, int chunkX, int chunkZ, ChunkContext context) {
IrisRiverRuntime runtime = context.getComplex().getRiverRuntime(); IrisRiverRuntime runtime = context.getComplex().getRiverRuntime();
@@ -96,11 +229,21 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
publishTunnels(writer, context, runtime, chunkX, chunkZ); publishTunnels(writer, context, runtime, chunkX, chunkZ);
IrisRiverCaves caves = runtime.caveSettings(); IrisRiverCaves caves = runtime.caveSettings();
if (caves.getMode() == IrisRiverCaveMode.SEALED || caves.getMaximumPerReach() <= 0) { if (caves.getMode() != IrisRiverCaveMode.SEALED && caves.getMaximumPerReach() > 0) {
return; publishCaveConnections(writer, context, runtime, caves, chunkX, chunkZ);
} }
publishDeepPools(writer, context, runtime, caves.getDeepPools(), chunkX, chunkZ);
}
MantleRiverCaveVoxelView view = createView(writer, context); private void publishCaveConnections(
MantleWriter writer,
ChunkContext context,
IrisRiverRuntime runtime,
IrisRiverCaves caves,
int chunkX,
int chunkZ
) {
MantleRiverCaveVoxelView view = createView(writer, context, RiverCaveFluidKind.RIVER);
int candidateHalo = candidateHalo(caves); int candidateHalo = candidateHalo(caves);
int minimumX = (chunkX << 4) - candidateHalo; int minimumX = (chunkX << 4) - candidateHalo;
int minimumZ = (chunkZ << 4) - candidateHalo; int minimumZ = (chunkZ << 4) - candidateHalo;
@@ -146,11 +289,202 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
} }
RiverCavePlanningResult result = planner.planAll(view, sources, settings); RiverCavePlanningResult result = planner.planAll(view, sources, settings);
MantleRiverCaveVoxelView revalidationView = createView(writer, context); MantleRiverCaveVoxelView revalidationView = createView(
writer,
context,
RiverCaveFluidKind.RIVER
);
if (!preconditionsHold(revalidationView, result.baselinePreconditions())) { if (!preconditionsHold(revalidationView, result.baselinePreconditions())) {
return; return;
} }
publishLocal(writer, chunkX, chunkZ, result, floodedBiomes); publishLocal(
writer,
chunkX,
chunkZ,
result,
floodedBiomes,
RiverCaveFluidKind.RIVER
);
}
private void publishDeepPools(
MantleWriter writer,
ChunkContext context,
IrisRiverRuntime runtime,
IrisRiverDeepPools deepPools,
int chunkX,
int chunkZ
) {
if (deepPools == null || !deepPools.isEnabled() || deepPools.getMaximumPerReach() <= 0) {
return;
}
MantleRiverCaveVoxelView view = createView(
writer,
context,
RiverCaveFluidKind.DEEP_POOL
);
int candidateHalo = deepPoolCandidateHalo(deepPools);
int minimumX = (chunkX << 4) - candidateHalo;
int minimumZ = (chunkZ << 4) - candidateHalo;
int maximumX = ((chunkX + 1) << 4) + candidateHalo;
int maximumZ = ((chunkZ + 1) << 4) + candidateHalo;
List<RiverAnchor> anchors = runtime.candidateAnchors(
minimumX,
minimumZ,
maximumX,
maximumZ,
deepPools.getMinimumSpacing(),
DEEP_POOL_CANDIDATE_SALT
);
if (anchors.isEmpty()) {
return;
}
RiverCavePlannerSettings settings = deepPoolPlannerSettings(deepPools, seed(), getData());
List<RiverCaveSource> sources = new ArrayList<>();
Map<Long, String> floodedBiomes = new HashMap<>();
for (RiverAnchor anchor : anchors) {
if (!runtime.acceptsDeepPoolAnchor(anchor)) {
continue;
}
RiverCaveSource source = deepPoolSourceFor(
view,
deepPools,
anchor,
getDimension().getMinHeight(),
seed()
);
if (source == null) {
continue;
}
sources.add(source);
floodedBiomes.put(source.sourceId(), runtime.selectFloodedCaveBiome(anchor));
}
if (sources.isEmpty()) {
return;
}
RiverCavePlanningResult result = planner.planAll(view, sources, settings);
MantleRiverCaveVoxelView revalidationView = createView(
writer,
context,
RiverCaveFluidKind.DEEP_POOL
);
if (!preconditionsHold(revalidationView, result.baselinePreconditions())) {
return;
}
publishLocal(
writer,
chunkX,
chunkZ,
result,
floodedBiomes,
RiverCaveFluidKind.DEEP_POOL
);
}
static RiverCavePlannerSettings deepPoolPlannerSettings(
IrisRiverDeepPools deepPools,
long seed,
IrisData data
) {
int verticalDepth = deepPools.getVerticalRadius() * 2 - deepPools.getDryHeadroom() + 1;
int proofRadius = (int) StrictMath.ceil(
StrictMath.sqrt(2D) * deepPools.getHorizontalRadius()
) + 1;
return new RiverCavePlannerSettings(
proofRadius,
verticalDepth,
deepPools.getMaximumVolume(),
deepPools.getVerticalRadius(),
1,
deepPools.getHorizontalRadius(),
deepPools.getVerticalRadius(),
deepPools.getDryHeadroom(),
RiverCaveFluidPolicy.REJECT_EXISTING,
new ConfiguredRiverGrottoShape(
seed ^ DEEP_POOL_POSITION_SALT,
data,
deepPools.getShapeStyle(),
deepPools.getWarpStyle(),
deepPools.getWarpStrength(),
deepPools.getShapeVariation()
),
proofRadius,
verticalDepth
);
}
static RiverCaveSource deepPoolSourceFor(
CaveVoxelView view,
IrisRiverDeepPools deepPools,
RiverAnchor anchor,
int worldMinimumY,
long seed
) {
int minimumHead = deepPools.getMinimumFluidY() - worldMinimumY;
int maximumHead = deepPools.getMaximumFluidY() - worldMinimumY;
int headRange = maximumHead - minimumHead + 1;
if (headRange <= 0) {
return null;
}
int searchRadius = deepPools.getSearchRadius();
int searchWidth = searchRadius * 2 + 1;
int targetDepth = Math.max(
1,
deepPools.getVerticalRadius() - deepPools.getDryHeadroom()
);
for (int attempt = 0; attempt < deepPools.getSearchAttempts(); attempt++) {
long hash = RiverNetwork.mix(
seed
^ anchor.stableId()
^ DEEP_POOL_POSITION_SALT
^ (long) attempt * 0x9E3779B97F4A7C15L
);
int offsetX = searchRadius == 0
? 0
: Math.floorMod((int) hash, searchWidth) - searchRadius;
int offsetZ = searchRadius == 0
? 0
: Math.floorMod((int) (hash >>> 32), searchWidth) - searchRadius;
if ((long) offsetX * offsetX + (long) offsetZ * offsetZ
> (long) searchRadius * searchRadius) {
continue;
}
int x = (int) StrictMath.floor(anchor.x()) + offsetX;
int z = (int) StrictMath.floor(anchor.z()) + offsetZ;
int startOffset = (int) StrictMath.floor(unit(hash) * headRange);
for (int scanned = 0; scanned < headRange; scanned++) {
int headY = maximumHead - Math.floorMod(startOffset + scanned, headRange);
CavePosition floor = new CavePosition(x, headY, z);
CavePosition above = new CavePosition(x, headY + 1, z);
CavePosition target = new CavePosition(x, headY - targetDepth, z);
if (!view.isInWorld(target)
|| !view.isInWorld(above)
|| view.voxelAt(floor) != CaveVoxel.SOLID
|| view.voxelAt(above) != CaveVoxel.CAVE_AIR
|| view.isOpenToSurface(above)) {
continue;
}
long sourceId = RiverNetwork.mix(
anchor.stableId()
^ BlockPosition.toLong(x, headY, z)
^ DEEP_POOL_POSITION_SALT
);
return new RiverCaveSource(
sourceId,
floor,
target,
headY,
RiverCaveMode.DEEP_POOL
);
}
}
return null;
}
private static double unit(long hash) {
return (hash >>> 11) * 0x1.0p-53;
} }
@Override @Override
@@ -176,9 +510,15 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
return false; return false;
} }
IrisRiverCaves caves = dimension.getRivers().getCaves(); IrisRiverCaves caves = dimension.getRivers().getCaves();
return caves != null if (caves == null) {
&& caves.getMode() != IrisRiverCaveMode.SEALED return false;
}
boolean caveConnections = caves.getMode() != IrisRiverCaveMode.SEALED
&& caves.getMaximumPerReach() > 0; && caves.getMaximumPerReach() > 0;
IrisRiverDeepPools deepPools = caves.getDeepPools();
return caveConnections || deepPools != null
&& deepPools.isEnabled()
&& deepPools.getMaximumPerReach() > 0;
} }
static int planningHalo(IrisRiverCaves caves) { static int planningHalo(IrisRiverCaves caves) {
@@ -186,16 +526,33 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
} }
static int inputRadius(IrisRiverCaves caves, int tunnelRadius) { static int inputRadius(IrisRiverCaves caves, int tunnelRadius) {
if (caves.getMode() == IrisRiverCaveMode.SEALED || caves.getMaximumPerReach() <= 0) { int radius = tunnelRadius;
return tunnelRadius; if (caves.getMode() != IrisRiverCaveMode.SEALED && caves.getMaximumPerReach() > 0) {
radius = Math.max(radius, planningHalo(caves));
} }
return Math.max(tunnelRadius, planningHalo(caves)); IrisRiverDeepPools deepPools = caves.getDeepPools();
if (deepPools != null && deepPools.isEnabled() && deepPools.getMaximumPerReach() > 0) {
radius = Math.max(radius, deepPoolPlanningHalo(deepPools));
}
return radius;
} }
static int candidateHalo(IrisRiverCaves caves) { static int candidateHalo(IrisRiverCaves caves) {
return cavePublicationRadius(caves) * 3; return cavePublicationRadius(caves) * 3;
} }
static int deepPoolPlanningHalo(IrisRiverDeepPools deepPools) {
return deepPoolPublicationRadius(deepPools) * 4;
}
static int deepPoolCandidateHalo(IrisRiverDeepPools deepPools) {
return deepPoolPublicationRadius(deepPools) * 3;
}
static int deepPoolPublicationRadius(IrisRiverDeepPools deepPools) {
return deepPools.getSearchRadius() + deepPools.getHorizontalRadius() + 1;
}
static int cavePublicationRadius(IrisRiverCaves caves) { static int cavePublicationRadius(IrisRiverCaves caves) {
int generatedRadius = generatedGrottoPublicationRadius(caves); int generatedRadius = generatedGrottoPublicationRadius(caves);
return switch (caves.getMode()) { return switch (caves.getMode()) {
@@ -296,13 +653,13 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
boolean changed; boolean changed;
do { do {
changed = false; changed = false;
Set<CavePosition> candidateActions = mergeActions(containedColumns).keySet(); Long2ObjectOpenHashMap<TunnelColumn> candidateColumns = indexColumns(containedColumns);
for (int index = containedColumns.size() - 1; index >= 0; index--) { for (int index = containedColumns.size() - 1; index >= 0; index--) {
TunnelColumn column = containedColumns.get(index); TunnelColumn column = containedColumns.get(index);
if (!isTunnelColumnContained( if (!isTunnelColumnContained(
view, view,
column, column,
candidateActions, candidateColumns,
dryHeadroom, dryHeadroom,
surfaceSampler surfaceSampler
)) { )) {
@@ -348,7 +705,10 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
)); ));
} }
} }
return new TunnelPlan(Map.copyOf(actions), Map.copyOf(preconditions)); return new TunnelPlan(
Collections.unmodifiableMap(actions),
Collections.unmodifiableMap(preconditions)
);
} }
private static TunnelColumn createTunnelColumn( private static TunnelColumn createTunnelColumn(
@@ -360,8 +720,9 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
if (sample == null) { if (sample == null) {
return null; return null;
} }
LinkedHashMap<CavePosition, RiverCaveAction> actions = new LinkedHashMap<>(); int minimumY = sample.bedY() + 1;
for (int y = sample.bedY() + 1; y <= sample.ceilingY(); y++) { int maximumY = sample.ceilingY();
for (int y = minimumY; y <= maximumY; y++) {
CavePosition position = new CavePosition(x, y, z); CavePosition position = new CavePosition(x, y, z);
RiverCaveAction action = y <= sample.waterHeadY() RiverCaveAction action = y <= sample.waterHeadY()
? RiverCaveAction.WET_SOURCE ? RiverCaveAction.WET_SOURCE
@@ -371,22 +732,24 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
&& !matchesPublishedAction(view, position, action))) { && !matchesPublishedAction(view, position, action))) {
return null; return null;
} }
actions.put(position, action);
} }
return actions.isEmpty() ? null : new TunnelColumn(actions); return minimumY > maximumY
? null
: new TunnelColumn(x, z, minimumY, sample.waterHeadY(), maximumY);
} }
private static boolean isTunnelColumnContained( private static boolean isTunnelColumnContained(
CaveVoxelView view, CaveVoxelView view,
TunnelColumn column, TunnelColumn column,
Set<CavePosition> candidateActions, Long2ObjectOpenHashMap<TunnelColumn> candidateColumns,
int dryHeadroom, int dryHeadroom,
SurfaceSampler surfaceSampler SurfaceSampler surfaceSampler
) { ) {
for (CavePosition position : column.actions().keySet()) { for (int y = column.minimumY(); y <= column.maximumY(); y++) {
CavePosition position = new CavePosition(column.x(), y, column.z());
for (int[] offset : NEIGHBORS) { for (int[] offset : NEIGHBORS) {
CavePosition neighbor = offset(position, offset); CavePosition neighbor = offset(position, offset);
if (candidateActions.contains(neighbor)) { if (containsAction(candidateColumns, neighbor)) {
continue; continue;
} }
if (!view.isInWorld(neighbor)) { if (!view.isInWorld(neighbor)) {
@@ -417,7 +780,9 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
SurfaceSampler surfaceSampler SurfaceSampler surfaceSampler
) { ) {
IrisRiverSurfaceSample sample = surfaceSampler.sample(position.x(), position.z()); IrisRiverSurfaceSample sample = surfaceSampler.sample(position.x(), position.z());
if (!isWetChannelBed(sample) || sample.subterranean()) { if (!sample.river().present()
|| sample.river().state() != RiverRouteState.WET
|| sample.subterranean()) {
return false; return false;
} }
int bedY = (int) Math.round(sample.terrainHeight()); int bedY = (int) Math.round(sample.terrainHeight());
@@ -428,18 +793,48 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
private static Map<CavePosition, RiverCaveAction> mergeActions(List<TunnelColumn> columns) { private static Map<CavePosition, RiverCaveAction> mergeActions(List<TunnelColumn> columns) {
LinkedHashMap<CavePosition, RiverCaveAction> actions = new LinkedHashMap<>(); LinkedHashMap<CavePosition, RiverCaveAction> actions = new LinkedHashMap<>();
for (TunnelColumn column : columns) { for (TunnelColumn column : columns) {
actions.putAll(column.actions()); for (int y = column.minimumY(); y <= column.maximumY(); y++) {
actions.put(
new CavePosition(column.x(), y, column.z()),
y <= column.waterHeadY()
? RiverCaveAction.WET_SOURCE
: RiverCaveAction.DRY_AIR
);
}
} }
return actions; return actions;
} }
private static Long2ObjectOpenHashMap<TunnelColumn> indexColumns(List<TunnelColumn> columns) {
Long2ObjectOpenHashMap<TunnelColumn> indexed = new Long2ObjectOpenHashMap<>(columns.size());
for (TunnelColumn column : columns) {
indexed.put(Cache.key(column.x(), column.z()), column);
}
return indexed;
}
private static boolean containsAction(
Long2ObjectOpenHashMap<TunnelColumn> columns,
CavePosition position
) {
TunnelColumn column = columns.get(Cache.key(position.x(), position.z()));
return column != null
&& position.y() >= column.minimumY()
&& position.y() <= column.maximumY();
}
private static boolean matchesPublishedAction( private static boolean matchesPublishedAction(
CaveVoxelView view, CaveVoxelView view,
CavePosition position, CavePosition position,
RiverCaveAction action RiverCaveAction action
) { ) {
return view instanceof TunnelVoxelView tunnelView if (!(view instanceof TunnelVoxelView tunnelView)) {
&& tunnelView.riverActionAt(position) == action; return false;
}
RiverCaveHydrology hydrology = tunnelView.riverHydrologyAt(position);
return hydrology != null
&& hydrology.action() == action
&& hydrology.fluidKind() == RiverCaveFluidKind.RIVER;
} }
private static CavePosition offset(CavePosition position, int[] offset) { private static CavePosition offset(CavePosition position, int[] offset) {
@@ -450,15 +845,39 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
); );
} }
private MantleRiverCaveVoxelView createView(MantleWriter writer, ChunkContext context) { private MantleRiverCaveVoxelView createView(
MantleWriter writer,
ChunkContext context,
RiverCaveFluidKind fluidKind
) {
return new MantleRiverCaveVoxelView( return new MantleRiverCaveVoxelView(
writer.getMantle(), writer.getMantle(),
writer.getMantle().getWorldHeight(), writer.getMantle().getWorldHeight(),
(x, z) -> context.getComplex().getRoundedHeighteightStream().get(x, z), (x, z) -> context.getComplex().getRoundedHeighteightStream().get(x, z),
(x, z) -> context.getComplex().getFluidStream().get(x, z) (x, z) -> context.getComplex().resolveRiverCaveFluid(fluidKind, x, z),
fluidKind,
(chunkX, chunkZ) -> generateCarvingInput(writer, context, chunkX, chunkZ)
); );
} }
private void generateCarvingInput(
MantleWriter writer,
ChunkContext context,
int chunkX,
int chunkZ
) {
MantleComponent carving = getEngineMantle().getRegisteredComponents().get(ReservedFlag.CARVED);
if (carving == null || !carving.isEnabled()) {
throw new IllegalStateException("River hydrology requires the carving component");
}
MantleChunk<Matter> chunk = writer.acquireChunk(chunkX, chunkZ);
if (chunk == null) {
throw new IllegalStateException("River hydrology read exceeded the prepared mantle radius at "
+ chunkX + "," + chunkZ);
}
chunk.raiseFlagSuspend(ReservedFlag.CARVED, () -> carving.generateLayer(writer, chunkX, chunkZ, context));
}
private void publishTunnels( private void publishTunnels(
MantleWriter writer, MantleWriter writer,
ChunkContext context, ChunkContext context,
@@ -467,7 +886,11 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
int chunkZ int chunkZ
) { ) {
for (int attempt = 0; attempt < 2; attempt++) { for (int attempt = 0; attempt < 2; attempt++) {
MantleRiverCaveVoxelView view = createView(writer, context); MantleRiverCaveVoxelView view = createView(
writer,
context,
RiverCaveFluidKind.RIVER
);
TunnelPlan plan = planTunnels( TunnelPlan plan = planTunnels(
view, view,
chunkX, chunkX,
@@ -478,7 +901,11 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
runtime::sampleTunnel, runtime::sampleTunnel,
runtime::sample runtime::sample
); );
MantleRiverCaveVoxelView revalidationView = createView(writer, context); MantleRiverCaveVoxelView revalidationView = createView(
writer,
context,
RiverCaveFluidKind.RIVER
);
if (preconditionsHold(revalidationView, plan.preconditions())) { if (preconditionsHold(revalidationView, plan.preconditions())) {
publishTunnelLocal(writer, chunkX, chunkZ, plan); publishTunnelLocal(writer, chunkX, chunkZ, plan);
return; return;
@@ -505,7 +932,8 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
data, data,
caves.getGrottoShapeStyle(), caves.getGrottoShapeStyle(),
caves.getGrottoWarpStyle(), caves.getGrottoWarpStyle(),
caves.getGrottoWarpStrength() caves.getGrottoWarpStrength(),
0.2D
), ),
caves.getMaxFloodRadius(), caves.getMaxFloodRadius(),
caves.getMaxFloodDepth() caves.getMaxFloodDepth()
@@ -673,7 +1101,8 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
int chunkX, int chunkX,
int chunkZ, int chunkZ,
RiverCavePlanningResult result, RiverCavePlanningResult result,
Map<Long, String> floodedBiomes Map<Long, String> floodedBiomes,
RiverCaveFluidKind fluidKind
) { ) {
Map<CavePosition, RiverCaveSource> owners = actionOwners(result); Map<CavePosition, RiverCaveSource> owners = actionOwners(result);
ArrayList<Map.Entry<CavePosition, RiverCaveAction>> actions = new ArrayList<>(result.actions().entrySet()); ArrayList<Map.Entry<CavePosition, RiverCaveAction>> actions = new ArrayList<>(result.actions().entrySet());
@@ -692,7 +1121,7 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
position.x(), position.x(),
position.y(), position.y(),
position.z(), position.z(),
new RiverCaveHydrology(entry.getValue(), biome) new RiverCaveHydrology(entry.getValue(), biome, fluidKind)
); );
} }
} }
@@ -708,7 +1137,12 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
for (Map.Entry<CavePosition, RiverCaveAction> entry : actions) { for (Map.Entry<CavePosition, RiverCaveAction> entry : actions) {
CavePosition position = entry.getKey(); CavePosition position = entry.getKey();
if (owns(chunkX, chunkZ, position)) { if (owns(chunkX, chunkZ, position)) {
writer.setData(position.x(), position.y(), position.z(), RiverCaveHydrology.of(entry.getValue())); writer.setData(
position.x(),
position.y(),
position.z(),
RiverCaveHydrology.of(entry.getValue(), RiverCaveFluidKind.RIVER)
);
} }
} }
} }
@@ -749,7 +1183,7 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
} }
interface TunnelVoxelView extends CaveVoxelView { interface TunnelVoxelView extends CaveVoxelView {
RiverCaveAction riverActionAt(CavePosition position); RiverCaveHydrology riverHydrologyAt(CavePosition position);
} }
record TunnelPlan( record TunnelPlan(
@@ -761,6 +1195,12 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
} }
} }
private record TunnelColumn(Map<CavePosition, RiverCaveAction> actions) { private record TunnelColumn(
int x,
int z,
int minimumY,
int waterHeadY,
int maximumY
) {
} }
} }
@@ -57,7 +57,11 @@ public class ModeOverworld extends IrisEngineMode implements EngineMode {
if (shouldBypassMantleStages()) { if (shouldBypassMantleStages()) {
return; return;
} }
generateMatter(x >> 4, z >> 4, m, c); generateMatter(
x >> 4,
z >> 4,
m || getEngine().isStudio(),
c);
}; };
EngineStage sTerrain = (x, z, k, p, m, c) -> terrain.actuate(x, z, k, m, c); EngineStage sTerrain = (x, z, k, p, m, c) -> terrain.actuate(x, z, k, m, c);
EngineStage sDecorant = (x, z, k, p, m, c) -> decorant.actuate(x, z, k, m, c); EngineStage sDecorant = (x, z, k, p, m, c) -> decorant.actuate(x, z, k, m, c);
@@ -117,7 +117,9 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
columnMasks, columnMasks,
upperSurfaceHeights, upperSurfaceHeights,
worldHeightSpan, worldHeightSpan,
caveLavaHeight caveLavaHeight,
chunkBlockX,
chunkBlockZ
); );
CarveResolver carveResolver = new CarveResolver(resolutionContext); CarveResolver carveResolver = new CarveResolver(resolutionContext);
mantleChunk.iterate(MatterCavern.class, (xx, yy, zz, cavern) -> carveResolver.apply( mantleChunk.iterate(MatterCavern.class, (xx, yy, zz, cavern) -> carveResolver.apply(
@@ -166,8 +168,6 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
}); });
for (int columnIndex = 0; columnIndex < 256; columnIndex++) { for (int columnIndex = 0; columnIndex < 256; columnIndex++) {
int localX = PowerOfTwoCoordinates.unpackLocal16X(columnIndex);
int localZ = columnIndex & 15;
processColumnFromMask( processColumnFromMask(
output, output,
mantleChunk, mantleChunk,
@@ -178,8 +178,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
z, z,
resolverState, resolverState,
caveBiomeCache, caveBiomeCache,
customBiomeCache, customBiomeCache
context.getFluid().get(localX, localZ)
); );
} }
@@ -279,7 +278,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
} }
return switch (hydrology.action()) { return switch (hydrology.action()) {
case WET_SOURCE -> fluid; case WET_SOURCE -> fluid;
case FALLING_WATER -> fallingFluidState(fluid); case FALLING_FLUID -> fallingFluidState(fluid);
case DRY_AIR -> air; case DRY_AIR -> air;
case SEAL_GUARD -> normalizeWaterlogging(current, null); case SEAL_GUARD -> normalizeWaterlogging(current, null);
}; };
@@ -369,9 +368,16 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
return; return;
} }
PlatformBlockState fluid = isFluidIntent(cavern) PlatformBlockState fluid = null;
? context.chunkContext().getFluid().get(localX, localZ) if (isFluidIntent(cavern)) {
: null; fluid = hydrology == null
? context.chunkContext().getFluid().get(localX, localZ)
: getComplex().resolveRiverCaveFluid(
hydrology.fluidKind(),
context.chunkBlockX() + localX,
context.chunkBlockZ() + localZ
);
}
if (hydrology != null) { if (hydrology != null) {
context.output().setRaw(localX, y, localZ, context.output().setRaw(localX, y, localZ,
resolveHydrologyState(hydrology, current, fluid, AIR)); resolveHydrologyState(hydrology, current, fluid, AIR));
@@ -395,7 +401,9 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
CarveColumnMask[] columnMasks, CarveColumnMask[] columnMasks,
int[] upperSurfaceHeights, int[] upperSurfaceHeights,
int worldHeightSpan, int worldHeightSpan,
int caveLavaHeight int caveLavaHeight,
int chunkBlockX,
int chunkBlockZ
) { ) {
} }
@@ -572,8 +580,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
int chunkZ, int chunkZ,
IrisDimensionCarvingResolver.State resolverState, IrisDimensionCarvingResolver.State resolverState,
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache, Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache,
Map<String, IrisBiome> customBiomeCache, Map<String, IrisBiome> customBiomeCache
PlatformBlockState columnFluid
) { ) {
if (columnMask == null || columnMask.isEmpty()) { if (columnMask == null || columnMask.isEmpty()) {
return; return;
@@ -601,7 +608,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
} else { } else {
if (zone.isValid(getEngine())) { if (zone.isValid(getEngine())) {
processZone(output, mc, mantle, zone, rx, rz, worldX, worldZ, resolverState, processZone(output, mc, mantle, zone, rx, rz, worldX, worldZ, resolverState,
caveBiomeCache, customBiomeCache, columnFluid); caveBiomeCache, customBiomeCache);
} }
zone = new CaveZone(); zone = new CaveZone();
zone.setFloor(y); zone.setFloor(y);
@@ -614,7 +621,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
if (zone.isValid(getEngine())) { if (zone.isValid(getEngine())) {
processZone(output, mc, mantle, zone, rx, rz, worldX, worldZ, resolverState, processZone(output, mc, mantle, zone, rx, rz, worldX, worldZ, resolverState,
caveBiomeCache, customBiomeCache, columnFluid); caveBiomeCache, customBiomeCache);
} }
} }
@@ -767,8 +774,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
CaveZone zone, int rx, int rz, int xx, int zz, CaveZone zone, int rx, int rz, int xx, int zz,
IrisDimensionCarvingResolver.State resolverState, IrisDimensionCarvingResolver.State resolverState,
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache, Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache,
Map<String, IrisBiome> customBiomeCache, Map<String, IrisBiome> customBiomeCache) {
PlatformBlockState columnFluid) {
int maxY = output.getHeight(); int maxY = output.getHeight();
if (zone.ceiling + 1 < maxY && B.isDecorant(output.getRaw(rx, zone.ceiling + 1, rz))) { if (zone.ceiling + 1 < maxY && B.isDecorant(output.getRaw(rx, zone.ceiling + 1, rz))) {
@@ -793,7 +799,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
IrisBiome floorBiome = resolveCaveBoundaryBiome(mc, rx, zone.floor, rz, xx, zz, resolverState, caveBiomeCache, customBiomeCache); IrisBiome floorBiome = resolveCaveBoundaryBiome(mc, rx, zone.floor, rz, xx, zz, resolverState, caveBiomeCache, customBiomeCache);
IrisBiome ceilingBiome = resolveCaveBoundaryBiome(mc, rx, zone.ceiling, rz, xx, zz, resolverState, caveBiomeCache, customBiomeCache); IrisBiome ceilingBiome = resolveCaveBoundaryBiome(mc, rx, zone.ceiling, rz, xx, zz, resolverState, caveBiomeCache, customBiomeCache);
if (floorBiome == null && ceilingBiome == null) { if (floorBiome == null && ceilingBiome == null) {
normalizeCaveZoneWaterlogging(output, mc, zone, rx, rz, columnFluid); normalizeCaveZoneWaterlogging(output, mc, zone, rx, rz, xx, zz);
return; return;
} }
@@ -865,7 +871,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
decorant.getCeilingDecorator().decorate(rx, rz, xx, xx, xx, zz, zz, zz, output, ceilingBiome, InferredType.CAVE, zone.getCeiling(), zone.airThickness()); decorant.getCeilingDecorator().decorate(rx, rz, xx, xx, xx, zz, zz, zz, output, ceilingBiome, InferredType.CAVE, zone.getCeiling(), zone.airThickness());
} }
normalizeCaveZoneWaterlogging(output, mc, zone, rx, rz, columnFluid); normalizeCaveZoneWaterlogging(output, mc, zone, rx, rz, xx, zz);
} }
private void normalizeCaveZoneWaterlogging( private void normalizeCaveZoneWaterlogging(
@@ -874,7 +880,8 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
CaveZone zone, CaveZone zone,
int localX, int localX,
int localZ, int localZ,
PlatformBlockState columnFluid int worldX,
int worldZ
) { ) {
int minimumY = Math.max(0, zone.floor - 1); int minimumY = Math.max(0, zone.floor - 1);
int maximumY = Math.min(output.getHeight() - 1, zone.ceiling + 1); int maximumY = Math.min(output.getHeight() - 1, zone.ceiling + 1);
@@ -887,6 +894,11 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
MatterCavern baseline = dataIfPresent( MatterCavern baseline = dataIfPresent(
mantleChunk, localX, y, localZ, MatterCavern.class); mantleChunk, localX, y, localZ, MatterCavern.class);
PlatformBlockState current = output.getRaw(localX, y, localZ); PlatformBlockState current = output.getRaw(localX, y, localZ);
PlatformBlockState columnFluid = getComplex().resolveRiverCaveFluid(
hydrology.fluidKind(),
worldX,
worldZ
);
PlatformBlockState normalized = normalizeHydrologyWaterlogging( PlatformBlockState normalized = normalizeHydrologyWaterlogging(
current, current,
baseline, baseline,
@@ -10,7 +10,10 @@ import lombok.experimental.Accessors;
@Desc("A two-dimensional image-map coordinate") @Desc("A two-dimensional image-map coordinate")
@Data @Data
public class IrisImageMapOrigin { public class IrisImageMapOrigin {
@Desc("X coordinate in world blocks for origin, or source pixels for sourceOrigin")
private double x = 0D; private double x = 0D;
@Desc("Z coordinate in world blocks for origin, or source image Y pixels for sourceOrigin")
private double z = 0D; private double z = 0D;
public IrisImageMapOrigin(double x, double z) { public IrisImageMapOrigin(double x, double z) {
@@ -25,6 +25,11 @@ import art.arcane.iris.util.common.math.IrisBlockVector;
import art.arcane.iris.util.common.math.IrisVector; import art.arcane.iris.util.common.math.IrisVector;
import art.arcane.iris.util.project.interpolation.Interpolation3D; import art.arcane.iris.util.project.interpolation.Interpolation3D;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
/** /**
* Geometric transforms for {@link IrisObject}: rotation, scaling and the interpolated upscalers. * Geometric transforms for {@link IrisObject}: rotation, scaling and the interpolated upscalers.
*/ */
@@ -133,6 +138,7 @@ final class IrisObjectTransforms {
VectorMap<PlatformBlockState> b = new VectorMap<>(); VectorMap<PlatformBlockState> b = new VectorMap<>();
IrisPosition min = self.getAABB().min(); IrisPosition min = self.getAABB().min();
IrisPosition max = self.getAABB().max(); IrisPosition max = self.getAABB().max();
NearestBlockIndex nearestBlocks = NearestBlockIndex.create(v);
for (int x = min.getX(); x <= max.getX(); x++) { for (int x = min.getX(); x <= max.getX(); x++) {
for (int y = min.getY(); y <= max.getY(); y++) { for (int y = min.getY(); y <= max.getY(); y++) {
@@ -146,7 +152,7 @@ final class IrisObjectTransforms {
return 1; return 1;
}) >= 0.5) { }) >= 0.5) {
b.put(new IrisBlockVector(x, y, z), nearestBlockData(self, x, y, z)); b.put(new IrisBlockVector(x, y, z), nearestBlockData(v, nearestBlocks, x, y, z));
} else { } else {
b.put(new IrisBlockVector(x, y, z), IrisObject.States.AIR); b.put(new IrisBlockVector(x, y, z), IrisObject.States.AIR);
} }
@@ -169,6 +175,7 @@ final class IrisObjectTransforms {
VectorMap<PlatformBlockState> b = new VectorMap<>(); VectorMap<PlatformBlockState> b = new VectorMap<>();
IrisPosition min = self.getAABB().min(); IrisPosition min = self.getAABB().min();
IrisPosition max = self.getAABB().max(); IrisPosition max = self.getAABB().max();
NearestBlockIndex nearestBlocks = NearestBlockIndex.create(v);
for (int x = min.getX(); x <= max.getX(); x++) { for (int x = min.getX(); x <= max.getX(); x++) {
for (int y = min.getY(); y <= max.getY(); y++) { for (int y = min.getY(); y <= max.getY(); y++) {
@@ -182,7 +189,7 @@ final class IrisObjectTransforms {
return 1; return 1;
}) >= 0.5) { }) >= 0.5) {
b.put(new IrisBlockVector(x, y, z), nearestBlockData(self, x, y, z)); b.put(new IrisBlockVector(x, y, z), nearestBlockData(v, nearestBlocks, x, y, z));
} else { } else {
b.put(new IrisBlockVector(x, y, z), IrisObject.States.AIR); b.put(new IrisBlockVector(x, y, z), IrisObject.States.AIR);
} }
@@ -209,6 +216,7 @@ final class IrisObjectTransforms {
VectorMap<PlatformBlockState> b = new VectorMap<>(); VectorMap<PlatformBlockState> b = new VectorMap<>();
IrisPosition min = self.getAABB().min(); IrisPosition min = self.getAABB().min();
IrisPosition max = self.getAABB().max(); IrisPosition max = self.getAABB().max();
NearestBlockIndex nearestBlocks = NearestBlockIndex.create(v);
for (int x = min.getX(); x <= max.getX(); x++) { for (int x = min.getX(); x <= max.getX(); x++) {
for (int y = min.getY(); y <= max.getY(); y++) { for (int y = min.getY(); y <= max.getY(); y++) {
@@ -222,7 +230,7 @@ final class IrisObjectTransforms {
return 1; return 1;
}, tension, bias) >= 0.5) { }, tension, bias) >= 0.5) {
b.put(new IrisBlockVector(x, y, z), nearestBlockData(self, x, y, z)); b.put(new IrisBlockVector(x, y, z), nearestBlockData(v, nearestBlocks, x, y, z));
} else { } else {
b.put(new IrisBlockVector(x, y, z), IrisObject.States.AIR); b.put(new IrisBlockVector(x, y, z), IrisObject.States.AIR);
} }
@@ -238,36 +246,124 @@ final class IrisObjectTransforms {
} }
} }
private static PlatformBlockState nearestBlockData(IrisObject self, int x, int y, int z) { private static PlatformBlockState nearestBlockData(VectorMap<PlatformBlockState> blocks,
NearestBlockIndex nearestBlocks,
int x, int y, int z) {
IrisBlockVector vv = new IrisBlockVector(x, y, z); IrisBlockVector vv = new IrisBlockVector(x, y, z);
self.readLock.lock(); PlatformBlockState direct = blocks.get(vv);
try { if (!B.isAir(direct)) {
PlatformBlockState r = self.blocks.get(vv); return direct;
}
return nearestBlocks.nearest(x, y, z, direct);
}
if (!B.isAir(r)) { static final class NearestBlockIndex {
return r; private static final Comparator<NearestBlock> X_ORDER = Comparator
.comparingInt(NearestBlock::x)
.thenComparingInt(NearestBlock::rank);
private static final Comparator<NearestBlock> Y_ORDER = Comparator
.comparingInt(NearestBlock::y)
.thenComparingInt(NearestBlock::rank);
private static final Comparator<NearestBlock> Z_ORDER = Comparator
.comparingInt(NearestBlock::z)
.thenComparingInt(NearestBlock::rank);
private final NearestNode root;
private PlatformBlockState bestState;
private double bestDistance;
private int bestRank;
private NearestBlockIndex(NearestNode root) {
this.root = root;
}
static NearestBlockIndex create(VectorMap<PlatformBlockState> blocks) {
List<NearestBlock> points = new ArrayList<>(blocks.size());
VectorMap<PlatformBlockState>.Cursor cursor = blocks.cursor();
int rank = 0;
while (cursor.next()) {
PlatformBlockState state = cursor.value();
if (!B.isAir(state)) {
IrisBlockVector position = cursor.key();
points.add(new NearestBlock(position.getBlockX(), position.getBlockY(), position.getBlockZ(),
rank, state));
}
rank++;
} }
double d = Double.MAX_VALUE; NearestBlock[] pointArray = points.toArray(new NearestBlock[0]);
return new NearestBlockIndex(build(pointArray, 0, pointArray.length, 0));
}
for (var entry : self.blocks) { PlatformBlockState nearest(int x, int y, int z, PlatformBlockState fallback) {
PlatformBlockState dat = entry.getValue(); if (root == null) {
return fallback;
if (B.isAir(dat)) {
continue;
}
double dx = entry.getKey().distanceSquared(vv);
if (dx < d) {
d = dx;
r = dat;
}
} }
return r; bestState = fallback;
} finally { bestDistance = Double.MAX_VALUE;
self.readLock.unlock(); bestRank = Integer.MAX_VALUE;
search(root, x, y, z);
return bestState;
}
private static NearestNode build(NearestBlock[] points, int from, int to, int depth) {
if (from >= to) {
return null;
}
int axis = depth % 3;
Arrays.sort(points, from, to, comparator(axis));
int middle = (from + to) >>> 1;
return new NearestNode(
points[middle],
axis,
build(points, from, middle, depth + 1),
build(points, middle + 1, to, depth + 1)
);
}
private static Comparator<NearestBlock> comparator(int axis) {
return switch (axis) {
case 0 -> X_ORDER;
case 1 -> Y_ORDER;
default -> Z_ORDER;
};
}
private void search(NearestNode node, int x, int y, int z) {
if (node == null) {
return;
}
NearestBlock point = node.point();
double xDistance = point.x() - x;
double yDistance = point.y() - y;
double zDistance = point.z() - z;
double distance = (xDistance * xDistance) + (yDistance * yDistance) + (zDistance * zDistance);
if (distance < bestDistance || (distance == bestDistance && point.rank() < bestRank)) {
bestState = point.state();
bestDistance = distance;
bestRank = point.rank();
}
double axisDistance = switch (node.axis()) {
case 0 -> x - point.x();
case 1 -> y - point.y();
default -> z - point.z();
};
NearestNode near = axisDistance <= 0D ? node.lower() : node.upper();
NearestNode far = axisDistance <= 0D ? node.upper() : node.lower();
search(near, x, y, z);
if ((axisDistance * axisDistance) <= bestDistance) {
search(far, x, y, z);
}
} }
} }
private record NearestNode(NearestBlock point, int axis, NearestNode lower, NearestNode upper) {
}
private record NearestBlock(int x, int y, int z, int rank, PlatformBlockState state) {
}
} }
@@ -97,4 +97,7 @@ public class IrisRiverCaves {
@Desc("The policy for fluid already present in a candidate contained cave body.") @Desc("The policy for fluid already present in a candidate contained cave body.")
private IrisRiverExistingFluidPolicy existingFluidPolicy = IrisRiverExistingFluidPolicy.REJECT; private IrisRiverExistingFluidPolicy existingFluidPolicy = IrisRiverExistingFluidPolicy.REJECT;
@Desc("Sparse, independently filled cave-floor pools generated at deep river-network anchors.")
private IrisRiverDeepPools deepPools = new IrisRiverDeepPools();
} }
@@ -0,0 +1,92 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.object.annotations.Desc;
import art.arcane.iris.engine.object.annotations.MaxNumber;
import art.arcane.iris.engine.object.annotations.MinNumber;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Accessors(chain = true)
@NoArgsConstructor
@Desc("Controls sparse, river-anchored fluid pools attached to deep cave floors.")
@Data
public class IrisRiverDeepPools {
@Desc("Enables independently configured deep cave pools along eligible wet river reaches.")
private boolean enabled = false;
@Desc("Selects complete wet river reaches that may host deep pools.")
private IrisRiverNoiseChance reach = new IrisRiverNoiseChance()
.setChance(1D / 3D)
.setStyle(new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(4096D))
.setInfluence(0.08D);
@MinNumber(16)
@MaxNumber(4096)
@Desc("The minimum distance in blocks between deep-pool candidates.")
private int minimumSpacing = 768;
@MinNumber(0)
@MaxNumber(16)
@Desc("The maximum accepted deep pools on one river reach.")
private int maximumPerReach = 1;
@MinNumber(-2048)
@MaxNumber(2048)
@Desc("The lowest absolute world Y considered for the pool fluid surface.")
private int minimumFluidY = -224;
@MinNumber(-2048)
@MaxNumber(2048)
@Desc("The highest absolute world Y considered for the pool fluid surface.")
private int maximumFluidY = -104;
@MinNumber(0)
@MaxNumber(256)
@Desc("The horizontal distance searched from a river anchor for a contained cave floor.")
private int searchRadius = 16;
@MinNumber(1)
@MaxNumber(64)
@Desc("The number of deterministic nearby columns tested for a contained cave floor.")
private int searchAttempts = 12;
@MinNumber(2)
@MaxNumber(128)
@Desc("The horizontal radius of the generated deep-pool chamber.")
private int horizontalRadius = 18;
@MinNumber(2)
@MaxNumber(64)
@Desc("The vertical radius of the generated deep-pool chamber.")
private int verticalRadius = 8;
@MinNumber(1)
@MaxNumber(63)
@Desc("The dry chamber height retained above the deep-pool fluid surface.")
private int dryHeadroom = 4;
@Desc("Noise shaping the deep-pool chamber boundary.")
private IrisGeneratorStyle shapeStyle = new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(12D);
@MinNumber(0)
@MaxNumber(0.75)
@Desc("The proportional noise displacement applied to the deep-pool chamber boundary.")
private double shapeVariation = 0.5D;
@Desc("Noise warping the deep-pool chamber coordinate field.")
private IrisGeneratorStyle warpStyle = new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(24D);
@MinNumber(0)
@MaxNumber(64)
@Desc("The maximum coordinate warp applied to the deep-pool chamber in blocks.")
private double warpStrength = 6D;
@MinNumber(64)
@MaxNumber(1048576)
@Desc("The greatest generated deep-pool chamber volume that may be transactionally published.")
private int maximumVolume = 32768;
@Desc("The fluid palette used only by accepted deep pools.")
private IrisMaterialPalette fluidPalette = new IrisMaterialPalette().qclear().qadd("lava");
}
@@ -24,6 +24,11 @@ public class IrisRiverTerrain {
@Desc("The wet-bed depth below the local water surface, or dry-channel depth below natural terrain, in blocks.") @Desc("The wet-bed depth below the local water surface, or dry-channel depth below natural terrain, in blocks.")
private IrisStyledRange depth = range(2D, 7D, NoiseStyle.IRIS, 768D); private IrisStyledRange depth = range(2D, 7D, NoiseStyle.IRIS, 768D);
@MinNumber(0)
@MaxNumber(64)
@Desc("The radius added to every channel after worm, regional, local, and stream-order shaping.")
private double channelRadiusBonus = 0D;
@MinNumber(1) @MinNumber(1)
@MaxNumber(2048) @MaxNumber(2048)
@Desc("The final wet-channel width cap after region, biome, and stream-order scaling.") @Desc("The final wet-channel width cap after region, biome, and stream-order scaling.")
@@ -64,7 +69,7 @@ public class IrisRiverTerrain {
@MinNumber(0) @MinNumber(0)
@MaxNumber(16) @MaxNumber(16)
@Desc("The extra lateral tunnel-mouth blend carved on each side where a surface river enters or exits solid terrain.") @Desc("The longitudinal transition length and maximum lateral and roof flare where a surface river enters or exits solid terrain.")
private double tunnelMouthBlend = 2D; private double tunnelMouthBlend = 2D;
@Desc("Noise modulating the submerged floor of river tunnels.") @Desc("Noise modulating the submerged floor of river tunnels.")
@@ -13,7 +13,15 @@ import lombok.experimental.Accessors;
@Data @Data
public class IrisRiverWater { public class IrisRiverWater {
@Desc("The strategy used to determine river water-surface height.") @Desc("The strategy used to determine river water-surface height.")
private IrisRiverWaterMode mode = IrisRiverWaterMode.SEA_LEVEL; private IrisRiverWaterMode mode = IrisRiverWaterMode.FIXED;
@MinNumber(-2048)
@MaxNumber(2048)
@Desc("The base river fluid surface in absolute world Y, independent of the dimension ocean height.")
private int fluidHeight = 63;
@Desc("The river fluid palette used by surface channels, contained tunnels, grottos, and waterfall throats.")
private IrisMaterialPalette fluidPalette = new IrisMaterialPalette().qclear().qadd("water");
@MinNumber(8) @MinNumber(8)
@MaxNumber(4096) @MaxNumber(4096)
@@ -22,7 +30,7 @@ public class IrisRiverWater {
@MinNumber(0) @MinNumber(0)
@MaxNumber(64) @MaxNumber(64)
@Desc("The greatest river water height permitted above the dimension fluid height.") @Desc("The greatest terraced river height permitted above fluidHeight.")
private int maximumPoolRise = 4; private int maximumPoolRise = 4;
@MinNumber(1) @MinNumber(1)
@@ -4,8 +4,8 @@ import art.arcane.iris.engine.object.annotations.Desc;
@Desc("Selects how a river determines its surface fluid height.") @Desc("Selects how a river determines its surface fluid height.")
public enum IrisRiverWaterMode { public enum IrisRiverWaterMode {
@Desc("Use the dimension fluid height for every wet river reach.") @Desc("Use the river water configuration's fixed fluid height for every wet reach.")
SEA_LEVEL, FIXED,
@Desc("Use flat pools connected by controlled vertical drops.") @Desc("Use flat pools connected by controlled vertical drops.")
TERRACED TERRACED
@@ -72,16 +72,21 @@ public class IrisRiverWorm {
@Desc("Depth multiplier for reaches selecting this worm.") @Desc("Depth multiplier for reaches selecting this worm.")
private double depthMultiplier = 1D; private double depthMultiplier = 1D;
@MinNumber(32) @MinNumber(8)
@MaxNumber(16384) @MaxNumber(16384)
@Desc("Primary world-space wavelength controlling longitudinal body swelling and pinching.") @Desc("Primary world-space wavelength controlling longitudinal body swelling and pinching.")
private double bodyWavelength = 512D; private double bodyWavelength = 512D;
@MinNumber(32) @MinNumber(8)
@MaxNumber(16384) @MaxNumber(16384)
@Desc("Detail wavelength adding smaller changes to the longitudinal body profile.") @Desc("Detail wavelength adding smaller changes to the longitudinal body profile.")
private double bodyDetailWavelength = 128D; private double bodyDetailWavelength = 128D;
@MinNumber(0)
@MaxNumber(1)
@Desc("Share of the longitudinal body field supplied by bodyDetailWavelength; the remainder uses bodyWavelength.")
private double bodyDetailInfluence = 0.3D;
@MinNumber(0) @MinNumber(0)
@MaxNumber(0.875) @MaxNumber(0.875)
@Desc("Maximum proportional channel-width variation along this style's body.") @Desc("Maximum proportional channel-width variation along this style's body.")
@@ -121,6 +121,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
private final AtomicInteger a = new AtomicInteger(0); private final AtomicInteger a = new AtomicInteger(0);
private volatile long lastChunkGenTime = 0L; private volatile long lastChunkGenTime = 0L;
private final CompletableFuture<Integer> spawnChunks = new CompletableFuture<>(); private final CompletableFuture<Integer> spawnChunks = new CompletableFuture<>();
private final CompletableFuture<Void> initialSpawnReady = new CompletableFuture<>();
private final AtomicCache<EngineTarget> targetCache = new AtomicCache<>(); private final AtomicCache<EngineTarget> targetCache = new AtomicCache<>();
private final AtomicReference<CompletableFuture<Void>> closeFuture = new AtomicReference<>(); private final AtomicReference<CompletableFuture<Void>> closeFuture = new AtomicReference<>();
private volatile Engine engine; private volatile Engine engine;
@@ -194,11 +195,19 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
engine.getPlatformHooks().applyWorldBoundary(engine); engine.getPlatformHooks().applyWorldBoundary(engine);
IrisLogging.debug("Injected Iris Biome Source into " + world.getName()); IrisLogging.debug("Injected Iris Biome Source into " + world.getName());
if (!studio) { if (!studio) {
J.s(() -> updateSpawnLocation(world), 1); J.sfut(() -> updateSpawnLocation(world), 1)
.whenComplete((ignored, failure) -> {
if (failure != null) {
initialSpawnReady.completeExceptionally(failure);
}
});
} else {
initialSpawnReady.complete(null);
} }
} catch (Throwable e) { } catch (Throwable e) {
initializationFailure = e; initializationFailure = e;
spawnChunks.completeExceptionally(e); spawnChunks.completeExceptionally(e);
initialSpawnReady.completeExceptionally(e);
IrisLogging.reportError(e); IrisLogging.reportError(e);
IrisLogging.error("Failed to initialize Iris generator for " + world.getName()); IrisLogging.error("Failed to initialize Iris generator for " + world.getName());
if (e instanceof RuntimeException runtimeException) { if (e instanceof RuntimeException runtimeException) {
@@ -231,16 +240,58 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
} }
private void updateSpawnLocation(World world) { private void updateSpawnLocation(World world) {
Location initialSpawn = getInitialSpawnLocation(world); try {
int chunkX = initialSpawn.getBlockX() >> 4; Location initialSpawn = getInitialSpawnLocation(world);
int chunkZ = initialSpawn.getBlockZ() >> 4; int chunkX = initialSpawn.getBlockX() >> 4;
CompletableFuture<Chunk> chunkFuture = requestChunkAsync(world, chunkX, chunkZ, true); int chunkZ = initialSpawn.getBlockZ() >> 4;
if (chunkFuture == null) { CompletableFuture<Chunk> chunkFuture = requestChunkAsync(world, chunkX, chunkZ, true);
return; if (chunkFuture == null) {
} initialSpawnReady.completeExceptionally(new IllegalStateException(
"Initial spawn chunk request returned no completion future for world \""
+ world.getName() + "\"."));
return;
}
chunkFuture.thenAccept(chunk -> chunkFuture.whenComplete((chunk, failure) -> {
J.runRegion(chunk.getWorld(), chunk.getX(), chunk.getZ(), () -> applySpawnLocation(chunk.getWorld(), initialSpawn))); try {
if (failure != null) {
initialSpawnReady.completeExceptionally(failure);
return;
}
if (chunk == null) {
throw new IllegalStateException(
"Initial spawn chunk request completed without a chunk for world \""
+ world.getName() + "\".");
}
J.runRegionFuture(
chunk.getWorld(),
chunk.getX(),
chunk.getZ(),
() -> completeSpawnLocation(chunk.getWorld(), initialSpawn))
.whenComplete((ignored, scheduleFailure) -> {
if (scheduleFailure != null) {
initialSpawnReady.completeExceptionally(scheduleFailure);
}
});
} catch (Throwable callbackFailure) {
initialSpawnReady.completeExceptionally(new IllegalStateException(
"Initial spawn preparation failed for world \""
+ world.getName() + "\".",
callbackFailure));
}
});
} catch (Throwable failure) {
initialSpawnReady.completeExceptionally(failure);
}
}
private void completeSpawnLocation(World world, Location initialSpawn) {
try {
applySpawnLocation(world, initialSpawn);
initialSpawnReady.complete(null);
} catch (Throwable failure) {
initialSpawnReady.completeExceptionally(failure);
}
} }
private void applySpawnLocation(World world, Location initialSpawn) { private void applySpawnLocation(World world, Location initialSpawn) {
@@ -255,10 +306,27 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
int minY = world.getMinHeight() + 1; int minY = world.getMinHeight() + 1;
int maxY = world.getMaxHeight() - 2; int maxY = world.getMaxHeight() - 2;
int y = Math.max(minY, Math.min(maxY, world.getHighestBlockYAt(initialSpawn) + 1)); int y = resolveInitialSpawnY(world, initialSpawn, minY, maxY);
world.setSpawnLocation(new Location(world, initialSpawn.getX(), y, initialSpawn.getZ(), initialSpawn.getYaw(), initialSpawn.getPitch())); world.setSpawnLocation(new Location(world, initialSpawn.getX(), y, initialSpawn.getZ(), initialSpawn.getYaw(), initialSpawn.getPitch()));
} }
private int resolveInitialSpawnY(World world, Location initialSpawn, int minY, int maxY) {
Engine activeEngine = engine;
if (activeEngine != null && activeEngine.getComplex() != null && activeEngine.getComplex().getHeightStream() != null) {
int generatedY = activeEngine.getMinHeight()
+ activeEngine.getComplex().getHeightStream().get(initialSpawn.getX(), initialSpawn.getZ()).intValue()
+ 1;
return Math.max(minY, Math.min(maxY, generatedY));
}
return Math.max(minY, Math.min(maxY, world.getHighestBlockYAt(initialSpawn) + 1));
}
@Override
public CompletableFuture<Void> getInitialSpawnReady() {
return initialSpawnReady;
}
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private CompletableFuture<Chunk> requestChunkAsync(World world, int chunkX, int chunkZ, boolean generate) { private CompletableFuture<Chunk> requestChunkAsync(World world, int chunkX, int chunkZ, boolean generate) {
try { try {
@@ -377,6 +445,11 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
this.hotloader = shouldRunStudioHotload(studio, closing, jigsawStudioActive) ? new Looper() { this.hotloader = shouldRunStudioHotload(studio, closing, jigsawStudioActive) ? new Looper() {
@Override @Override
protected long loop() { protected long loop() {
Engine activeEngine = engine;
if (activeEngine instanceof IrisEngine irisEngine
&& irisEngine.isGenerationCacheWarmPending()) {
return HOTLOAD_LOOP_DELAY_MS;
}
if (shouldThrottleHotload()) { if (shouldThrottleHotload()) {
return HOTLOAD_MAINTENANCE_DELAY_MS; return HOTLOAD_MAINTENANCE_DELAY_MS;
} }
@@ -76,4 +76,8 @@ public interface PlatformChunkGenerator extends Hotloadable, DataProvider {
} }
CompletableFuture<Integer> getSpawnChunks(); CompletableFuture<Integer> getSpawnChunks();
default CompletableFuture<Void> getInitialSpawnReady() {
return CompletableFuture.completedFuture(null);
}
} }
@@ -119,6 +119,15 @@ public final class RiverBodyProfile {
return roofScales[index]; return roofScales[index];
} }
public int intervalIndex(double alongReach) {
double position = StrictMath.max(0D, StrictMath.min(1D, alongReach));
int index = Arrays.binarySearch(positions, position);
if (index >= 0) {
return StrictMath.min(index, positions.length - 2);
}
return StrictMath.max(0, StrictMath.min(-index - 2, positions.length - 2));
}
@Override @Override
public boolean equals(Object object) { public boolean equals(Object object) {
if (this == object) { if (this == object) {
@@ -38,7 +38,7 @@ public final class RiverNetwork {
private static final long BRANCH_SLOT_SALT = 0x94D049BB133111EBL; private static final long BRANCH_SLOT_SALT = 0x94D049BB133111EBL;
private static final long BRANCH_GATE_SALT = 0x2545F4914F6CDD1DL; private static final long BRANCH_GATE_SALT = 0x2545F4914F6CDD1DL;
private static final int MINIMUM_BODY_PROFILE_SAMPLES = 12; private static final int MINIMUM_BODY_PROFILE_SAMPLES = 12;
private static final int MAXIMUM_BODY_PROFILE_SAMPLES = 32; private static final int MAXIMUM_BODY_PROFILE_SAMPLES = 512;
private static final double PERLIN_NORMALIZATION = 1.4142135623730951D; private static final double PERLIN_NORMALIZATION = 1.4142135623730951D;
private final RiverNetworkOptions options; private final RiverNetworkOptions options;
@@ -670,7 +670,8 @@ public final class RiverNetwork {
worm.bodyDetailWavelength(), worm.bodyDetailWavelength(),
worm.seed() ^ detailSalt worm.seed() ^ detailSalt
); );
return primary * 0.7D + detail * 0.3D; double detailInfluence = worm.bodyDetailInfluence();
return primary * (1D - detailInfluence) + detail * detailInfluence;
} }
private FlowTangent resolveFlowTangent(RiverNode node, RiverTerrainSampler terrain) { private FlowTangent resolveFlowTangent(RiverNode node, RiverTerrainSampler terrain) {
@@ -1263,6 +1264,7 @@ public final class RiverNetwork {
BODY_WIDTH_DETAIL_SALT, BODY_WIDTH_DETAIL_SALT,
worm.widthVariation() worm.widthVariation()
) )
+ options.channelRadiusBonus() * 2D
) )
); );
double baseBankWidth = nonNegativeOrFallback( double baseBankWidth = nonNegativeOrFallback(
@@ -29,6 +29,7 @@ public record RiverNetworkOptions(
double channelWidth, double channelWidth,
double bankWidth, double bankWidth,
double depth, double depth,
double channelRadiusBonus,
double maxChannelWidth, double maxChannelWidth,
double maxBankWidth, double maxBankWidth,
double maxDepth, double maxDepth,
@@ -60,6 +61,7 @@ public record RiverNetworkOptions(
requirePositive(channelWidth, "channelWidth"); requirePositive(channelWidth, "channelWidth");
requireFiniteNonNegative(bankWidth, "bankWidth"); requireFiniteNonNegative(bankWidth, "bankWidth");
requirePositive(depth, "depth"); requirePositive(depth, "depth");
requireFiniteNonNegative(channelRadiusBonus, "channelRadiusBonus");
requirePositive(maxChannelWidth, "maxChannelWidth"); requirePositive(maxChannelWidth, "maxChannelWidth");
requireFiniteNonNegative(maxBankWidth, "maxBankWidth"); requireFiniteNonNegative(maxBankWidth, "maxBankWidth");
requirePositive(maxDepth, "maxDepth"); requirePositive(maxDepth, "maxDepth");
@@ -215,6 +217,7 @@ public record RiverNetworkOptions(
private double channelWidth; private double channelWidth;
private double bankWidth; private double bankWidth;
private double depth; private double depth;
private double channelRadiusBonus;
private double maxChannelWidth; private double maxChannelWidth;
private double maxBankWidth; private double maxBankWidth;
private double maxDepth; private double maxDepth;
@@ -269,6 +272,7 @@ public record RiverNetworkOptions(
1D, 1D,
512D, 512D,
128D, 128D,
0.3D,
0D, 0D,
0D, 0D,
0D, 0D,
@@ -397,6 +401,11 @@ public record RiverNetworkOptions(
return this; return this;
} }
public Builder channelRadiusBonus(double value) {
channelRadiusBonus = value;
return this;
}
public Builder maxChannelWidth(double value) { public Builder maxChannelWidth(double value) {
maxChannelWidth = value; maxChannelWidth = value;
return this; return this;
@@ -461,6 +470,7 @@ public record RiverNetworkOptions(
channelWidth, channelWidth,
bankWidth, bankWidth,
depth, depth,
channelRadiusBonus,
maxChannelWidth, maxChannelWidth,
maxBankWidth, maxBankWidth,
maxDepth, maxDepth,
@@ -325,25 +325,35 @@ public final class RiverTile {
double additionalRadius double additionalRadius
) { ) {
RiverPolyline polyline = reach.polyline(); RiverPolyline polyline = reach.polyline();
if (polyline.length() == 0D) { RiverBodyProfile bodyProfile = reach.bodyProfile();
double polylineLength = polyline.length();
if (polylineLength == 0D) {
double distanceSquared = squared(x - polyline.x(0)) + squared(z - polyline.z(0)); double distanceSquared = squared(x - polyline.x(0)) + squared(z - polyline.z(0));
double radius = reach.widthAt(0D) * 0.5D + reach.bankWidthAt(0D) + additionalRadius; double radius = reach.widthAt(0D) * 0.5D + reach.bankWidthAt(0D) + additionalRadius;
return distanceSquared <= radius * radius ? new ClosestPoint(distanceSquared, 0D) : null; return distanceSquared <= radius * radius ? new ClosestPoint(distanceSquared, 0D) : null;
} }
double nearest = Double.POSITIVE_INFINITY; double nearest = Double.POSITIVE_INFINITY;
double nearestAlong = 0.0; double nearestAlong = 0.0;
for (int point = 0; point < polyline.size() - 1; point++) { int pointLimit = polyline.size() - 1;
double segmentStartAlong = polyline.cumulativeLength(point) / polyline.length(); int profileLimit = bodyProfile.size() - 1;
double segmentEndAlong = polyline.cumulativeLength(point + 1) / polyline.length(); for (int point = 0; point < pointLimit; point++) {
double segmentStartAlong = polyline.cumulativeLength(point) / polylineLength;
double segmentEndAlong = polyline.cumulativeLength(point + 1) / polylineLength;
double segmentAlongSpan = segmentEndAlong - segmentStartAlong; double segmentAlongSpan = segmentEndAlong - segmentStartAlong;
if (segmentAlongSpan == 0D) { if (segmentAlongSpan == 0D) {
continue; continue;
} }
double deltaX = polyline.x(point + 1) - polyline.x(point); double startX = polyline.x(point);
double deltaZ = polyline.z(point + 1) - polyline.z(point); double startZ = polyline.z(point);
for (int profileIndex = 0; profileIndex < reach.bodyProfile().size() - 1; profileIndex++) { double deltaX = polyline.x(point + 1) - startX;
double profileStart = reach.bodyProfile().position(profileIndex); double deltaZ = polyline.z(point + 1) - startZ;
double profileEnd = reach.bodyProfile().position(profileIndex + 1); int firstProfileIndex = bodyProfile.intervalIndex(segmentStartAlong);
for (int profileIndex = firstProfileIndex;
profileIndex < profileLimit
&& bodyProfile.position(profileIndex) <= segmentEndAlong;
profileIndex++) {
double profileStart = bodyProfile.position(profileIndex);
double profileEnd = bodyProfile.position(profileIndex + 1);
double overlapStart = StrictMath.max(segmentStartAlong, profileStart); double overlapStart = StrictMath.max(segmentStartAlong, profileStart);
double overlapEnd = StrictMath.min(segmentEndAlong, profileEnd); double overlapEnd = StrictMath.min(segmentEndAlong, profileEnd);
if (overlapStart > overlapEnd) { if (overlapStart > overlapEnd) {
@@ -351,13 +361,16 @@ public final class RiverTile {
} }
double intervalStart = (overlapStart - segmentStartAlong) / segmentAlongSpan; double intervalStart = (overlapStart - segmentStartAlong) / segmentAlongSpan;
double intervalEnd = (overlapEnd - segmentStartAlong) / segmentAlongSpan; double intervalEnd = (overlapEnd - segmentStartAlong) / segmentAlongSpan;
double widthSlope = (reach.bodyProfile().widthAtIndex(profileIndex + 1) double profileSpan = profileEnd - profileStart;
- reach.bodyProfile().widthAtIndex(profileIndex)) / (profileEnd - profileStart); double profileWidth = bodyProfile.widthAtIndex(profileIndex);
double bankSlope = (reach.bodyProfile().bankWidthAtIndex(profileIndex + 1) double profileBankWidth = bodyProfile.bankWidthAtIndex(profileIndex);
- reach.bodyProfile().bankWidthAtIndex(profileIndex)) / (profileEnd - profileStart); double widthSlope = (bodyProfile.widthAtIndex(profileIndex + 1)
double radiusBase = (reach.bodyProfile().widthAtIndex(profileIndex) - profileWidth) / profileSpan;
double bankSlope = (bodyProfile.bankWidthAtIndex(profileIndex + 1)
- profileBankWidth) / profileSpan;
double radiusBase = (profileWidth
+ widthSlope * (segmentStartAlong - profileStart)) * 0.5D + widthSlope * (segmentStartAlong - profileStart)) * 0.5D
+ reach.bodyProfile().bankWidthAtIndex(profileIndex) + profileBankWidth
+ bankSlope * (segmentStartAlong - profileStart) + bankSlope * (segmentStartAlong - profileStart)
+ additionalRadius; + additionalRadius;
double radiusSlope = (widthSlope * 0.5D + bankSlope) * segmentAlongSpan; double radiusSlope = (widthSlope * 0.5D + bankSlope) * segmentAlongSpan;
@@ -365,9 +378,9 @@ public final class RiverTile {
intervalStart, intervalStart,
intervalEnd, intervalEnd,
deltaX, deltaX,
polyline.x(point) - x, startX - x,
deltaZ, deltaZ,
polyline.z(point) - z, startZ - z,
radiusSlope, radiusSlope,
radiusBase, radiusBase,
segmentStartAlong, segmentStartAlong,
@@ -390,7 +403,9 @@ public final class RiverTile {
double maximumZ double maximumZ
) { ) {
RiverPolyline polyline = reach.polyline(); RiverPolyline polyline = reach.polyline();
if (polyline.length() == 0D) { RiverBodyProfile bodyProfile = reach.bodyProfile();
double polylineLength = polyline.length();
if (polylineLength == 0D) {
double distanceSquared = pointRectangleDistanceSquared( double distanceSquared = pointRectangleDistanceSquared(
polyline.x(0), polyline.x(0),
polyline.z(0), polyline.z(0),
@@ -404,9 +419,11 @@ public final class RiverTile {
} }
double nearest = Double.POSITIVE_INFINITY; double nearest = Double.POSITIVE_INFINITY;
double nearestAlong = 0.0; double nearestAlong = 0.0;
for (int point = 0; point < polyline.size() - 1; point++) { int pointLimit = polyline.size() - 1;
double segmentStartAlong = polyline.cumulativeLength(point) / polyline.length(); int profileLimit = bodyProfile.size() - 1;
double segmentEndAlong = polyline.cumulativeLength(point + 1) / polyline.length(); for (int point = 0; point < pointLimit; point++) {
double segmentStartAlong = polyline.cumulativeLength(point) / polylineLength;
double segmentEndAlong = polyline.cumulativeLength(point + 1) / polylineLength;
double segmentAlongSpan = segmentEndAlong - segmentStartAlong; double segmentAlongSpan = segmentEndAlong - segmentStartAlong;
if (segmentAlongSpan == 0D) { if (segmentAlongSpan == 0D) {
continue; continue;
@@ -415,9 +432,13 @@ public final class RiverTile {
double startZ = polyline.z(point); double startZ = polyline.z(point);
double deltaX = polyline.x(point + 1) - startX; double deltaX = polyline.x(point + 1) - startX;
double deltaZ = polyline.z(point + 1) - startZ; double deltaZ = polyline.z(point + 1) - startZ;
for (int profileIndex = 0; profileIndex < reach.bodyProfile().size() - 1; profileIndex++) { int firstProfileIndex = bodyProfile.intervalIndex(segmentStartAlong);
double profileStart = reach.bodyProfile().position(profileIndex); for (int profileIndex = firstProfileIndex;
double profileEnd = reach.bodyProfile().position(profileIndex + 1); profileIndex < profileLimit
&& bodyProfile.position(profileIndex) <= segmentEndAlong;
profileIndex++) {
double profileStart = bodyProfile.position(profileIndex);
double profileEnd = bodyProfile.position(profileIndex + 1);
double overlapStart = StrictMath.max(segmentStartAlong, profileStart); double overlapStart = StrictMath.max(segmentStartAlong, profileStart);
double overlapEnd = StrictMath.min(segmentEndAlong, profileEnd); double overlapEnd = StrictMath.min(segmentEndAlong, profileEnd);
if (overlapStart > overlapEnd) { if (overlapStart > overlapEnd) {
@@ -425,13 +446,16 @@ public final class RiverTile {
} }
double intervalStart = (overlapStart - segmentStartAlong) / segmentAlongSpan; double intervalStart = (overlapStart - segmentStartAlong) / segmentAlongSpan;
double intervalEnd = (overlapEnd - segmentStartAlong) / segmentAlongSpan; double intervalEnd = (overlapEnd - segmentStartAlong) / segmentAlongSpan;
double widthSlope = (reach.bodyProfile().widthAtIndex(profileIndex + 1) double profileSpan = profileEnd - profileStart;
- reach.bodyProfile().widthAtIndex(profileIndex)) / (profileEnd - profileStart); double profileWidth = bodyProfile.widthAtIndex(profileIndex);
double bankSlope = (reach.bodyProfile().bankWidthAtIndex(profileIndex + 1) double profileBankWidth = bodyProfile.bankWidthAtIndex(profileIndex);
- reach.bodyProfile().bankWidthAtIndex(profileIndex)) / (profileEnd - profileStart); double widthSlope = (bodyProfile.widthAtIndex(profileIndex + 1)
double radiusBase = (reach.bodyProfile().widthAtIndex(profileIndex) - profileWidth) / profileSpan;
double bankSlope = (bodyProfile.bankWidthAtIndex(profileIndex + 1)
- profileBankWidth) / profileSpan;
double radiusBase = (profileWidth
+ widthSlope * (segmentStartAlong - profileStart)) * 0.5D + widthSlope * (segmentStartAlong - profileStart)) * 0.5D
+ reach.bodyProfile().bankWidthAtIndex(profileIndex) + profileBankWidth
+ bankSlope * (segmentStartAlong - profileStart); + bankSlope * (segmentStartAlong - profileStart);
double radiusSlope = (widthSlope * 0.5D + bankSlope) * segmentAlongSpan; double radiusSlope = (widthSlope * 0.5D + bankSlope) * segmentAlongSpan;
double cursor = intervalStart; double cursor = intervalStart;
@@ -626,8 +650,7 @@ public final class RiverTile {
} }
private List<RiverReach> indexedReaches(double x, double z) { private List<RiverReach> indexedReaches(double x, double z) {
List<RiverReach> indexed = spatialIndex.get(bucketKey(bucket(x), bucket(z))); return spatialIndex.getOrDefault(bucketKey(bucket(x), bucket(z)), List.of());
return indexed == null ? List.of() : indexed;
} }
private List<RiverReach> indexedReaches( private List<RiverReach> indexedReaches(
@@ -641,12 +664,15 @@ public final class RiverTile {
int maximumBucketX = bucket(StrictMath.nextDown(queryMaximumX)); int maximumBucketX = bucket(StrictMath.nextDown(queryMaximumX));
int minimumBucketZ = bucket(queryMinimumZ); int minimumBucketZ = bucket(queryMinimumZ);
int maximumBucketZ = bucket(StrictMath.nextDown(queryMaximumZ)); int maximumBucketZ = bucket(StrictMath.nextDown(queryMaximumZ));
if (minimumBucketX == maximumBucketX && minimumBucketZ == maximumBucketZ) {
return spatialIndex.getOrDefault(
bucketKey(minimumBucketX, minimumBucketZ),
List.of()
);
}
for (int bucketX = minimumBucketX; bucketX <= maximumBucketX; bucketX++) { for (int bucketX = minimumBucketX; bucketX <= maximumBucketX; bucketX++) {
for (int bucketZ = minimumBucketZ; bucketZ <= maximumBucketZ; bucketZ++) { for (int bucketZ = minimumBucketZ; bucketZ <= maximumBucketZ; bucketZ++) {
List<RiverReach> bucketReaches = spatialIndex.get(bucketKey(bucketX, bucketZ)); indexed.addAll(spatialIndex.getOrDefault(bucketKey(bucketX, bucketZ), List.of()));
if (bucketReaches != null) {
indexed.addAll(bucketReaches);
}
} }
} }
return List.copyOf(indexed); return List.copyOf(indexed);
@@ -662,26 +688,13 @@ public final class RiverTile {
int maximumBucketX = bucket(queryMaximumX); int maximumBucketX = bucket(queryMaximumX);
int minimumBucketZ = bucket(queryMinimumZ); int minimumBucketZ = bucket(queryMinimumZ);
int maximumBucketZ = bucket(queryMaximumZ); int maximumBucketZ = bucket(queryMaximumZ);
if (spatialIndex.isEmpty()) {
return List.of();
}
if (minimumBucketX == maximumBucketX && minimumBucketZ == maximumBucketZ) { if (minimumBucketX == maximumBucketX && minimumBucketZ == maximumBucketZ) {
List<RiverReach> bucketReaches = spatialIndex.get(bucketKey(minimumBucketX, minimumBucketZ)); return indexedReaches(queryMinimumX, queryMinimumZ);
return bucketReaches == null ? List.of() : bucketReaches;
}
long bucketWidth = (long) maximumBucketX - minimumBucketX + 1L;
long bucketDepth = (long) maximumBucketZ - minimumBucketZ + 1L;
if (bucketWidth > spatialIndex.size() / bucketDepth
|| bucketWidth * bucketDepth >= spatialIndex.size()) {
return reaches;
} }
LinkedHashSet<RiverReach> indexed = new LinkedHashSet<>(); LinkedHashSet<RiverReach> indexed = new LinkedHashSet<>();
for (int bucketX = minimumBucketX; bucketX <= maximumBucketX; bucketX++) { for (int bucketX = minimumBucketX; bucketX <= maximumBucketX; bucketX++) {
for (int bucketZ = minimumBucketZ; bucketZ <= maximumBucketZ; bucketZ++) { for (int bucketZ = minimumBucketZ; bucketZ <= maximumBucketZ; bucketZ++) {
List<RiverReach> bucketReaches = spatialIndex.get(bucketKey(bucketX, bucketZ)); indexed.addAll(spatialIndex.getOrDefault(bucketKey(bucketX, bucketZ), List.of()));
if (bucketReaches != null) {
indexed.addAll(bucketReaches);
}
} }
} }
return List.copyOf(indexed); return List.copyOf(indexed);
@@ -17,6 +17,7 @@ public record RiverWorm(
double depthMultiplier, double depthMultiplier,
double bodyWavelength, double bodyWavelength,
double bodyDetailWavelength, double bodyDetailWavelength,
double bodyDetailInfluence,
double widthVariation, double widthVariation,
double bankVariation, double bankVariation,
double depthVariation, double depthVariation,
@@ -44,8 +45,9 @@ public record RiverWorm(
requireRange(widthMultiplier, 0.125D, 8D, "widthMultiplier"); requireRange(widthMultiplier, 0.125D, 8D, "widthMultiplier");
requireRange(bankMultiplier, 0.125D, 8D, "bankMultiplier"); requireRange(bankMultiplier, 0.125D, 8D, "bankMultiplier");
requireRange(depthMultiplier, 0.125D, 8D, "depthMultiplier"); requireRange(depthMultiplier, 0.125D, 8D, "depthMultiplier");
requireRange(bodyWavelength, 32D, 16384D, "bodyWavelength"); requireRange(bodyWavelength, 8D, 16384D, "bodyWavelength");
requireRange(bodyDetailWavelength, 32D, 16384D, "bodyDetailWavelength"); requireRange(bodyDetailWavelength, 8D, 16384D, "bodyDetailWavelength");
requireRange(bodyDetailInfluence, 0D, 1D, "bodyDetailInfluence");
requireRange(widthVariation, 0D, 0.875D, "widthVariation"); requireRange(widthVariation, 0D, 0.875D, "widthVariation");
requireRange(bankVariation, 0D, 0.875D, "bankVariation"); requireRange(bankVariation, 0D, 0.875D, "bankVariation");
requireRange(depthVariation, 0D, 0.875D, "depthVariation"); requireRange(depthVariation, 0D, 0.875D, "depthVariation");
@@ -2,7 +2,7 @@ package art.arcane.iris.engine.river.cave;
public enum RiverCaveAction { public enum RiverCaveAction {
WET_SOURCE, WET_SOURCE,
FALLING_WATER, FALLING_FLUID,
DRY_AIR, DRY_AIR,
SEAL_GUARD SEAL_GUARD
} }
@@ -70,6 +70,7 @@ public final class RiverCaveContainmentPlanner {
settings, settings,
throat.positions() throat.positions()
); );
case DEEP_POOL -> planDeepPool(view, source, settings, throat.positions());
}; };
} }
@@ -302,6 +303,111 @@ public final class RiverCaveContainmentPlanner {
return accepted(view, source, actions); return accepted(view, source, actions);
} }
private RiverCavePlan planDeepPool(
CaveVoxelView view,
RiverCaveSource source,
RiverCavePlannerSettings settings,
List<CavePosition> throat
) {
GrottoResult grotto = buildGrotto(source, settings);
if (grotto.rejection() != RiverCaveRejection.NONE) {
return rejected(source, grotto.rejection());
}
Set<CavePosition> chamber = grotto.positions();
Set<CavePosition> carve = new HashSet<>(chamber.size() + throat.size());
carve.addAll(chamber);
carve.addAll(throat);
RiverCaveRejection carveRejection = validateDeepPoolCarve(view, source, settings, carve);
if (carveRejection != RiverCaveRejection.NONE) {
return rejected(source, carveRejection);
}
BoundaryResult boundary = validateDeepPoolBoundary(view, source, settings, carve);
if (boundary.rejection() != RiverCaveRejection.NONE) {
return rejected(source, boundary.rejection());
}
Map<CavePosition, RiverCaveAction> actions = new HashMap<>();
addChamberActions(actions, chamber, source.waterHeadY());
addThroatActions(actions, throat, source);
for (CavePosition position : boundary.sealGuards()) {
actions.put(position, RiverCaveAction.SEAL_GUARD);
}
return accepted(view, source, actions);
}
private RiverCaveRejection validateDeepPoolCarve(
CaveVoxelView view,
RiverCaveSource source,
RiverCavePlannerSettings settings,
Set<CavePosition> carve
) {
for (CavePosition position : carve) {
if (!view.isInWorld(position)) {
return RiverCaveRejection.WORLD_BOUNDARY;
}
RiverCaveRejection boundsRejection = validateBounds(source, settings, position);
if (boundsRejection != RiverCaveRejection.NONE) {
return boundsRejection;
}
CaveVoxel voxel = voxelAt(view, position);
RiverCaveRejection hazard = rejectionForHazard(voxel, settings);
if (hazard != RiverCaveRejection.NONE) {
return hazard;
}
if (voxel == CaveVoxel.SOLID) {
continue;
}
if (position.y() > source.waterHeadY()
&& voxel == CaveVoxel.CAVE_AIR
&& !view.isOpenToSurface(position)) {
continue;
}
return RiverCaveRejection.GROTTO_INTERSECTION;
}
return RiverCaveRejection.NONE;
}
private BoundaryResult validateDeepPoolBoundary(
CaveVoxelView view,
RiverCaveSource source,
RiverCavePlannerSettings settings,
Set<CavePosition> carve
) {
Set<CavePosition> guards = new HashSet<>();
for (CavePosition position : carve) {
for (CavePosition direction : DIRECTIONS) {
CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z());
if (carve.contains(neighbor)) {
continue;
}
if (!view.isInWorld(neighbor)) {
return BoundaryResult.rejected(RiverCaveRejection.WORLD_BOUNDARY);
}
RiverCaveRejection boundsRejection = validateBounds(source, settings, neighbor);
if (boundsRejection != RiverCaveRejection.NONE) {
return BoundaryResult.rejected(boundsRejection);
}
CaveVoxel voxel = voxelAt(view, neighbor);
RiverCaveRejection hazard = rejectionForHazard(voxel, settings);
if (hazard != RiverCaveRejection.NONE) {
return BoundaryResult.rejected(hazard);
}
if (voxel == CaveVoxel.SOLID) {
guards.add(neighbor);
continue;
}
if (neighbor.y() > source.waterHeadY()
&& voxel == CaveVoxel.CAVE_AIR
&& !view.isOpenToSurface(neighbor)) {
continue;
}
return BoundaryResult.rejected(RiverCaveRejection.GROTTO_SHELL_OPEN);
}
}
return BoundaryResult.accepted(guards);
}
private ComponentResult resolveClosedComponent( private ComponentResult resolveClosedComponent(
CaveVoxelView view, CaveVoxelView view,
RiverCaveSource source, RiverCaveSource source,
@@ -732,7 +838,7 @@ public final class RiverCaveContainmentPlanner {
action = RiverCaveAction.WET_SOURCE; action = RiverCaveAction.WET_SOURCE;
} else if (source.mode() == RiverCaveMode.WATERFALL_POOL } else if (source.mode() == RiverCaveMode.WATERFALL_POOL
|| source.mode() == RiverCaveMode.GENERATED_GROTTO) { || source.mode() == RiverCaveMode.GENERATED_GROTTO) {
action = RiverCaveAction.FALLING_WATER; action = RiverCaveAction.FALLING_FLUID;
} else { } else {
action = RiverCaveAction.DRY_AIR; action = RiverCaveAction.DRY_AIR;
} }
@@ -0,0 +1,6 @@
package art.arcane.iris.engine.river.cave;
public enum RiverCaveFluidKind {
RIVER,
DEEP_POOL
}
@@ -11,20 +11,30 @@ public final class RiverCaveHydrology {
private final RiverCaveAction action; private final RiverCaveAction action;
private final String floodedBiomeKey; private final String floodedBiomeKey;
private final RiverCaveFluidKind fluidKind;
private final MatterCavern cavern; private final MatterCavern cavern;
public RiverCaveHydrology(RiverCaveAction action, String floodedBiomeKey) { public RiverCaveHydrology(
RiverCaveAction action,
String floodedBiomeKey,
RiverCaveFluidKind fluidKind
) {
this.action = Objects.requireNonNull(action); this.action = Objects.requireNonNull(action);
this.floodedBiomeKey = floodedBiomeKey == null ? "" : floodedBiomeKey.trim(); this.floodedBiomeKey = floodedBiomeKey == null ? "" : floodedBiomeKey.trim();
this.fluidKind = Objects.requireNonNull(fluidKind);
this.cavern = switch (action) { this.cavern = switch (action) {
case WET_SOURCE, FALLING_WATER -> new MatterCavern(true, this.floodedBiomeKey, LIQUID_FLUID); case WET_SOURCE, FALLING_FLUID -> new MatterCavern(true, this.floodedBiomeKey, LIQUID_FLUID);
case DRY_AIR -> new MatterCavern(true, this.floodedBiomeKey, LIQUID_FORCED_AIR); case DRY_AIR -> new MatterCavern(true, this.floodedBiomeKey, LIQUID_FORCED_AIR);
case SEAL_GUARD -> null; case SEAL_GUARD -> null;
}; };
} }
public static RiverCaveHydrology of(RiverCaveAction action) { public static RiverCaveHydrology of(RiverCaveAction action) {
return new RiverCaveHydrology(action, ""); return new RiverCaveHydrology(action, "", RiverCaveFluidKind.RIVER);
}
public static RiverCaveHydrology of(RiverCaveAction action, RiverCaveFluidKind fluidKind) {
return new RiverCaveHydrology(action, "", fluidKind);
} }
public Optional<String> floodedBiome() { public Optional<String> floodedBiome() {
@@ -36,11 +46,11 @@ public final class RiverCaveHydrology {
} }
public boolean isWet() { public boolean isWet() {
return action == RiverCaveAction.WET_SOURCE || action == RiverCaveAction.FALLING_WATER; return action == RiverCaveAction.WET_SOURCE || action == RiverCaveAction.FALLING_FLUID;
} }
public boolean isFalling() { public boolean isFalling() {
return action == RiverCaveAction.FALLING_WATER; return action == RiverCaveAction.FALLING_FLUID;
} }
public boolean protectsPlacement() { public boolean protectsPlacement() {
@@ -59,6 +69,10 @@ public final class RiverCaveHydrology {
return floodedBiomeKey; return floodedBiomeKey;
} }
public RiverCaveFluidKind fluidKind() {
return fluidKind;
}
@Override @Override
public boolean equals(Object object) { public boolean equals(Object object) {
if (this == object) { if (this == object) {
@@ -67,16 +81,19 @@ public final class RiverCaveHydrology {
if (!(object instanceof RiverCaveHydrology hydrology)) { if (!(object instanceof RiverCaveHydrology hydrology)) {
return false; return false;
} }
return action == hydrology.action && floodedBiomeKey.equals(hydrology.floodedBiomeKey); return action == hydrology.action
&& floodedBiomeKey.equals(hydrology.floodedBiomeKey)
&& fluidKind == hydrology.fluidKind;
} }
@Override @Override
public int hashCode() { public int hashCode() {
return (31 * action.hashCode()) + floodedBiomeKey.hashCode(); return (31 * ((31 * action.hashCode()) + floodedBiomeKey.hashCode())) + fluidKind.hashCode();
} }
@Override @Override
public String toString() { public String toString() {
return "RiverCaveHydrology[action=" + action + ", floodedBiomeKey=" + floodedBiomeKey + "]"; return "RiverCaveHydrology[action=" + action + ", floodedBiomeKey=" + floodedBiomeKey
+ ", fluidKind=" + fluidKind + "]";
} }
} }
@@ -4,5 +4,6 @@ public enum RiverCaveMode {
CLOSED_COMPONENT, CLOSED_COMPONENT,
GENERATED_GROTTO, GENERATED_GROTTO,
GROTTO_OR_CLOSED_COMPONENT, GROTTO_OR_CLOSED_COMPONENT,
WATERFALL_POOL WATERFALL_POOL,
DEEP_POOL
} }
@@ -9,6 +9,7 @@ import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisRiverNetwork; import art.arcane.iris.engine.object.IrisRiverNetwork;
import art.arcane.iris.engine.object.IrisRiverCaveMode; import art.arcane.iris.engine.object.IrisRiverCaveMode;
import art.arcane.iris.engine.object.IrisRiverCaves; import art.arcane.iris.engine.object.IrisRiverCaves;
import art.arcane.iris.engine.object.IrisRiverDeepPools;
import art.arcane.iris.engine.object.IrisRiverNoiseChance; import art.arcane.iris.engine.object.IrisRiverNoiseChance;
import art.arcane.iris.engine.object.IrisRiverRoutingPolicy; import art.arcane.iris.engine.object.IrisRiverRoutingPolicy;
import art.arcane.iris.engine.object.IrisRiverTerminalMode; import art.arcane.iris.engine.object.IrisRiverTerminalMode;
@@ -41,6 +42,8 @@ import art.arcane.iris.util.project.noise.CNG;
import art.arcane.iris.util.project.stream.ProceduralStream; import art.arcane.iris.util.project.stream.ProceduralStream;
import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.math.RNG;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
@@ -48,6 +51,7 @@ import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReferenceArray;
public final class IrisRiverRuntime implements AutoCloseable { public final class IrisRiverRuntime implements AutoCloseable {
static final int MAXIMUM_REACH_FEASIBILITY_SAMPLES = 65; static final int MAXIMUM_REACH_FEASIBILITY_SAMPLES = 65;
@@ -66,17 +70,22 @@ public final class IrisRiverRuntime implements AutoCloseable {
private static final long BIOME_NOISE_SALT = 0x2FFD72DBD01ADFB7L; private static final long BIOME_NOISE_SALT = 0x2FFD72DBD01ADFB7L;
private static final long CAVE_ENTRY_NOISE_SALT = 0xB8E1AFED6A267E96L; private static final long CAVE_ENTRY_NOISE_SALT = 0xB8E1AFED6A267E96L;
private static final long CAVE_ENTRY_GATE_SALT = 0xBA7C9045F12C7F99L; private static final long CAVE_ENTRY_GATE_SALT = 0xBA7C9045F12C7F99L;
private static final long DEEP_POOL_REACH_NOISE_SALT = 0x7137449123EF65CDL;
private static final long DEEP_POOL_REACH_GATE_SALT = 0xE9B5DBA58189DBBCL;
private static final long TUNNEL_FLOOR_NOISE_SALT = 0x8CB92BA72F3D8DD7L; private static final long TUNNEL_FLOOR_NOISE_SALT = 0x8CB92BA72F3D8DD7L;
private static final long TUNNEL_ROOF_NOISE_SALT = 0xDB4F0B9175AE2165L; private static final long TUNNEL_ROOF_NOISE_SALT = 0xDB4F0B9175AE2165L;
private static final long TUNNEL_WIDTH_NOISE_SALT = 0xC6EF372FE94F82BEL; private static final long TUNNEL_WIDTH_NOISE_SALT = 0xC6EF372FE94F82BEL;
private static final long FLOODED_CAVE_BIOME_SALT = 0x24A19947B3916CF7L; private static final long FLOODED_CAVE_BIOME_SALT = 0x24A19947B3916CF7L;
private static final long TERMINAL_CAVE_ANCHOR_SALT = 0x9E3779B97F4A7C15L; private static final long TERMINAL_CAVE_ANCHOR_SALT = 0x9E3779B97F4A7C15L;
private static final int TILE_CACHE_SIZE = 32; private static final int TILE_CACHE_SIZE = 32;
private static final int TUNNEL_SAMPLE_CHUNK_CACHE_SIZE = 4_096;
private static final Object ABSENT_TUNNEL_SAMPLE = new Object();
private final long seed; private final long seed;
private final IrisRiverNetwork configuration; private final IrisRiverNetwork configuration;
private final IrisData data; private final IrisData data;
private final int fluidHeight; private final int riverFluidHeight;
private final int dimensionFluidHeight;
private final boolean boreMantleActive; private final boolean boreMantleActive;
private final boolean caveHydrologyActive; private final boolean caveHydrologyActive;
private final boolean blockingRoutingPossible; private final boolean blockingRoutingPossible;
@@ -101,12 +110,14 @@ public final class IrisRiverRuntime implements AutoCloseable {
private final CNG bedNoise; private final CNG bedNoise;
private final CNG biomeNoise; private final CNG biomeNoise;
private final CNG caveEntryNoise; private final CNG caveEntryNoise;
private final CNG deepPoolReachNoise;
private final CNG tunnelFloorNoise; private final CNG tunnelFloorNoise;
private final CNG tunnelRoofNoise; private final CNG tunnelRoofNoise;
private final CNG tunnelWidthNoise; private final CNG tunnelWidthNoise;
private final art.arcane.iris.engine.river.RiverNetwork network; private final art.arcane.iris.engine.river.RiverNetwork network;
private final RuntimeTerrainSampler terrainSampler; private final RuntimeTerrainSampler terrainSampler;
private final RiverTileCache tileCache; private final RiverTileCache tileCache;
private final Cache<Long, TunnelSampleChunk> tunnelSampleCache;
private final ConcurrentHashMap<IdentitySettingsKey, EffectiveRiverSettings> settingsCache; private final ConcurrentHashMap<IdentitySettingsKey, EffectiveRiverSettings> settingsCache;
private final ConcurrentHashMap<BiomePoolKey, List<IrisBiome>> biomePoolCache; private final ConcurrentHashMap<BiomePoolKey, List<IrisBiome>> biomePoolCache;
@@ -115,7 +126,8 @@ public final class IrisRiverRuntime implements AutoCloseable {
seed = context.seed(); seed = context.seed();
configuration = context.configuration(); configuration = context.configuration();
data = context.data(); data = context.data();
fluidHeight = context.fluidHeight(); riverFluidHeight = context.riverFluidHeight();
dimensionFluidHeight = context.dimensionFluidHeight();
boreMantleActive = context.boreMantleActive(); boreMantleActive = context.boreMantleActive();
caveHydrologyActive = context.caveHydrologyActive(); caveHydrologyActive = context.caveHydrologyActive();
blockingRoutingPossible = context.blockingRoutingPossible(); blockingRoutingPossible = context.blockingRoutingPossible();
@@ -146,6 +158,11 @@ public final class IrisRiverRuntime implements AutoCloseable {
bedNoise = noise(terrain.getBedRoughnessStyle(), BED_NOISE_SALT); bedNoise = noise(terrain.getBedRoughnessStyle(), BED_NOISE_SALT);
biomeNoise = noise(configuration.getBiomes().getSelectionStyle(), BIOME_NOISE_SALT); biomeNoise = noise(configuration.getBiomes().getSelectionStyle(), BIOME_NOISE_SALT);
caveEntryNoise = noise(caves.getEntry(), CAVE_ENTRY_NOISE_SALT); caveEntryNoise = noise(caves.getEntry(), CAVE_ENTRY_NOISE_SALT);
IrisRiverDeepPools deepPools = caves.getDeepPools();
deepPoolReachNoise = noise(
deepPools == null ? null : deepPools.getReach(),
DEEP_POOL_REACH_NOISE_SALT
);
tunnelFloorNoise = noise(terrain.getTunnelFloorStyle(), TUNNEL_FLOOR_NOISE_SALT); tunnelFloorNoise = noise(terrain.getTunnelFloorStyle(), TUNNEL_FLOOR_NOISE_SALT);
tunnelRoofNoise = noise(terrain.getTunnelRoofStyle(), TUNNEL_ROOF_NOISE_SALT); tunnelRoofNoise = noise(terrain.getTunnelRoofStyle(), TUNNEL_ROOF_NOISE_SALT);
tunnelWidthNoise = noise(terrain.getTunnelWidthMultiplier(), TUNNEL_WIDTH_NOISE_SALT); tunnelWidthNoise = noise(terrain.getTunnelWidthMultiplier(), TUNNEL_WIDTH_NOISE_SALT);
@@ -158,12 +175,15 @@ public final class IrisRiverRuntime implements AutoCloseable {
TILE_CACHE_SIZE, TILE_CACHE_SIZE,
(tileX, tileZ) -> network.buildTile(tileX, tileZ, terrainSampler) (tileX, tileZ) -> network.buildTile(tileX, tileZ, terrainSampler)
); );
tunnelSampleCache = Caffeine.newBuilder()
.maximumSize(TUNNEL_SAMPLE_CHUNK_CACHE_SIZE)
.build();
} }
public IrisRiverSurfaceSample sample(double x, double z) { public IrisRiverSurfaceSample sample(double x, double z) {
ResolvedRiverColumn column = resolveColumn(x, z); ResolvedRiverColumn column = resolveColumn(x, z);
if (column == null) { if (column == null) {
return IrisRiverSurfaceSample.none(naturalHeight.get(x, z), fluidHeight); return IrisRiverSurfaceSample.none(naturalHeight.get(x, z), dimensionFluidHeight);
} }
if (column.subterranean()) { if (column.subterranean()) {
return new IrisRiverSurfaceSample( return new IrisRiverSurfaceSample(
@@ -229,9 +249,8 @@ public final class IrisRiverRuntime implements AutoCloseable {
if (!column.subterranean()) { if (!column.subterranean()) {
return null; return null;
} }
if (isTunnelMouth(column, mouthBlend)) { double mouthFactor = tunnelMouthFactor(column, mouthBlend);
channelRadius += mouthBlend; channelRadius += mouthBlend * mouthFactor;
}
if (column.river().distance() > channelRadius) { if (column.river().distance() > channelRadius) {
return null; return null;
} }
@@ -255,13 +274,20 @@ public final class IrisRiverRuntime implements AutoCloseable {
); );
int ceilingY = shapedTunnelCeilingY( int ceilingY = shapedTunnelCeilingY(
waterHeadY, waterHeadY,
caves.getDryHeadroom() * column.reach().roofScaleAt(column.river().alongReach()), caves.getDryHeadroom() * column.reach().roofScaleAt(column.river().alongReach())
+ mouthBlend * mouthFactor,
profile, profile,
roofOffset roofOffset
); );
return new IrisRiverTunnelSample(column.river(), bedY, waterHeadY, ceilingY); return new IrisRiverTunnelSample(column.river(), bedY, waterHeadY, ceilingY);
} }
public IrisRiverTunnelSample sampleTunnel(int x, int z) {
long chunkKey = ((long) (x >> 4) << 32) ^ ((z >> 4) & 0xFFFFFFFFL);
TunnelSampleChunk chunk = tunnelSampleCache.get(chunkKey, ignored -> new TunnelSampleChunk());
return chunk.sample(this, x, z);
}
static int shapedTunnelBedY( static int shapedTunnelBedY(
int waterHeadY, int waterHeadY,
double baseBedY, double baseBedY,
@@ -331,18 +357,44 @@ public final class IrisRiverRuntime implements AutoCloseable {
); );
} }
private boolean isTunnelMouth(ResolvedRiverColumn column, double mouthBlend) { private double tunnelMouthFactor(ResolvedRiverColumn column, double mouthBlend) {
if (mouthBlend <= 0D) { if (mouthBlend <= 0D) {
return false; return 0D;
} }
double length = column.reach().polyline().length(); double length = column.reach().polyline().length();
if (length <= 0D) { if (length <= 0D) {
return false; return 0D;
} }
double offset = mouthBlend / length; double offset = mouthBlend / length;
double alongReach = column.river().alongReach(); double alongReach = column.river().alongReach();
return !isCenterlineSubterranean(column.reach(), clamp01(alongReach - offset)) return Math.max(
|| !isCenterlineSubterranean(column.reach(), clamp01(alongReach + offset)); tunnelMouthFactor(column.reach(), alongReach, clamp01(alongReach - offset), mouthBlend),
tunnelMouthFactor(column.reach(), alongReach, clamp01(alongReach + offset), mouthBlend)
);
}
private double tunnelMouthFactor(
RiverReach reach,
double subterraneanAlong,
double candidateOpenAlong,
double mouthBlend
) {
if (candidateOpenAlong == subterraneanAlong
|| isCenterlineSubterranean(reach, candidateOpenAlong)) {
return 0D;
}
double openAlong = candidateOpenAlong;
double solidAlong = subterraneanAlong;
for (int iteration = 0; iteration < 5; iteration++) {
double midpoint = (openAlong + solidAlong) * 0.5D;
if (isCenterlineSubterranean(reach, midpoint)) {
solidAlong = midpoint;
} else {
openAlong = midpoint;
}
}
double distance = StrictMath.abs(solidAlong - subterraneanAlong) * reach.polyline().length();
return clamp01(1D - distance / mouthBlend);
} }
private boolean isCenterlineSubterranean(RiverReach reach, double alongReach) { private boolean isCenterlineSubterranean(RiverReach reach, double alongReach) {
@@ -401,6 +453,35 @@ public final class IrisRiverRuntime implements AutoCloseable {
return tileAt(centerX, centerZ).sampleFootprint(minimumX, minimumZ, maximumX, maximumZ); return tileAt(centerX, centerZ).sampleFootprint(minimumX, minimumZ, maximumX, maximumZ);
} }
public boolean hasRiverFootprint(
int minimumX,
int minimumZ,
int maximumX,
int maximumZ
) {
if (minimumX >= maximumX || minimumZ >= maximumZ) {
return false;
}
int minimumTileX = network.tileXForBlock(minimumX);
int minimumTileZ = network.tileZForBlock(minimumZ);
int maximumTileX = network.tileXForBlock(maximumX - 1);
int maximumTileZ = network.tileZForBlock(maximumZ - 1);
for (int tileX = minimumTileX; tileX <= maximumTileX; tileX++) {
for (int tileZ = minimumTileZ; tileZ <= maximumTileZ; tileZ++) {
RiverSample sample = tileCache.get(tileX, tileZ).sampleFootprint(
minimumX,
minimumZ,
maximumX,
maximumZ
);
if (sample.present()) {
return true;
}
}
}
return false;
}
public List<RiverAnchor> candidateAnchors( public List<RiverAnchor> candidateAnchors(
int minimumX, int minimumX,
int minimumZ, int minimumZ,
@@ -481,7 +562,9 @@ public final class IrisRiverRuntime implements AutoCloseable {
} }
public int maximumTunnelHeadroom() { public int maximumTunnelHeadroom() {
return caves.getDryHeadroom() + (int) StrictMath.ceil(terrain.getTunnelRoofVariation()); return caves.getDryHeadroom()
+ (int) StrictMath.ceil(terrain.getTunnelRoofVariation())
+ (int) StrictMath.ceil(terrain.getTunnelMouthBlend());
} }
public boolean acceptsCaveAnchor(RiverAnchor anchor) { public boolean acceptsCaveAnchor(RiverAnchor anchor) {
@@ -535,6 +618,52 @@ public final class IrisRiverRuntime implements AutoCloseable {
return false; return false;
} }
public boolean acceptsDeepPoolAnchor(RiverAnchor anchor) {
Objects.requireNonNull(anchor);
IrisRiverDeepPools deepPools = caves.getDeepPools();
if (deepPools == null
|| !deepPools.isEnabled()
|| deepPools.getMaximumPerReach() <= 0
|| anchor.state() != RiverRouteState.WET
|| !caveHydrologyActive) {
return false;
}
RiverReach reach = tileAt(anchor.x(), anchor.z()).reach(anchor.reachId());
if (reach == null || reach.state() != RiverRouteState.WET) {
return false;
}
if (!deepPoolReachEligible(deepPools, reach)) {
return false;
}
TerminalCaveAnchor terminal = terminalCaveAnchor(reach);
if (terminal != null && anchor.stableId() == terminal.stableId()) {
return false;
}
double firstDistance = unit(art.arcane.iris.engine.river.RiverNetwork.mix(
reach.id().stableId() ^ anchor.samplingSalt()
)) * anchor.samplingSpacing();
double anchorDistance = firstDistance + anchor.index() * anchor.samplingSpacing();
if (anchorDistance >= reach.polyline().length()) {
return false;
}
int accepted = 0;
for (int index = 0; index <= anchor.index(); index++) {
long stableId = art.arcane.iris.engine.river.RiverNetwork.mix(
reach.id().stableId()
^ anchor.samplingSalt()
^ (long) index * 0x9E3779B97F4A7C15L
);
if (index == anchor.index()) {
return stableId == anchor.stableId() && accepted < deepPools.getMaximumPerReach();
}
accepted++;
if (accepted >= deepPools.getMaximumPerReach()) {
return false;
}
}
return false;
}
private boolean isCaveAnchorSourceable(double x, double z) { private boolean isCaveAnchorSourceable(double x, double z) {
IrisRiverSurfaceSample surface = sample(x, z); IrisRiverSurfaceSample surface = sample(x, z);
if (surface.river().present() if (surface.river().present()
@@ -603,6 +732,7 @@ public final class IrisRiverRuntime implements AutoCloseable {
@Override @Override
public void close() { public void close() {
tileCache.close(); tileCache.close();
tunnelSampleCache.invalidateAll();
settingsCache.clear(); settingsCache.clear();
biomePoolCache.clear(); biomePoolCache.clear();
} }
@@ -620,7 +750,7 @@ public final class IrisRiverRuntime implements AutoCloseable {
.routingDeviationScaleCells(topology.getRoutingDeviationScaleCells()) .routingDeviationScaleCells(topology.getRoutingDeviationScaleCells())
.routingDeviationStrengthCells(topology.getRoutingDeviationStrengthCells()) .routingDeviationStrengthCells(topology.getRoutingDeviationStrengthCells())
.routingPlateauHeight(topology.getRoutingPlateauHeight()) .routingPlateauHeight(topology.getRoutingPlateauHeight())
.hydraulicBaseHeight(fluidHeight) .hydraulicBaseHeight(riverFluidHeight)
.requireOcean(topology.isRequireOcean()) .requireOcean(topology.isRequireOcean())
.sourceChance(chance(topology.getSource())) .sourceChance(chance(topology.getSource()))
.reachChance(chance(topology.getContinuation())) .reachChance(chance(topology.getContinuation()))
@@ -633,6 +763,7 @@ public final class IrisRiverRuntime implements AutoCloseable {
.channelWidth(mid(riverTerrain.getChannelWidth(), 12D)) .channelWidth(mid(riverTerrain.getChannelWidth(), 12D))
.bankWidth(mid(riverTerrain.getBankWidth(), 8D)) .bankWidth(mid(riverTerrain.getBankWidth(), 8D))
.depth(mid(riverTerrain.getDepth(), 4D)) .depth(mid(riverTerrain.getDepth(), 4D))
.channelRadiusBonus(riverTerrain.getChannelRadiusBonus())
.maxChannelWidth(riverTerrain.getMaxChannelWidth()) .maxChannelWidth(riverTerrain.getMaxChannelWidth())
.maxBankWidth(riverTerrain.getMaxBankWidth()) .maxBankWidth(riverTerrain.getMaxBankWidth())
.maxDepth(riverTerrain.getMaxDepth()) .maxDepth(riverTerrain.getMaxDepth())
@@ -683,6 +814,7 @@ public final class IrisRiverRuntime implements AutoCloseable {
configured.getDepthMultiplier(), configured.getDepthMultiplier(),
configured.getBodyWavelength(), configured.getBodyWavelength(),
configured.getBodyDetailWavelength(), configured.getBodyDetailWavelength(),
configured.getBodyDetailInfluence(),
configured.getWidthVariation(), configured.getWidthVariation(),
configured.getBankVariation(), configured.getBankVariation(),
configured.getDepthVariation(), configured.getDepthVariation(),
@@ -757,6 +889,24 @@ public final class IrisRiverRuntime implements AutoCloseable {
return unit(hash) < chance; return unit(hash) < chance;
} }
private boolean deepPoolReachEligible(
IrisRiverDeepPools deepPools,
RiverReach reach
) {
CenterlinePosition center = centerlinePosition(reach, 0.5D);
EffectiveRiverSettings settings = settingsAt(center.x(), center.z());
double chance = clamp01(effectiveChance(
deepPools.getReach(),
deepPoolReachNoise,
(int) StrictMath.floor(center.x()),
(int) StrictMath.floor(center.z())
) * settings.caveEntryMultiplier());
long hash = art.arcane.iris.engine.river.RiverNetwork.mix(
seed ^ reach.id().stableId() ^ DEEP_POOL_REACH_GATE_SALT
);
return unit(hash) < chance;
}
private TerminalCaveAnchor terminalCaveAnchor(RiverReach reach) { private TerminalCaveAnchor terminalCaveAnchor(RiverReach reach) {
if (!caveHydrologyActive || !reach.terminal() || reach.state() != RiverRouteState.WET) { if (!caveHydrologyActive || !reach.terminal() || reach.state() != RiverRouteState.WET) {
return null; return null;
@@ -809,8 +959,11 @@ public final class IrisRiverRuntime implements AutoCloseable {
} }
double waterSurface(RiverReach reach, double alongReach, boolean naturalOcean) { double waterSurface(RiverReach reach, double alongReach, boolean naturalOcean) {
if (reach == null || water.getMode() == IrisRiverWaterMode.SEA_LEVEL || naturalOcean) { if (naturalOcean) {
return fluidHeight; return dimensionFluidHeight;
}
if (reach == null || water.getMode() == IrisRiverWaterMode.FIXED) {
return riverFluidHeight;
} }
return terracedWaterSurface( return terracedWaterSurface(
reach.from().hydraulicHeight(), reach.from().hydraulicHeight(),
@@ -883,10 +1036,10 @@ public final class IrisRiverRuntime implements AutoCloseable {
private int nodeWaterHead(double naturalNodeHeight, int dropHeight) { private int nodeWaterHead(double naturalNodeHeight, int dropHeight) {
int availableRise = Math.max(0, water.getMaximumPoolRise()); int availableRise = Math.max(0, water.getMaximumPoolRise());
int maximumHead = fluidHeight + availableRise; int maximumHead = riverFluidHeight + availableRise;
int naturalHead = (int) StrictMath.floor(naturalNodeHeight - 1D); int naturalHead = (int) StrictMath.floor(naturalNodeHeight - 1D);
int clamped = Math.max(fluidHeight, Math.min(maximumHead, naturalHead)); int clamped = Math.max(riverFluidHeight, Math.min(maximumHead, naturalHead));
return fluidHeight + Math.floorDiv(clamped - fluidHeight, dropHeight) * dropHeight; return riverFluidHeight + Math.floorDiv(clamped - riverFluidHeight, dropHeight) * dropHeight;
} }
private static boolean isNaturalOcean(IrisBiome biome) { private static boolean isNaturalOcean(IrisBiome biome) {
@@ -1011,6 +1164,25 @@ public final class IrisRiverRuntime implements AutoCloseable {
return (hash >>> 11) * 0x1.0p-53; return (hash >>> 11) * 0x1.0p-53;
} }
private static final class TunnelSampleChunk {
private final AtomicReferenceArray<Object> samples = new AtomicReferenceArray<>(256);
private IrisRiverTunnelSample sample(IrisRiverRuntime runtime, int x, int z) {
int index = ((x & 15) << 4) | (z & 15);
Object cached = samples.get(index);
if (cached == null) {
IrisRiverTunnelSample computed = runtime.sampleTunnel((double) x, (double) z);
Object encoded = computed == null ? ABSENT_TUNNEL_SAMPLE : computed;
if (samples.compareAndSet(index, null, encoded)) {
cached = encoded;
} else {
cached = samples.get(index);
}
}
return cached == ABSENT_TUNNEL_SAMPLE ? null : (IrisRiverTunnelSample) cached;
}
}
private final class RuntimeTerrainSampler implements RiverTerrainSampler { private final class RuntimeTerrainSampler implements RiverTerrainSampler {
private final IrisRiverTopology topology; private final IrisRiverTopology topology;
@@ -1022,11 +1194,11 @@ public final class IrisRiverRuntime implements AutoCloseable {
public RiverTerrainNodeSample sampleNode(int blockX, int blockZ) { public RiverTerrainNodeSample sampleNode(int blockX, int blockZ) {
boolean oceanIntent = Boolean.TRUE.equals(naturalOcean.get(blockX, blockZ)); boolean oceanIntent = Boolean.TRUE.equals(naturalOcean.get(blockX, blockZ));
boolean naturalHeightRequired = topology.getTerrainHeightWeight() > 0D boolean naturalHeightRequired = topology.getTerrainHeightWeight() > 0D
|| water.getMode() != IrisRiverWaterMode.SEA_LEVEL || water.getMode() != IrisRiverWaterMode.FIXED
|| oceanIntent; || oceanIntent;
double sampledNaturalHeight = naturalHeightRequired double sampledNaturalHeight = naturalHeightRequired
? naturalHeight.get(blockX, blockZ) ? naturalHeight.get(blockX, blockZ)
: fluidHeight; : dimensionFluidHeight;
boolean sampledOcean = oceanIntent && isSubmergedOutlet(sampledNaturalHeight); boolean sampledOcean = oceanIntent && isSubmergedOutlet(sampledNaturalHeight);
IrisRegion sampledRegion = region.get(blockX, blockZ); IrisRegion sampledRegion = region.get(blockX, blockZ);
IrisBiome sampledBiome = biomeRiverOverridesPossible ? naturalBiome.get(blockX, blockZ) : null; IrisBiome sampledBiome = biomeRiverOverridesPossible ? naturalBiome.get(blockX, blockZ) : null;
@@ -1068,7 +1240,7 @@ public final class IrisRiverRuntime implements AutoCloseable {
} }
private boolean isSubmergedOutlet(double sampledNaturalHeight) { private boolean isSubmergedOutlet(double sampledNaturalHeight) {
return Math.round(sampledNaturalHeight) < Math.round(fluidHeight); return Math.round(sampledNaturalHeight) < Math.round(dimensionFluidHeight);
} }
@Override @Override
@@ -1150,8 +1322,8 @@ public final class IrisRiverRuntime implements AutoCloseable {
} }
maximumIncision *= settings.maxIncisionMultiplier(); maximumIncision *= settings.maxIncisionMultiplier();
} }
double head = configuration.getWater().getMode() == IrisRiverWaterMode.SEA_LEVEL double head = configuration.getWater().getMode() == IrisRiverWaterMode.FIXED
? fluidHeight ? riverFluidHeight
: terracedWaterSurface( : terracedWaterSurface(
context.from().hydraulicHeight(), context.from().hydraulicHeight(),
context.to().hydraulicHeight(), context.to().hydraulicHeight(),
@@ -12,7 +12,8 @@ public record IrisRiverRuntimeContext(
long seed, long seed,
IrisRiverNetwork configuration, IrisRiverNetwork configuration,
IrisData data, IrisData data,
int fluidHeight, int riverFluidHeight,
int dimensionFluidHeight,
boolean boreMantleActive, boolean boreMantleActive,
boolean caveHydrologyActive, boolean caveHydrologyActive,
boolean blockingRoutingPossible, boolean blockingRoutingPossible,
@@ -52,6 +52,7 @@ import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.event.entity.CreatureSpawnEvent; import org.bukkit.event.entity.CreatureSpawnEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.Plugin; import org.bukkit.plugin.Plugin;
import org.bukkit.util.Vector; import org.bukkit.util.Vector;
@@ -236,6 +237,11 @@ public final class BukkitPlatform implements IrisPlatform {
return io.papermc.lib.PaperLib.teleportAsync(entity, destination); return io.papermc.lib.PaperLib.teleportAsync(entity, destination);
} }
public static java.util.concurrent.CompletableFuture<Boolean> teleportAsync(Entity entity, Location destination,
PlayerTeleportEvent.TeleportCause cause) {
return io.papermc.lib.PaperLib.teleportAsync(entity, destination, cause);
}
public static boolean isPaperServer() { public static boolean isPaperServer() {
return io.papermc.lib.PaperLib.isPaper(); return io.papermc.lib.PaperLib.isPaper();
} }
@@ -19,12 +19,55 @@ public class SlimJar {
private static final ReentrantLock lock = new ReentrantLock(); private static final ReentrantLock lock = new ReentrantLock();
private static final AtomicBoolean loaded = new AtomicBoolean(); private static final AtomicBoolean loaded = new AtomicBoolean();
public static void loadBootstrap(Path downloadPath, BootstrapLogger logger) {
if (loaded.get()) {
return;
}
lock.lock();
try {
if (loaded.get()) {
return;
}
ApplicationBuilder.appending("Iris")
.injectableFactory(InjectableFactory.selecting(
InjectableFactory.ERROR,
InjectableFactory.INJECTABLE,
InjectableFactory.WRAPPED,
InjectableFactory.UNSAFE))
.downloadDirectoryPath(downloadPath)
.logger(new ProcessLogger() {
@Override
public void info(@NotNull String message, @Nullable Object... args) {
logger.info(message.formatted(args));
}
@Override
public void error(@NotNull String message, @Nullable Object... args) {
logger.error(message.formatted(args));
}
@Override
public void debug(@NotNull String message, @Nullable Object... args) {
logger.debug(message.formatted(args));
}
})
.build();
loaded.set(true);
} finally {
lock.unlock();
}
}
public static void load() { public static void load() {
if (loaded.get()) return; if (loaded.get()) {
return;
}
lock.lock(); lock.lock();
try { try {
if (loaded.getAndSet(true)) return; if (loaded.get()) {
return;
}
VolmitPlugin plugin = BukkitPlatform.volmitPlugin(); VolmitPlugin plugin = BukkitPlatform.volmitPlugin();
Path downloadPath = plugin.getDataFolder("cache", "libraries").toPath(); Path downloadPath = plugin.getDataFolder("cache", "libraries").toPath();
debug(plugin, "Loading libraries..."); debug(plugin, "Loading libraries...");
@@ -58,6 +101,7 @@ public class SlimJar {
}) })
.build(); .build();
} }
loaded.set(true);
debug(plugin, "Libraries loaded successfully!"); debug(plugin, "Libraries loaded successfully!");
} finally { } finally {
lock.unlock(); lock.unlock();
@@ -69,4 +113,12 @@ public class SlimJar {
plugin.getLogger().info("[DEBUG] " + message); plugin.getLogger().info("[DEBUG] " + message);
} }
} }
public interface BootstrapLogger {
void info(String message);
void error(String message);
void debug(String message);
}
} }
@@ -303,6 +303,43 @@ public class J {
return true; return true;
} }
public static CompletableFuture<Void> runRegionFuture(
World world,
int chunkX,
int chunkZ,
Runnable runnable
) {
if (world == null || runnable == null) {
return CompletableFuture.failedFuture(new IllegalArgumentException(
"Region task world and runnable are required."));
}
if (isFolia()) {
CompletableFuture<Void> future = new CompletableFuture<>();
if (isOwnedByCurrentRegion(world, chunkX, chunkZ)) {
settle(future, runnable);
return future;
}
if (!runRegionImmediate(
world,
chunkX,
chunkZ,
() -> settle(future, runnable))) {
future.completeExceptionally(new IllegalStateException(
"Failed to schedule region task for " + world.getName()
+ "@" + chunkX + "," + chunkZ + "."));
}
return future;
}
if (isPrimaryThread()) {
CompletableFuture<Void> future = new CompletableFuture<>();
settle(future, runnable);
return future;
}
return sfut(runnable);
}
public static boolean runGlobal(Runnable runnable) { public static boolean runGlobal(Runnable runnable) {
if (runnable == null) { if (runnable == null) {
return false; return false;
@@ -1,6 +1,7 @@
package art.arcane.iris.util.project.matter.slices; package art.arcane.iris.util.project.matter.slices;
import art.arcane.iris.engine.river.cave.RiverCaveAction; import art.arcane.iris.engine.river.cave.RiverCaveAction;
import art.arcane.iris.engine.river.cave.RiverCaveFluidKind;
import art.arcane.iris.engine.river.cave.RiverCaveHydrology; import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
import art.arcane.volmlib.util.data.palette.Palette; import art.arcane.volmlib.util.data.palette.Palette;
import art.arcane.volmlib.util.matter.Sliced; import art.arcane.volmlib.util.matter.Sliced;
@@ -28,19 +29,21 @@ public final class RiverCaveHydrologyMatter extends RawMatter<RiverCaveHydrology
@Override @Override
public void writeNode(RiverCaveHydrology hydrology, DataOutputStream output) throws IOException { public void writeNode(RiverCaveHydrology hydrology, DataOutputStream output) throws IOException {
output.writeByte(actionCode(hydrology.action())); output.writeByte(actionCode(hydrology.action()));
output.writeByte(fluidKindCode(hydrology.fluidKind()));
output.writeUTF(hydrology.floodedBiomeKey()); output.writeUTF(hydrology.floodedBiomeKey());
} }
@Override @Override
public RiverCaveHydrology readNode(DataInputStream input) throws IOException { public RiverCaveHydrology readNode(DataInputStream input) throws IOException {
RiverCaveAction action = actionFromCode(input.readUnsignedByte()); RiverCaveAction action = actionFromCode(input.readUnsignedByte());
return new RiverCaveHydrology(action, input.readUTF()); RiverCaveFluidKind fluidKind = fluidKindFromCode(input.readUnsignedByte());
return new RiverCaveHydrology(action, input.readUTF(), fluidKind);
} }
private int actionCode(RiverCaveAction action) { private int actionCode(RiverCaveAction action) {
return switch (action) { return switch (action) {
case WET_SOURCE -> 1; case WET_SOURCE -> 1;
case FALLING_WATER -> 2; case FALLING_FLUID -> 2;
case DRY_AIR -> 3; case DRY_AIR -> 3;
case SEAL_GUARD -> 4; case SEAL_GUARD -> 4;
}; };
@@ -49,10 +52,25 @@ public final class RiverCaveHydrologyMatter extends RawMatter<RiverCaveHydrology
private RiverCaveAction actionFromCode(int code) throws IOException { private RiverCaveAction actionFromCode(int code) throws IOException {
return switch (code) { return switch (code) {
case 1 -> RiverCaveAction.WET_SOURCE; case 1 -> RiverCaveAction.WET_SOURCE;
case 2 -> RiverCaveAction.FALLING_WATER; case 2 -> RiverCaveAction.FALLING_FLUID;
case 3 -> RiverCaveAction.DRY_AIR; case 3 -> RiverCaveAction.DRY_AIR;
case 4 -> RiverCaveAction.SEAL_GUARD; case 4 -> RiverCaveAction.SEAL_GUARD;
default -> throw new IOException("Unknown river cave hydrology action code " + code); default -> throw new IOException("Unknown river cave hydrology action code " + code);
}; };
} }
private int fluidKindCode(RiverCaveFluidKind fluidKind) {
return switch (fluidKind) {
case RIVER -> 1;
case DEEP_POOL -> 2;
};
}
private RiverCaveFluidKind fluidKindFromCode(int code) throws IOException {
return switch (code) {
case 1 -> RiverCaveFluidKind.RIVER;
case 2 -> RiverCaveFluidKind.DEEP_POOL;
default -> throw new IOException("Unknown river cave fluid-kind code " + code);
};
}
} }
@@ -447,6 +447,15 @@ public class ServerConfiguratorDatapackFingerprintTest {
assertFalse(ServerConfigurator.loadedRegistrySatisfies( assertFalse(ServerConfigurator.loadedRegistrySatisfies(
loaded, loaded,
Map.of("worldgen/biome/overworld:new", "biome-new"))); Map.of("worldgen/biome/overworld:new", "biome-new")));
assertFalse(ServerConfigurator.runtimeRequiresRegistryRestart(
loaded,
Map.of(
"dimension_type/iris:overworld", "dimension-a",
"worldgen/biome/overworld:forest", "biome-a")));
assertTrue(ServerConfigurator.runtimeRequiresRegistryRestart(
loaded,
Map.of("dimension_type/iris:overworld", "dimension-b")));
assertFalse(ServerConfigurator.runtimeRequiresRegistryRestart(loaded, Map.of()));
} }
@Test @Test
@@ -0,0 +1,106 @@
package art.arcane.iris.core;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.nms.INMSBinding;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.collection.KList;
import org.bukkit.Bukkit;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.io.File;
import java.util.List;
import java.util.stream.Stream;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
public class ServerConfiguratorRegistryVerificationTest {
@Test
public void freshlyCompiledRegistryMissReportsOneRestartWarning() {
VerificationFixture fixture = missingRegistryFixture();
try (MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class);
MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<INMS> nms = mockStatic(INMS.class)) {
nms.when(INMS::get).thenReturn(fixture.binding());
bukkit.when(Bukkit::getOnlinePlayers).thenReturn(List.of());
assertTrue(ServerConfigurator.verifyDataPacksPost(Stream.of(fixture.data())));
logging.verify(() -> IrisLogging.debug("Checking Pack: packs/overworld"));
logging.verify(() -> IrisLogging.warn(ServerConfigurator.POST_COMPILE_RESTART_WARNING));
logging.verifyNoMoreInteractions();
}
}
@Test
public void worldCreationRegistryMissRetainsFullOperationalError() {
VerificationFixture fixture = missingRegistryFixture();
try (MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class);
MockedStatic<INMS> nms = mockStatic(INMS.class)) {
nms.when(INMS::get).thenReturn(fixture.binding());
assertFalse(ServerConfigurator.verifyDataPackInstalled(fixture.dimension()));
logging.verify(() -> IrisLogging.warn(
"The Biome overworld:missing is not registered on the server."));
logging.verify(() -> IrisLogging.warn(
"The Dimension Type for packs/overworld/dimensions/overworld.json is not registered on the server."));
logging.verify(() -> IrisLogging.error(
"The Pack overworld is INCAPABLE of generating custom biomes"));
logging.verify(() -> IrisLogging.error(
"If not done automatically, restart your server before generating with this pack!"));
logging.verifyNoMoreInteractions();
}
}
@SuppressWarnings("unchecked")
private static VerificationFixture missingRegistryFixture() {
IrisBiomeCustom customBiome = mock(IrisBiomeCustom.class);
when(customBiome.getId()).thenReturn("missing");
IrisBiome biome = mock(IrisBiome.class);
when(biome.isCustom()).thenReturn(true);
when(biome.getCustomDerivitives()).thenReturn(new KList<>(customBiome));
IrisData data = mock(IrisData.class);
when(data.getDataFolder()).thenReturn(new File("packs/overworld"));
IrisDimension dimension = mock(IrisDimension.class);
when(dimension.getAllBiomes(any())).thenReturn(new KList<>(biome));
when(dimension.getLoadKey()).thenReturn("overworld");
when(dimension.getLoadFile()).thenReturn(
new File("packs/overworld/dimensions/overworld.json"));
when(dimension.getDimensionTypeKey()).thenReturn("iris:overworld");
when(dimension.getLoader()).thenReturn(data);
ResourceLoader<IrisDimension> loader = mock(ResourceLoader.class);
when(loader.getPossibleKeys()).thenReturn(new String[]{"overworld"});
when(loader.loadAll(any(String[].class))).thenReturn(new KList<>(dimension));
when(data.getDimensionLoader()).thenReturn(loader);
INMSBinding binding = mock(INMSBinding.class);
when(binding.supportsDataPacks()).thenReturn(true);
when(binding.getCustomBiomeBaseFor("overworld:missing")).thenReturn(null);
when(binding.missingDimensionTypes("iris:overworld")).thenReturn(true);
return new VerificationFixture(data, dimension, binding);
}
private record VerificationFixture(
IrisData data,
IrisDimension dimension,
INMSBinding binding
) {
}
}
@@ -11,6 +11,7 @@ import art.arcane.volmlib.util.localization.MessageValue;
import art.arcane.volmlib.util.localization.PluralValue; import art.arcane.volmlib.util.localization.PluralValue;
import art.arcane.volmlib.util.localization.TextValue; import art.arcane.volmlib.util.localization.TextValue;
import art.arcane.volmlib.util.localization.VolmitLocales; import art.arcane.volmlib.util.localization.VolmitLocales;
import com.google.gson.JsonArray;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
@@ -174,6 +175,29 @@ public class IrisLanguageTest {
} }
} }
@Test
public void compactBukkitLocaleUsesSortedCatalogPositionsAndEnglishFallbacks() {
List<String> messageIds = IrisLanguage.catalog().ids().stream()
.filter(id -> !id.startsWith("iris.modded."))
.sorted()
.toList();
String translatedId = IrisMessages.COMMAND_UNKNOWN.id();
JsonArray values = new JsonArray(messageIds.size());
for (int i = 0; i < messageIds.size(); i++) {
values.add(messageIds.get(i).equals(translatedId) ? "Unbekannter Iris-Befehl" : null);
}
JsonArray compact = new JsonArray(2);
compact.add("de_DE");
compact.add(values);
LocaleOverlay overlay = IrisLanguage.parseOverlay("test", "de_DE", compact.toString());
assertEquals(Set.of(translatedId), overlay.values().keySet());
assertEquals(
"Unbekannter Iris-Befehl",
((TextValue) overlay.value(translatedId)).template());
}
@Test @Test
public void bundledServerResourcesExactlyMatchNonEnglishManifest() throws Exception { public void bundledServerResourcesExactlyMatchNonEnglishManifest() throws Exception {
Set<String> expected = VolmitLocales.nonEnglish().stream() Set<String> expected = VolmitLocales.nonEnglish().stream()
@@ -651,6 +651,35 @@ public class PackDownloaderTest {
assertEquals(0, PackDownloader.downloadLockCount()); assertEquals(0, PackDownloader.downloadLockCount());
} }
@Test
public void invalidBuiltInRiverSchemaPreservesExistingTarget() throws Exception {
File packsFolder = temp.newFolder("invalid-river-packs");
File target = writePack(packsFolder.toPath().resolve("overworld"), "overworld", "old");
File extracted = writePack(temp.newFolder("invalid-river-source").toPath(), "overworld", "new");
Files.writeString(
extracted.toPath().resolve("dimensions/overworld.json"),
"{\"name\":\"Overworld\",\"regions\":[\"local\"],\"logicalHeight\":256,"
+ "\"dimensionHeight\":{\"min\":-64,\"max\":320},\"rivers\":{\"enabled\":true,"
+ "\"terrain\":{},\"water\":{\"mode\":\"SEA_LEVEL\"}}}",
StandardCharsets.UTF_8
);
PackDownloader.PackInstallResult result = PackDownloader.installExtractedPack(
packsFolder,
extracted,
true,
"overworld",
ignored -> {
}
);
assertNull(result);
assertEquals("old", Files.readString(target.toPath().resolve("state.txt"), StandardCharsets.UTF_8));
assertTrue(Files.isRegularFile(target.toPath().resolve("regions/local.json")));
assertTransactionStateClean(packsFolder);
assertEquals(0, PackDownloader.downloadLockCount());
}
@Test @Test
public void forceOverwriteReplacesEnginelessLoadedPackData() throws Exception { public void forceOverwriteReplacesEnginelessLoadedPackData() throws Exception {
// A registered loader with no engines is a stale catalog registration (startup // A registered loader with no engines is a stale catalog registration (startup
@@ -127,8 +127,9 @@ public class PackRiverValidatorTest {
{ {
"id": "trunk", "id": "trunk",
"seed": 17, "seed": 17,
"bodyWavelength": 31, "bodyWavelength": 7,
"bodyDetailWavelength": 16385, "bodyDetailWavelength": 16385,
"bodyDetailInfluence": 1.1,
"widthVariation": -0.1, "widthVariation": -0.1,
"bankVariation": 0.876, "bankVariation": 0.876,
"depthVariation": -0.1, "depthVariation": -0.1,
@@ -150,8 +151,9 @@ public class PackRiverValidatorTest {
PackRiverValidator.Validation result = validate(pack); PackRiverValidator.Validation result = validate(pack);
assertContains(result.errors(), "rivers.terrain.worms[0].bodyWavelength must be at least 32"); assertContains(result.errors(), "rivers.terrain.worms[0].bodyWavelength must be at least 8");
assertContains(result.errors(), "rivers.terrain.worms[0].bodyDetailWavelength must be at most 16384"); assertContains(result.errors(), "rivers.terrain.worms[0].bodyDetailWavelength must be at most 16384");
assertContains(result.errors(), "rivers.terrain.worms[0].bodyDetailInfluence must be at most 1");
assertContains(result.errors(), "rivers.terrain.worms[0].widthVariation must be at least 0"); assertContains(result.errors(), "rivers.terrain.worms[0].widthVariation must be at least 0");
assertContains(result.errors(), "rivers.terrain.worms[0].bankVariation must be at most 0.875"); assertContains(result.errors(), "rivers.terrain.worms[0].bankVariation must be at most 0.875");
assertContains(result.errors(), "rivers.terrain.worms[0].depthVariation must be at least 0"); assertContains(result.errors(), "rivers.terrain.worms[0].depthVariation must be at least 0");
@@ -357,6 +359,140 @@ public class PackRiverValidatorTest {
assertContains(result.errors(), "rivers.water.dropHeight must not exceed maximumPoolRise"); assertContains(result.errors(), "rivers.water.dropHeight must not exceed maximumPoolRise");
} }
@Test
public void acceptsIndependentCaveLavaRiverHeightAndPalette() throws Exception {
File pack = pack("""
{
"regions": ["region"],
"dimensionHeight": {"min": -64, "max": 320},
"fluidHeight": 63,
"rivers": {
"enabled": true,
"terrain": {"worms": [{"id": "river"}]},
"water": {
"mode": "FIXED",
"fluidHeight": -48,
"fluidPalette": {
"palette": [{"block": "minecraft:lava"}]
}
}
}
}
""");
PackRiverValidator.Validation result = validate(pack);
assertTrue(result.errors().toString(), result.errors().isEmpty());
}
@Test
public void acceptsSparseBlobbyDeepLavaPoolsWithIndependentHeight() throws Exception {
File pack = pack("""
{
"regions": ["region"],
"dimensionHeight": {"min": -256, "max": 512},
"rivers": {
"enabled": true,
"terrain": {"worms": [{"id": "river"}]},
"caves": {
"deepPools": {
"enabled": true,
"reach": {
"chance": 0.08,
"influence": 0.04,
"style": {"style": "IRIS", "zoom": 4096}
},
"minimumSpacing": 768,
"maximumPerReach": 1,
"minimumFluidY": -224,
"maximumFluidY": -108,
"searchRadius": 20,
"searchAttempts": 12,
"horizontalRadius": 24,
"verticalRadius": 10,
"dryHeadroom": 5,
"shapeStyle": {"style": "IRIS", "zoom": 12},
"shapeVariation": 0.6,
"warpStyle": {"style": "IRIS", "zoom": 24},
"warpStrength": 8,
"maximumVolume": 65536,
"fluidPalette": {
"palette": [{"block": "minecraft:lava"}]
}
}
}
}
}
""");
PackRiverValidator.Validation result = validate(pack);
assertTrue(result.errors().toString(), result.errors().isEmpty());
}
@Test
public void rejectsUnsafeDeepPoolEnvelopeShapeAndPalette() throws Exception {
File pack = pack("""
{
"regions": ["region"],
"dimensionHeight": {"min": -64, "max": 320},
"rivers": {
"enabled": true,
"terrain": {"worms": [{"id": "river"}]},
"caves": {
"deepPools": {
"enabled": true,
"reach": {"chance": 0},
"minimumFluidY": -90,
"maximumFluidY": -120,
"searchRadius": 120,
"horizontalRadius": 20,
"verticalRadius": 8,
"dryHeadroom": 8,
"maximumVolume": 64,
"fluidPalette": {"palette": []}
}
}
}
}
""");
PackRiverValidator.Validation result = validate(pack);
assertContains(result.errors(), "deepPools.minimumFluidY must not exceed maximumFluidY");
assertContains(result.errors(), "deepPools.dryHeadroom must be smaller than verticalRadius");
assertContains(result.errors(), "deepPools.searchRadius plus horizontalRadius must not exceed 128");
assertContains(result.errors(), "deepPools.maximumVolume must be at least");
assertContains(result.errors(), "deepPools.fluidPalette.palette must contain at least one fluid block");
assertContains(result.errors(), "deepPools fluid range and chamber envelope must remain inside dimensionHeight");
assertContains(result.warnings(), "deepPools is enabled but its reach gate cannot accept any pools");
}
@Test
public void rejectsRetiredWaterModeInvalidPaletteAndOutOfBoundsHeight() throws Exception {
File pack = pack("""
{
"regions": ["region"],
"dimensionHeight": {"min": -64, "max": 320},
"rivers": {
"enabled": true,
"terrain": {"worms": [{"id": "river"}]},
"water": {
"mode": "SEA_LEVEL",
"fluidHeight": -80,
"fluidPalette": {"palette": []}
}
}
}
""");
PackRiverValidator.Validation result = validate(pack);
assertContains(result.errors(), "rivers.water.mode must be one of");
assertContains(result.errors(), "rivers.water.fluidHeight must remain inside dimensionHeight");
assertContains(result.errors(), "rivers.water.fluidPalette.palette must contain at least one fluid block");
}
@Test @Test
public void rejectsPathologicalCombinedTopologyComplexity() throws Exception { public void rejectsPathologicalCombinedTopologyComplexity() throws Exception {
File pack = pack(""" File pack = pack("""
@@ -3,11 +3,17 @@ package art.arcane.iris.core.pregenerator.methods;
import org.junit.Test; import org.junit.Test;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame; import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public class AsyncPregenMethodConcurrencyCapTest { public class AsyncPregenMethodConcurrencyCapTest {
@Test @Test
@@ -77,4 +83,31 @@ public class AsyncPregenMethodConcurrencyCapTest {
assertEquals(0, slowRequests.get()); assertEquals(0, slowRequests.get());
assertEquals("generated", request.join()); assertEquals("generated", request.join());
} }
@Test
public void closeDrainWaitsPastWarningIntervalsUntilEveryPermitReturns() throws Exception {
Semaphore semaphore = new Semaphore(0);
AtomicInteger warnings = new AtomicInteger();
ExecutorService executor = Executors.newSingleThreadExecutor();
try {
Future<Boolean> drain = executor.submit(() -> AsyncPregenMethod.awaitDrain(
semaphore,
2,
10L,
TimeUnit.MILLISECONDS,
warnings::incrementAndGet
));
while (warnings.get() == 0) {
Thread.onSpinWait();
}
semaphore.release(2);
assertFalse(drain.get(1L, TimeUnit.SECONDS));
assertTrue(warnings.get() > 0);
assertEquals(0, semaphore.availablePermits());
} finally {
executor.shutdownNow();
}
}
} }

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