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/
.repro/
.perf/
.release-gate/
# Throwaway worktree copies used by the API / PlaceholderAPI rebuild lanes. Generated, never source.
.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.IrisBiomeCustom;
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.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.math.RNG;
@@ -521,6 +522,16 @@ public class CustomBiomeSource extends BiomeSource {
int y,
int z,
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);
Holder<Biome> cachedHolder = noiseBiomeCache.get(cacheKey);
@@ -528,7 +539,7 @@ public class CustomBiomeSource extends BiomeSource {
return cachedHolder;
}
Holder<Biome> resolvedHolder = resolveVisibleBiomeHolder(x, y, z);
Holder<Biome> resolvedHolder = resolveVisibleBiomeHolder(x, y, z, resolverState);
Holder<Biome> existingHolder = noiseBiomeCache.putIfAbsent(cacheKey, resolvedHolder);
if (existingHolder != null) {
return existingHolder;
@@ -602,8 +613,13 @@ public class CustomBiomeSource extends BiomeSource {
return holder;
}
private Holder<Biome> resolveVisibleBiomeHolder(int x, int y, int z) {
BiomeResolution resolution = resolveBiomeResolution(x, y, z);
private Holder<Biome> resolveVisibleBiomeHolder(
int x,
int y,
int z,
IrisDimensionCarvingResolver.State resolverState
) {
BiomeResolution resolution = resolveBiomeResolution(x, y, z, resolverState);
if (resolution == null) {
return getFallbackBiome();
}
@@ -636,6 +652,15 @@ public class CustomBiomeSource extends BiomeSource {
}
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()) {
return null;
}
@@ -657,7 +682,7 @@ public class CustomBiomeSource extends BiomeSource {
int surfaceInternalY = engine.getComplex().getHeightStream().get(blockX, blockZ).intValue();
underground = internalY <= surfaceInternalY - 8;
irisBiome = underground
? engine.getCaveBiome(blockX, internalY, blockZ)
? engine.getCaveBiome(blockX, internalY, blockZ, resolverState)
: engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
} else {
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.platform.BukkitChunkGenerator;
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.IrisNativeStructureDecision;
import art.arcane.iris.nativegen.NativeStructureGenerationException;
@@ -643,8 +644,10 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_create_biomes");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
customBiomeSource.prepareVisibleBiomeBatch();
IrisDimensionCarvingResolver.State resolverState = new IrisDimensionCarvingResolver.State();
ichunkaccess.fillBiomesFromNoise(
customBiomeSource::getVisibleNoiseBiomeWithActiveGenerationLease,
(x, y, z, sampler) -> customBiomeSource.getVisibleNoiseBiomeWithActiveGenerationLease(
x, y, z, sampler, resolverState),
randomstate.sampler());
return CompletableFuture.completedFuture(ichunkaccess);
}
@@ -258,7 +258,8 @@ public class IrisChunkGeneratorFailureContractTest {
assertTrue(createBiomes.contains("requireGenerationStage(\"bukkit_nms_create_biomes\")"));
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(carvers.contains("delegate.applyCarvers("));
assertTrue(noise.contains("requireNoiseGenerationStage("));
@@ -573,7 +573,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
}
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.pack.DefaultPackBootstrapProvisioner;
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.PluginBootstrap;
import io.papermc.paper.plugin.lifecycle.event.LifecycleEvent;
@@ -26,6 +27,7 @@ public final class IrisBootstrap implements PluginBootstrap {
public void bootstrap(BootstrapContext context) {
WorldReplacementBootstrapMarker.markBootstrapped();
try {
loadRuntimeLibraries(context);
BukkitStartupPaths startupPaths = BukkitStartupPaths.resolveCurrent();
reconcilePendingWorldReplacements(context, startupPaths);
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(
BootstrapContext context,
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.List;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
@@ -412,7 +413,8 @@ public class CommandStudio implements DirectorExecutor {
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));
}
@@ -30,6 +30,7 @@ import org.bukkit.event.Listener;
import java.util.ArrayList;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public final class BukkitGuiHost implements GuiHost.Provider {
@@ -72,8 +73,8 @@ public final class BukkitGuiHost implements GuiHost.Provider {
}
@Override
public GuiOverlay overlayFor(Engine engine) {
return engine == null ? null : new BukkitVisionOverlay(engine);
public GuiOverlay overlayFor(Engine engine, UUID openerId) {
return engine == null ? null : new BukkitVisionOverlay(engine, openerId);
}
private static final class HotloadListener implements Listener {
@@ -18,7 +18,6 @@
package art.arcane.iris.core.gui;
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine;
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.util.common.scheduling.J;
import art.arcane.volmlib.util.format.Form;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.LivingEntity;
@@ -39,6 +37,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
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 {
private final Engine engine;
private final UUID openerId;
private final AtomicBoolean nativeTeleportActive = new AtomicBoolean();
private final AtomicBoolean playerRefreshQueued = new AtomicBoolean();
private final AtomicLong teleportSequence = new AtomicLong();
private final AtomicReference<VisionTeleportRequest> latestTeleport = new AtomicReference<>();
private volatile List<GuiMarker> playerMarkers = List.of();
public BukkitVisionOverlay(Engine engine) {
public BukkitVisionOverlay(Engine engine, UUID openerId) {
this.engine = engine;
this.openerId = openerId;
}
/**
@@ -177,67 +178,20 @@ public final class BukkitVisionOverlay implements GuiOverlay {
return;
}
List<Player> players = BukkitWorldBinding.players(target);
if (players.isEmpty()) {
Player player = selectPlayer(players);
if (player == null) {
finish(request);
return;
}
Player player = players.get(0);
requestTeleportChunk(request, target, player, world);
});
if (!scheduled) {
finish(request);
}
}
private void requestTeleportChunk(
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);
int blockX = request.blockX;
int blockZ = request.blockZ;
try {
int blockY = engine.getMinHeight() + engine.getHeight(blockX, blockZ, false) + 2;
Location destination = new Location(
world,
blockX + 0.5D,
blockY,
blockZ + 0.5D);
if (!J.runEntity(player, () -> delegateTeleport(
request,
target,
@@ -247,12 +201,25 @@ public final class BukkitVisionOverlay implements GuiOverlay {
fail(request, target, world, new IllegalStateException(
"Failed to schedule the Vision teleport on the player entity."));
}
});
if (!scheduled) {
fail(request, target, world, new IllegalStateException(
"Failed to schedule the Vision surface lookup on its owning region."));
} catch (Throwable failure) {
fail(request, target, world, failure);
}
});
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(
@@ -2,6 +2,7 @@ name: ${name}
version: ${version}
main: ${main}
bootstrapper: ${bootstrapper}
loader: art.arcane.iris.IrisPluginLoader
folia-supported: true
api-version: '${apiVersion}'
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("loader: " + IrisPluginLoader.class.getName()));
assertTrue(metadata.contains("folia-supported: true"));
assertTrue(metadata.contains("load: STARTUP"));
assertFalse(metadata.contains("commands:"));
@@ -1,108 +1,67 @@
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.object.IrisWorld;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
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.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class BukkitVisionOverlayFoliaContractTest {
@Test
public void teleportLoadsTheDestinationChunkBeforeItsOwningRegionReadsTheSurface() 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(");
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() {
public void teleportDelegatesImmediatelyToTheNativeAsyncPath() {
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) {
harness.overlay.teleport(1.5D, 1.5D);
harness.overlay.teleport(33.5D, 33.5D);
firstChunk.complete(harness.chunk);
assertEquals(0, harness.destinations.size());
secondChunk.complete(harness.chunk);
assertEquals(1, harness.destinations.size());
Location destination = harness.destinations.get(0);
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(33, harness.destinations.get(0).getBlockX());
assertEquals(33, harness.destinations.get(0).getBlockZ());
Location destination = harness.destinations.get(0);
assertEquals(-0.5D, destination.getX(), 0D);
assertEquals(-16.5D, destination.getZ(), 0D);
verify(harness.engine).getHeight(-1, -17, false);
}
}
@Test
public void latestRequestRunsAfterAnOlderNativeTeleportSettles() {
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> secondTeleport = new CompletableFuture<>();
harness.nativeTeleports.add(firstTeleport);
@@ -121,42 +80,17 @@ public class BukkitVisionOverlayFoliaContractTest {
}
}
private static void assertBefore(String source, String first, String second) {
int firstIndex = source.indexOf(first);
int secondIndex = source.indexOf(second);
assertTrue("Missing source contract token: " + first, firstIndex >= 0);
assertTrue("Missing source contract token: " + second, secondIndex >= 0);
assertTrue(first + " must occur before " + second, firstIndex < secondIndex);
}
@Test
public void missingOpenerDoesNotTeleportAnotherPlayer() {
VisionHarness harness = new VisionHarness();
harness.binding.when(() -> BukkitWorldBinding.players(harness.target))
.thenReturn(List.of(harness.otherPlayer));
private static int occurrences(String source, String match) {
int count = 0;
int offset = 0;
while ((offset = source.indexOf(match, offset)) >= 0) {
count++;
offset += match.length();
}
return count;
}
try (harness) {
harness.overlay.teleport(1.5D, 1.5D);
private static String method(String source, String signature) {
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);
}
}
assertEquals(0, harness.destinations.size());
}
throw new IllegalArgumentException("Unclosed source contract method: " + signature);
}
private static final class VisionHarness implements AutoCloseable {
@@ -164,11 +98,10 @@ public class BukkitVisionOverlayFoliaContractTest {
private final IrisWorld target;
private final World world;
private final Player player;
private final Chunk chunk;
private final WorldRuntimeControlService runtime;
private final Player otherPlayer;
private final UUID openerId;
private final MockedStatic<J> scheduling;
private final MockedStatic<BukkitWorldBinding> binding;
private final MockedStatic<WorldRuntimeControlService> runtimeAccess;
private final MockedStatic<BukkitPlatform> platform;
private final List<Location> destinations;
private final List<CompletableFuture<Boolean>> nativeTeleports;
@@ -180,33 +113,26 @@ public class BukkitVisionOverlayFoliaContractTest {
target = mock(IrisWorld.class);
world = mock(World.class);
player = mock(Player.class);
chunk = mock(Chunk.class);
runtime = mock(WorldRuntimeControlService.class);
otherPlayer = mock(Player.class);
openerId = UUID.randomUUID();
destinations = new ArrayList<>();
nativeTeleports = new ArrayList<>();
nativeTeleportIndex = new AtomicInteger();
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(player.isOnline()).thenReturn(true);
when(player.getWorld()).thenReturn(world);
when(chunk.getWorld()).thenReturn(world);
when(world.getHighestBlockYAt(anyInt(), anyInt())).thenReturn(70);
when(player.getUniqueId()).thenReturn(openerId);
when(otherPlayer.getUniqueId()).thenReturn(UUID.randomUUID());
scheduling = mockStatic(J.class);
scheduling.when(() -> J.runGlobal(any(Runnable.class))).thenAnswer(invocation -> {
invocation.getArgument(0, Runnable.class).run();
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 -> {
invocation.getArgument(1, Runnable.class).run();
return true;
@@ -214,10 +140,7 @@ public class BukkitVisionOverlayFoliaContractTest {
binding = mockStatic(BukkitWorldBinding.class);
binding.when(() -> BukkitWorldBinding.world(target)).thenReturn(world);
binding.when(() -> BukkitWorldBinding.players(target)).thenReturn(List.of(player));
runtimeAccess = mockStatic(WorldRuntimeControlService.class);
runtimeAccess.when(WorldRuntimeControlService::get).thenReturn(runtime);
binding.when(() -> BukkitWorldBinding.players(target)).thenReturn(List.of(otherPlayer, player));
platform = mockStatic(BukkitPlatform.class);
platform.when(() -> BukkitPlatform.teleportAsync(same(player), any(Location.class)))
@@ -228,26 +151,12 @@ public class BukkitVisionOverlayFoliaContractTest {
? nativeTeleports.get(index)
: CompletableFuture.completedFuture(true);
});
overlay = new BukkitVisionOverlay(engine);
}
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);
overlay = new BukkitVisionOverlay(engine, openerId);
}
@Override
public void close() {
platform.close();
runtimeAccess.close();
binding.close();
scheduling.close();
}
@@ -21,6 +21,7 @@ package art.arcane.iris.fabric;
import art.arcane.iris.modded.ModdedLoader;
import art.arcane.iris.modded.service.ModdedTreeFellerService;
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.permission.v1.PermissionContextOwner;
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.
}
@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
public boolean clientEnvironment() {
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.minecraftforge.common.util.Result;
import net.minecraftforge.event.level.BlockEvent;
import net.minecraftforge.event.level.LevelEvent;
import net.minecraftforge.fml.ModList;
import net.minecraftforge.fml.loading.FMLEnvironment;
import net.minecraftforge.fml.loading.FMLLoader;
@@ -77,6 +78,16 @@ public final class ForgeModdedLoader implements ModdedLoader {
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
public boolean clientEnvironment() {
return FMLEnvironment.dist.isClient();
@@ -56,13 +56,16 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
public final class ModdedDimensionManager {
private static final int TELEPORT_WARM_RADIUS = 0;
private static final Object LOCK = new Object();
private static final ConcurrentHashMap<String, Handle> HANDLES = new ConcurrentHashMap<>();
private static final TicketType TELEPORT_WARM_TICKET = new TicketType(TicketType.NO_TIMEOUT,
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 ModdedDimensionManager() {
@@ -213,15 +216,16 @@ public final class ModdedDimensionManager {
instanceof IrisModdedChunkGenerator irisGenerator
? irisGenerator
: null;
boolean generatorUnbound = false;
boolean unloadEventStarted = false;
try {
evacuate(server, level);
level.save(null, true, false);
unloadEventStarted = true;
ModdedEngineBootstrap.loader().fireDynamicLevelUnload(server, level);
if (generator != null) {
generator.unbindEngine(level);
generatorUnbound = true;
}
ModdedWorldEngines.evictOrThrow(level);
level.save(null, true, false);
serverAccess.removeLevel(server, key);
// Undo snapshots pin the ServerLevel and could replay into the dead level.
art.arcane.iris.modded.command.ModdedObjectUndo.forget(level);
@@ -233,7 +237,7 @@ public final class ModdedDimensionManager {
ModdedIrisLog.info("Iris removed runtime dimension '{}'", dimensionId);
return true;
} 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);
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,
ResourceKey<Level> key, ServerLevel level,
IrisModdedChunkGenerator generator, boolean generatorUnbound,
Throwable failure) {
IrisModdedChunkGenerator generator, boolean unloadEventStarted,
Throwable failure) {
try {
if (!generatorUnbound || generator == null || !serverAccess.hasLevel(server, key)) {
if (!unloadEventStarted || !serverAccess.hasLevel(server, key)) {
return;
}
generator.bindLevel(level);
if (generator != null) {
generator.bindLevel(level);
}
ModdedEngineBootstrap.loader().fireDynamicLevelLoad(server, level);
} catch (Throwable rollbackFailure) {
if (rollbackFailure != failure) {
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) {
ServerLevel level = level(server, dimensionId);
if (level == null) {
return false;
}
int blockX = (int) Math.floor(x);
int blockZ = (int) Math.floor(z);
ChunkPos chunkPos = new ChunkPos(blockX >> 4, blockZ >> 4);
if (level.getChunkSource().hasChunk(chunkPos.x(), chunkPos.z())) {
completeTeleport(player, level, x, y, z, blockX, blockZ);
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;
public static CompletableFuture<Boolean> teleportAsync(
ServerPlayer player,
MinecraftServer server,
String dimensionId,
double x,
double y,
double z
) {
return teleportAsync(player, server, dimensionId, x, y, z,
System.nanoTime() + TimeUnit.SECONDS.toNanos(TELEPORT_TIMEOUT_SECONDS));
}
private static void completeTeleport(ServerPlayer player, ServerLevel level, double x, double y, double z, int blockX, int blockZ) {
double targetY = y;
if (y == Double.MIN_VALUE) {
targetY = level.getHeight(Heightmap.Types.MOTION_BLOCKING, blockX, blockZ);
public static CompletableFuture<Boolean> teleportAsync(
ServerPlayer player,
MinecraftServer server,
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) {
@@ -362,6 +588,7 @@ public final class ModdedDimensionManager {
List.of(),
false);
boolean loadEventStarted = false;
try {
generator.bindLevel(level);
Handle handle = new Handle(dimensionId, pack, packDimensionKey, seed, level, generator);
@@ -371,9 +598,11 @@ public final class ModdedDimensionManager {
+ "': the level was registered concurrently");
}
server.getPlayerList().addWorldborderListener(level);
loadEventStarted = true;
ModdedEngineBootstrap.loader().fireDynamicLevelLoad(server, level);
return handle;
} catch (Throwable error) {
rollbackInjection(server, serverAccess, key, level, generator, error);
rollbackInjection(server, serverAccess, key, level, generator, loadEventStarted, error);
if (error instanceof RuntimeException runtimeException) {
throw runtimeException;
}
@@ -386,7 +615,17 @@ public final class ModdedDimensionManager {
private static void rollbackInjection(MinecraftServer server, ModdedServerAccess serverAccess,
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 {
if (serverAccess.hasLevel(server, key)) {
ServerLevel removed = serverAccess.removeLevel(server, key);
@@ -38,6 +38,10 @@ public interface ModdedLoader {
void invalidateLevelCache(MinecraftServer server);
void fireDynamicLevelLoad(MinecraftServer server, ServerLevel level);
void fireDynamicLevelUnload(MinecraftServer server, ServerLevel level);
boolean clientEnvironment();
Path configDir();
@@ -26,12 +26,14 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
public final class ModdedPrimaryWorldRouter {
private static final int TICK_INTERVAL = 20;
private static final Set<UUID> routed = ConcurrentHashMap.newKeySet();
private static final Set<UUID> inFlight = ConcurrentHashMap.newKeySet();
private static int tickCounter = 0;
private ModdedPrimaryWorldRouter() {
@@ -39,6 +41,7 @@ public final class ModdedPrimaryWorldRouter {
public static void clear() {
routed.clear();
inFlight.clear();
}
/**
@@ -48,6 +51,7 @@ public final class ModdedPrimaryWorldRouter {
public static void forget(UUID player) {
if (player != null) {
routed.remove(player);
inFlight.remove(player);
}
}
@@ -82,17 +86,35 @@ public final class ModdedPrimaryWorldRouter {
List<ServerPlayer> players = new ArrayList<>(server.getPlayerList().getPlayers());
for (ServerPlayer player : players) {
UUID id = player.getUUID();
if (routed.contains(id)) {
if (routed.contains(id) || !inFlight.add(id)) {
continue;
}
if (player.level() != overworld) {
inFlight.remove(id);
routed.add(id);
continue;
}
try {
ModdedDimensionManager.teleport(player, server, primary, player.getX(), Double.MIN_VALUE, player.getZ());
routed.add(id);
CompletableFuture<Boolean> teleport = ModdedDimensionManager.teleportAsync(
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) {
inFlight.remove(id);
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;
}
String dimensionId = level.dimension().identifier().toString();
if (!ModdedDimensionManager.teleport(player, source.getServer(), dimensionId, 8.5D, Double.MIN_VALUE, 8.5D)) {
fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_TELEPORT_FAILED_DIMENSION_IS_NOT_LOADED, MessageArgument.untrusted("dimensionId", dimensionId)));
return 0;
}
MinecraftServer server = source.getServer();
CompletableFuture<Boolean> teleport = ModdedDimensionManager.teleportAsync(
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)));
return 1;
}
@@ -107,7 +107,7 @@ public final class ModdedGuiHost implements GuiHost.Provider {
}
@Override
public GuiOverlay overlayFor(Engine engine) {
public GuiOverlay overlayFor(Engine engine, UUID openerId) {
if (engine == null) {
return null;
}
@@ -115,6 +115,7 @@ public final class ModdedGuiHost implements GuiHost.Provider {
if (level == null || server == 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.modded.ModdedDimensionManager;
import art.arcane.iris.modded.ModdedEngineBootstrap;
import art.arcane.iris.modded.ModdedScheduler;
import art.arcane.iris.modded.ModdedWorkspaceGenerator;
import art.arcane.iris.util.common.parallel.BurstExecutor;
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.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Relative;
import org.zeroturnaround.zip.ZipUtil;
import java.awt.Desktop;
@@ -79,6 +79,8 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Predicate;
@@ -96,6 +98,7 @@ public final class ModdedStudioCommands {
private static final String DEFAULT_TEMPLATE = "example";
private static final UUID CONSOLE_OWNER = new UUID(0L, 0L);
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) -> {
ModdedCommandFeedback.tab(context.getSource());
try {
@@ -192,6 +195,7 @@ public final class ModdedStudioCommands {
}
public static void clear() {
TRANSITIONS.clear();
STUDIOS.clear();
}
@@ -272,7 +276,7 @@ public final class ModdedStudioCommands {
}
ServerPlayer player = source.getPlayer();
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())));
return 1;
}
@@ -402,126 +406,244 @@ public final class ModdedStudioCommands {
String dimensionId = player == null ? studioConsoleDimensionId() : studioDimensionId(player);
MinecraftServer server = source.getServer();
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");
thread.setDaemon(true);
thread.start();
CompletableFuture<Void> transition = TRANSITIONS.submit(
owner,
() -> openTransition(source, server, owner, dimensionId, pack, seed));
reportOpenFailure(source, server, dimensionId, pack, transition);
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 {
File packFolder = new File(ModdedPackCommands.packsRoot(), pack);
if (!new File(packFolder, "dimensions/" + pack + ".json").isFile()) {
server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(
ModdedCommandMessages.MODDED_STUDIO_COMMANDS_REQUIRED_PACK_IS_NOT_INSTALLED_INSTALL_THEN_RESTART,
MessageArgument.untrusted("pack", pack))));
return;
throw new IllegalStateException("Required Studio pack '" + pack
+ "' is not installed with dimensions/" + pack + ".json.");
}
IrisData data = IrisData.get(packFolder);
IrisDimension dimension = data.getDimensionLoader().load(pack);
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))));
return;
throw new IllegalStateException("Studio pack '" + pack
+ "' has no dimensions/" + pack + ".json definition.");
}
try {
ModdedWorkspaceGenerator.writeWorkspace(data, packFolder, true);
} catch (Throwable workspaceError) {
ModdedIrisLog.error("Iris workspace write failed for {}", packFolder, workspaceError);
server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(
ModdedCommandMessages.MODDED_STUDIO_COMMANDS_FAILED_WRITE_WORKSPACE,
MessageArgument.untrusted("value", packFolder.getAbsolutePath()),
MessageArgument.untrusted("value2", String.valueOf(workspaceError.getMessage())))));
}
server.execute(() -> {
if (owner.equals(CONSOLE_OWNER)) {
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)))));
ModdedWorkspaceGenerator.writeWorkspace(data, packFolder, true);
server.execute(() -> executeStudioOpen(
source,
server,
owner,
dimensionId,
pack,
seed,
transition));
} catch (Throwable failure) {
transition.completeExceptionally(failure);
}
}
private static void injectConsole(CommandSourceStack source, MinecraftServer server, String dimensionId, String pack, long seed) {
ModdedDimensionManager.Handle handle;
private static void executeStudioOpen(
CommandSourceStack source,
MinecraftServer server,
UUID owner,
String dimensionId,
String pack,
long seed,
CompletableFuture<Void> transition
) {
ModdedDimensionManager.Handle handle = null;
try {
replaceExistingStudio(server, owner, dimensionId);
handle = ModdedDimensionManager.create(server, dimensionId, pack, pack, seed);
} catch (Throwable e) {
ModdedIrisLog.error("Iris console studio injection failed for {} ({})", dimensionId, pack, e);
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_STUDIO_INJECTION_FAILED, MessageArgument.untrusted("value", e.getClass().getSimpleName()), MessageArgument.trusted("errorMessage", IrisLanguage.errorDetail(e))));
return;
}
STUDIOS.put(CONSOLE_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;
STUDIOS.put(owner, dimensionId);
if (owner.equals(CONSOLE_OWNER)) {
completeConsoleOpen(source, dimensionId, pack, seed, handle);
transition.complete(null);
return;
}
ServerPlayer player = server.getPlayerList().getPlayer(owner);
if (player == null) {
throw new IllegalStateException("Studio owner disconnected before teleport.");
}
ServerLevel studio = handle.level();
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_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_REMOVE_IT_WITH_IRIS_STUDIO_CLOSE));
}
private static void injectAndTeleport(CommandSourceStack source, MinecraftServer server, UUID owner, String dimensionId, String pack, long seed) {
ServerPlayer player = server.getPlayerList().getPlayer(owner);
if (player == null) {
return;
}
ModdedDimensionManager.Handle handle;
try {
handle = ModdedDimensionManager.create(server, dimensionId, pack, pack, seed);
} catch (Throwable e) {
ModdedIrisLog.error("Iris studio injection failed for {} ({})", dimensionId, pack, e);
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;
private static void reportOpenFailure(
CommandSourceStack source,
MinecraftServer server,
String dimensionId,
String pack,
CompletableFuture<Void> transition
) {
transition.whenComplete((ignored, failure) -> {
if (failure == null) {
return;
}
} catch (Throwable e) {
ModdedIrisLog.error("Iris studio surface probe failed for {}", dimensionId, 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_STUDIO_OPEN_NOW_RUNS_SEED_USE_IRIS_STUDIO_CLOSE_WHEN, MessageArgument.untrusted("dimensionId", dimensionId), MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("seed", seed)));
Throwable cause = unwrapFailure(failure);
ModdedIrisLog.error("Iris Studio open failed for '{}' ({})", dimensionId, pack, 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 int close(CommandSourceStack source) {
ServerPlayer player = source.getPlayer();
MinecraftServer server = source.getServer();
UUID owner = player == null ? CONSOLE_OWNER : player.getUUID();
// Commit the ownership drop only after removal succeeds: dropping it first orphaned a
// 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)));
TRANSITIONS.submit(owner, () -> closeTransition(source, server, owner));
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) {
MinecraftServer server = source.getServer();
List<ModdedDimensionManager.Handle> handles = ModdedDimensionManager.handles();
@@ -568,31 +690,102 @@ public final class ModdedStudioCommands {
return 0;
}
MinecraftServer server = source.getServer();
String dimensionId = STUDIOS.get(player.getUUID());
if (dimensionId == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_YOU_DO_NOT_HAVE_OPEN_STUDIO_USE_IRIS_STUDIO_OPEN_2));
return 0;
}
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)));
UUID owner = player.getUUID();
CompletableFuture<Void> transition = TRANSITIONS.submit(
owner,
() -> teleportToStudio(source, server, owner));
reportTeleportFailure(source, server, transition);
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) {
File folder = resolvePack(source, pack);
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.framework.Engine;
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.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.phys.Vec3;
import java.awt.Desktop;
@@ -37,6 +38,8 @@ import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
public final class ModdedVisionOverlay implements GuiOverlay {
@@ -80,11 +83,14 @@ public final class ModdedVisionOverlay implements GuiOverlay {
@Override
public void teleport(double worldX, double worldZ) {
int blockX = (int) worldX;
int blockZ = (int) worldZ;
int blockX = (int) Math.floor(worldX);
int blockZ = (int) Math.floor(worldZ);
server.execute(() -> {
ServerPlayer player = opener == null ? null : server.getPlayerList().getPlayer(opener);
if (player == null) {
if (opener != null) {
return;
}
List<ServerPlayer> players = level.players();
if (players.isEmpty()) {
return;
@@ -92,8 +98,24 @@ public final class ModdedVisionOverlay implements GuiOverlay {
player = players.get(0);
}
int surfaceY = engine.getMinHeight() + engine.getHeight(blockX, blockZ, false) + 2;
int safeY = Math.max(surfaceY, level.getHeight(Heightmap.Types.MOTION_BLOCKING, blockX, blockZ) + 1);
player.teleportTo(level, blockX + 0.5D, safeY, blockZ + 0.5D, java.util.Set.of(), player.getYRot(), player.getXRot(), false);
CompletableFuture<Boolean> teleport = ModdedDimensionManager.teleportAsync(
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 failure = catchBlock(reinjection);
assertTrue(failure.contains("LOGGER.error("));
assertTrue(failure.contains("ModdedIrisLog.error("));
assertFalse(failure.contains("e.toString()"));
assertFalse(failure.contains("throw new IllegalStateException("));
assertTrue(reinjection.contains("injected++;"));
@@ -152,7 +152,7 @@ public class ModdedLifecycleFailureContractTest {
assertBefore(stop, "\"world engines\"", "\"dimension manager\"");
assertBefore(stop, "\"server state\"", "if (failure != null)");
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(");
assertTrue(runStage.contains("catch (Throwable stageFailure)"));
@@ -207,21 +207,21 @@ public class ModdedLifecycleFailureContractTest {
String bootstrapSource = source("ModdedEngineBootstrap.java");
String unload = method(bootstrapSource, "public static void levelUnloaded(ServerLevel level)");
String failure = catchBlock(unload);
assertTrue(failure.contains("LOGGER.error("));
assertTrue(failure.contains("ModdedIrisLog.error("));
assertTrue(failure.contains("throw "));
String managerSource = source("ModdedDimensionManager.java");
String remove = method(managerSource, "public static boolean remove(MinecraftServer server, String dimensionId, boolean wipeStorage)");
assertTrue(remove.contains("ModdedWorldEngines.evictOrThrow(level);"));
assertFalse(remove.contains("ModdedWorldEngines.evict(level);"));
assertTrue(remove.contains("generatorUnbound = true;"));
assertTrue(remove.contains("rollbackRemoval(server, serverAccess, key, level, generator, generatorUnbound, e);"));
assertTrue(remove.contains("unloadEventStarted = true;"));
assertTrue(remove.contains("rollbackRemoval(server, serverAccess, key, level, generator, unloadEventStarted, e);"));
String rollback = method(managerSource, "private static void rollbackRemoval(");
assertTrue(rollback.contains("serverAccess.hasLevel(server, key)"));
assertTrue(rollback.contains("generator.bindLevel(level);"));
assertTrue(rollback.contains("failure.addSuppressed(rollbackFailure);"));
assertTrue(rollback.contains("LOGGER.error("));
assertTrue(rollback.contains("ModdedIrisLog.error("));
}
@Test
@@ -45,6 +45,14 @@ public class ModdedPlatformPathsTest {
public void invalidateLevelCache(MinecraftServer server) {
}
@Override
public void fireDynamicLevelLoad(MinecraftServer server, ServerLevel level) {
}
@Override
public void fireDynamicLevelUnload(MinecraftServer server, ServerLevel level) {
}
@Override
public boolean clientEnvironment() {
return false;
@@ -186,7 +186,7 @@ public class IrisModdedStructureCommandTest {
assertTrue(command.contains("ModdedLocateCommands.registeredStructureUnavailableMessage("));
assertTrue(command.contains("engine.getData().getStructureLoader().getPossibleKeys()"));
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\")"));
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.world.level.block.state.BlockState;
import net.neoforged.neoforge.common.NeoForge;
import net.neoforged.neoforge.event.level.LevelEvent;
import net.neoforged.neoforge.event.level.block.BreakBlockEvent;
import net.neoforged.fml.ModList;
import net.neoforged.fml.loading.FMLEnvironment;
@@ -77,6 +78,16 @@ public final class NeoForgeModdedLoader implements ModdedLoader {
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
public boolean clientEnvironment() {
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.jvm.tasks.Jar
import org.gradle.jvm.toolchain.JavaLanguageVersion
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
/*
* 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 forgeArtifactName = irisArtifactName('Forge', "${minecraftVersion}+${loaderDisplayVersion(forgeVersion)}")
String neoForgeArtifactName = irisArtifactName('NeoForge', "${minecraftVersion}+${loaderDisplayVersion(neoForgeVersion)}")
long maximumBukkitArtifactBytes = 7_000_000L
apply plugin: ApiGenerator
// 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 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 {
nmsBindings.keySet().each { key ->
add('included', project(path: ":adapters:bukkit:nms:${key}", configuration: 'runtimeElements'))
@@ -154,14 +167,66 @@ dependencies {
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 {
inputs.files(included)
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 {
delete(layout.buildDirectory.file("libs/Iris-${project.version}.jar"))
}
archiveFileName.set(bukkitArtifactName)
doLast {
JarCompactor.compact(archiveFile.get().asFile)
}
}
tasks.register('iris', Copy) {
@@ -537,10 +602,8 @@ List<String> requiredBukkitArtifactEntries = [
'art/arcane/iris/core/lifecycle/WorldLifecycleStaging.class',
'art/arcane/iris/util/simd/VectorSimdKernels.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/common/misc/getHardware.class',
'art/arcane/iris/util/caffeine/cache/Caffeine.class',
'art/arcane/volmlib/util/io/JarScanner.class',
'art/arcane/volmlib/util/director/runtime/DirectorRuntimeEngine.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
inputs.file(artifact)
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")
}
}
@@ -21,9 +21,7 @@ public final class BukkitArtifactVerifier {
// class under one of these is a NoClassDefFoundError waiting for the right code path.
private static final List<String> SHIPPED_PREFIXES = List.of(
"art/arcane/iris/",
"art/arcane/volmlib/",
"com/google/gson/",
"com/googlecode/concurrentlinkedhashmap/"
"art/arcane/volmlib/"
);
// 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.
@@ -38,9 +36,12 @@ public final class BukkitArtifactVerifier {
"art/arcane/iris/util/aether/",
"art/arcane/iris/util/guice/",
"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 LANGUAGE_DIRECTORY = "languages/";
@@ -48,10 +49,14 @@ public final class BukkitArtifactVerifier {
}
public static void verify(File artifact, List<String> requiredEntries, int minimumLocales,
int minimumCaffeineFactories, int minimumMatterSlices) {
int minimumMatterSlices, long maximumArtifactBytes) {
if (!artifact.isFile()) {
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)) {
for (String requiredEntry : requiredEntries) {
@@ -65,7 +70,6 @@ public final class BukkitArtifactVerifier {
Set<String> shippedClasses = new LinkedHashSet<>();
int locales = 0;
int caffeineFactories = 0;
int matterSlices = 0;
Enumeration<JarEntry> entries = jar.entries();
while (entries.hasMoreElements()) {
@@ -84,9 +88,6 @@ public final class BukkitArtifactVerifier {
String internalName = name.substring(0, name.length() - ".class".length());
shippedClasses.add(internalName);
if (isGeneratedCaffeineFactory(internalName)) {
caffeineFactories++;
}
if (internalName.startsWith(MATTER_SLICE_PACKAGE)) {
matterSlices++;
}
@@ -96,14 +97,6 @@ public final class BukkitArtifactVerifier {
throw new GradleException(artifact.getName() + " ships " + locales + " locale files; expected at least "
+ 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.
if (matterSlices < minimumMatterSlices) {
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 {
try (InputStream input = jar.getInputStream(entry)) {
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"
);
private static final int LOCALES = 2;
private static final int CAFFEINE_FACTORIES = 2;
private static final int MATTER_SLICES = 1;
@Rule
@@ -40,7 +39,7 @@ public class BukkitArtifactVerifierTest {
public void acceptsCompleteArtifact() throws Exception {
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
@@ -50,8 +49,7 @@ public class BukkitArtifactVerifierTest {
File artifact = createArtifact(entries);
GradleException failure = assertThrows(GradleException.class,
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, CAFFEINE_FACTORIES,
MATTER_SLICES));
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, MATTER_SLICES, Long.MAX_VALUE));
assertTrue(failure.getMessage().contains(PLUGIN_DESCRIPTOR));
}
@@ -62,8 +60,7 @@ public class BukkitArtifactVerifierTest {
File artifact = createArtifact(entries);
GradleException failure = assertThrows(GradleException.class,
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, CAFFEINE_FACTORIES,
MATTER_SLICES));
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, MATTER_SLICES, Long.MAX_VALUE));
assertTrue(failure.getMessage().contains(SLIMJAR_DEPENDENCIES));
}
@@ -74,8 +71,7 @@ public class BukkitArtifactVerifierTest {
File artifact = createArtifact(entries);
GradleException failure = assertThrows(GradleException.class,
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, CAFFEINE_FACTORIES,
MATTER_SLICES));
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, MATTER_SLICES, Long.MAX_VALUE));
assertTrue(failure.getMessage().contains(SLIMJAR_RESOLUTIONS));
}
@@ -86,8 +82,7 @@ public class BukkitArtifactVerifierTest {
File artifact = createArtifact(entries);
GradleException failure = assertThrows(GradleException.class,
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, CAFFEINE_FACTORIES,
MATTER_SLICES));
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, MATTER_SLICES, Long.MAX_VALUE));
assertTrue(failure.getMessage().contains("art/arcane/volmlib/util/noise/CNG"));
}
@@ -96,21 +91,31 @@ public class BukkitArtifactVerifierTest {
Map<String, byte[]> entries = validEntries();
entries.put("art/arcane/iris/Consumer.class",
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);
BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, CAFFEINE_FACTORIES, MATTER_SLICES);
BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, MATTER_SLICES, Long.MAX_VALUE);
}
@Test
public void rejectsStrippedCaffeineFactories() throws Exception {
Map<String, byte[]> entries = validEntries();
entries.remove("art/arcane/iris/util/caffeine/cache/SSMS.class");
File artifact = createArtifact(entries);
public void rejectsArtifactAboveConfiguredSize() throws Exception {
File artifact = createArtifact(validEntries());
GradleException failure = assertThrows(GradleException.class,
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, CAFFEINE_FACTORIES,
MATTER_SLICES));
assertTrue(failure.getMessage().contains("generated Caffeine cache classes"));
() -> BukkitArtifactVerifier.verify(
artifact,
REQUIRED_ENTRIES,
LOCALES,
MATTER_SLICES,
artifact.length() - 1L));
assertTrue(failure.getMessage().contains("must not exceed"));
}
@Test
@@ -120,8 +125,7 @@ public class BukkitArtifactVerifierTest {
File artifact = createArtifact(entries);
GradleException failure = assertThrows(GradleException.class,
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, CAFFEINE_FACTORIES,
MATTER_SLICES));
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, MATTER_SLICES, Long.MAX_VALUE));
assertTrue(failure.getMessage().contains("locale files"));
}
@@ -132,8 +136,7 @@ public class BukkitArtifactVerifierTest {
File artifact = createArtifact(entries);
GradleException failure = assertThrows(GradleException.class,
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, CAFFEINE_FACTORIES,
MATTER_SLICES));
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, MATTER_SLICES, Long.MAX_VALUE));
assertTrue(failure.getMessage().contains("Matter slice types"));
}
@@ -144,7 +147,7 @@ public class BukkitArtifactVerifierTest {
classAnnotatedWith("art/arcane/iris/Annotated", "com/google/errorprone/annotations/CanIgnoreReturnValue"));
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() {
@@ -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/matter/slices/BlockMatter.class",
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;
}
@@ -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) {
transitive = false
}
implementation(libs.gson)
implementation(libs.lru)
implementation(libs.caffeine)
implementation(libs.paralithic)
// Dynamically Loaded
slim(libs.gson)
slim(libs.lru)
slim(libs.caffeine)
slim(libs.paralithic)
slim(libs.paperlib)
slim(libs.adventure.api)
slim(libs.adventure.minimessage)
@@ -154,6 +153,8 @@ slimJar {
]
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('net.kyori', "${lib}.kyori")
relocate('org.bstats', "${lib}.metrics")
@@ -285,9 +286,149 @@ List<String> supersededVolmLibPackages = [
'art/arcane/volmlib/util/director/visual/**',
'art/arcane/volmlib/util/value/**',
'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
// in annotation attributes, which the JVM skips silently when the type is absent, so nothing loads
// 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")
exclude('modules/loader-agent.isolated-jar')
exclude(supersededVolmLibPackages)
exclude(unusedVolmLibClassEntries)
exclude(annotationOnlyArtifacts)
exclude(dependencyBuildMetadata)
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.volmlib.util.collection.KList;
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.plugin.VolmitSender;
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 String CODE_WORKSPACE_SUFFIX = ".code-workspace";
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 volatile boolean loadedDatapackRuntimeReady;
private static volatile String loadedDatapackCompilerInputFingerprint = "";
@@ -419,13 +420,18 @@ public class ServerConfigurator {
&& !reusableRuntimeFingerprint(
loadedDatapackCompilerInputFingerprint,
current);
boolean loadedRegistryRestartRequired = fullInstall
&& loadedCompilerInputsChanged
&& currentRegistryRequiresRestart();
if (!current.isEmpty() && current.equals(cached)) {
IrisLogging.debug("Data packs unchanged, skipping install.");
DatapackInstallResult result = fullInstall && loadedCompilerInputsChanged
DatapackInstallResult result = loadedRegistryRestartRequired
? DatapackInstallResult.restartRequiredResult()
: resultForUnchangedFingerprint(fullInstall, reapply);
if (result.restartRequired()) {
requireDatapackRestart();
} else if (result.succeeded()) {
loadedDatapackCompilerInputFingerprint = current;
}
reportTiming(timingConsumer, "datapack_install_if_changed_total", totalStart);
return result;
@@ -435,7 +441,7 @@ public class ServerConfigurator {
resolveDataFixer(),
fullInstall,
reapply);
if (fullInstall && loadedCompilerInputsChanged && result.succeeded()) {
if (loadedRegistryRestartRequired && result.succeeded()) {
result = DatapackInstallResult.restartRequiredResult();
}
if (result.restartRequired()) {
@@ -444,6 +450,7 @@ public class ServerConfigurator {
reportTiming(timingConsumer, "datapack_compile_publish", compileStart);
if (result.succeeded() && !result.restartRequired()) {
writeCompilerInputFingerprintCache(cacheFile.toPath(), current);
loadedDatapackCompilerInputFingerprint = current;
}
reportTiming(timingConsumer, "datapack_install_if_changed_total", totalStart);
return result;
@@ -456,6 +463,22 @@ public class ServerConfigurator {
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 {
File levelRoot = IrisWorldStorage.levelRoot();
String levelId = levelRoot.getName();
@@ -545,6 +568,19 @@ public class ServerConfigurator {
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) {
return loadedFingerprint != null
&& !loadedFingerprint.isBlank()
@@ -961,31 +997,29 @@ public class ServerConfigurator {
private static boolean verifyDataPacksPost() {
try (Stream<IrisData> stream = allPacks()) {
boolean bad = 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;
}
return verifyDataPacksPost(stream);
}
}
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()) {
// Three sentences, no rules: a separator carries the record's severity too, so a box drawn
// 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.");
IrisLogging.warn(POST_COMPILE_RESTART_WARNING);
for (Player i : Bukkit.getOnlinePlayers()) {
if (i.isOp() || i.hasPermission("iris.all")) {
@@ -1054,6 +1088,10 @@ public class ServerConfigurator {
}
public static boolean verifyDataPackInstalled(IrisDimension dimension) {
return verifyDataPackInstalled(dimension, true);
}
private static boolean verifyDataPackInstalled(IrisDimension dimension, boolean reportRuntimeFailure) {
KSet<String> keys = new KSet<>();
boolean warn = false;
@@ -1082,17 +1120,21 @@ public class ServerConfigurator {
Object o = INMS.get().getCustomBiomeBaseFor(i);
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;
}
}
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;
}
if (warn) {
if (warn && reportRuntimeFailure) {
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!");
}
@@ -31,6 +31,7 @@ import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.Locale;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -63,7 +64,7 @@ public final class GuiHost {
default void unregisterHotloadHook(Runnable onHotload) {
}
default GuiOverlay overlayFor(Engine engine) {
default GuiOverlay overlayFor(Engine engine, UUID openerId) {
return null;
}
}
@@ -73,6 +73,7 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
public final class VisionGUI extends JPanel implements MouseWheelListener, KeyListener, MouseMotionListener, MouseInputListener {
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 final JFrame hostFrame;
private final UUID openerId;
private final VisionRenderController controller;
private final Runnable hotloadHook;
private final Timer resizeTimer;
@@ -147,11 +149,12 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi
private boolean controlsUpdating;
private boolean closed;
private VisionGUI(JFrame hostFrame, Engine engine) {
private VisionGUI(JFrame hostFrame, Engine engine, UUID openerId) {
this.hostFrame = Objects.requireNonNull(hostFrame, "hostFrame");
this.engine = Objects.requireNonNull(engine, "engine");
this.openerId = openerId;
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.hotloadHook = () -> EventQueue.invokeLater(this::refreshContent);
this.notifications = new LinkedHashMap<>();
@@ -201,14 +204,14 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi
notificationTimer.start();
}
public static void launch(Engine engine) {
EventQueue.invokeLater(() -> createAndShowGUI(engine));
public static void launch(Engine engine, UUID openerId) {
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));
GuiHost.prepareFrame(frame);
VisionGUI vision = new VisionGUI(frame, engine);
VisionGUI vision = new VisionGUI(frame, engine, openerId);
frame.getContentPane().setBackground(BACKGROUND);
frame.setLayout(new BorderLayout());
frame.add(buildToolbar(vision), BorderLayout.NORTH);
@@ -739,7 +742,7 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi
}
engine = reacquired;
renderer = new IrisRenderer(reacquired);
overlay = GuiHost.get().overlayFor(reacquired);
overlay = GuiHost.get().overlayFor(reacquired, openerId);
contentRevision++;
captureHeightRange();
return true;
@@ -758,7 +761,7 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi
return;
}
renderer = new IrisRenderer(engine);
overlay = GuiHost.get().overlayFor(engine);
overlay = GuiHost.get().overlayFor(engine, openerId);
contentRevision++;
captureHeightRange();
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 LEGACY_COLOR = Pattern.compile("(?i)\\u00a7[0-9A-FK-ORX]");
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(
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);
if (parsed.isJsonArray()) {
return parseCompactBukkitOverlay(source, locale, parsed.getAsJsonArray());
}
if (!parsed.isJsonObject()) {
throw new IllegalArgumentException("Locale source is not a JSON object: " + source);
}
@@ -398,6 +405,49 @@ public final class IrisLanguage {
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) {
for (Map.Entry<String, JsonElement> entry : object.entrySet()) {
String key = prefix.isEmpty() ? entry.getKey() : prefix + "." + entry.getKey();
@@ -13,7 +13,7 @@ import java.util.List;
import java.util.Set;
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> ROUTING_POLICIES = Set.of("ALLOW", "AVOID", "BLOCK");
private static final Set<String> CAVE_MODES = Set.of(
@@ -91,7 +91,7 @@ final class PackRiverValidator {
validateTerrain(packFolder, path + ".terrain", terrain, errors, warnings);
}
if (water != null) {
validateWater(path + ".water", water, errors);
validateWater(path + ".water", water, context.dimension(), errors);
}
if (biomes != null) {
validateBiomePools(
@@ -107,7 +107,15 @@ final class PackRiverValidator {
boolean sinkholeTerminal = terrain != null
&& "SINKHOLE_GROTTO".equals(stringValue(terrain, "terminalMode", "DRY_CHANNEL"));
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) {
@@ -168,6 +176,7 @@ final class PackRiverValidator {
validateStyledRange(packFolder, terrain, "bankWidth", path, 0D, 2048D, errors, warnings);
validateStyledRange(packFolder, terrain, "depth", path, 1D, 512D, 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, "maxBankWidth", 0D, 2048D, 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.validateOptionalIntegerRange(path, water, "poolLength", 8, 4096, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, water, "maximumPoolRise", 0, 64, 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 dropHeight = integerValue(water, "dropHeight", 1);
if ("TERRACED".equals(mode) && dropHeight > maximumPoolRise) {
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(
@@ -345,9 +386,11 @@ final class PackRiverValidator {
PackJsonFieldChecks.validateOptionalDoubleRange(
wormPath, worm, "depthMultiplier", 0.125D, 8D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(
wormPath, worm, "bodyWavelength", 32D, 16384D, errors);
wormPath, worm, "bodyWavelength", 8D, 16384D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(
wormPath, worm, "bodyDetailWavelength", 32D, 16384D, errors);
wormPath, worm, "bodyDetailWavelength", 8D, 16384D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(
wormPath, worm, "bodyDetailInfluence", 0D, 1D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(
wormPath, worm, "widthVariation", 0D, 0.875D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(
@@ -452,6 +495,7 @@ final class PackRiverValidator {
}
private static void validateCaves(File packFolder, String path, JSONObject caves,
JSONObject dimension,
boolean forceGeneratedGrotto,
List<String> errors, List<String> warnings) {
PackJsonFieldChecks.validateOptionalEnum(path, caves, "mode", CAVE_MODES, errors);
@@ -473,6 +517,19 @@ final class PackRiverValidator {
validateNoiseChance(packFolder, caves, "entry", path, errors);
validateStyle(packFolder, caves, "grottoShapeStyle", 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");
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) {
int throatRadius = integerValue(caves, "throatRadius", 2);
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 int ADAPTIVE_SLOW_REQUEST_STEP = 3;
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 final World world;
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.
boolean interrupted = Thread.interrupted();
try {
boolean drained = false;
try {
drained = semaphore.tryAcquire(threads, CLOSE_DRAIN_TIMEOUT_SECONDS, TimeUnit.SECONDS);
} catch (InterruptedException e) {
interrupted = true;
}
if (!drained) {
IrisLogging.warn("Async pregen close did not drain in " + CLOSE_DRAIN_TIMEOUT_SECONDS
+ "s, continuing degraded. " + metricsSnapshot());
}
interrupted |= awaitDrain(
semaphore,
threads,
CLOSE_DRAIN_WARNING_SECONDS,
TimeUnit.SECONDS,
() -> IrisLogging.warn("Async pregen is still draining outstanding chunks. " + metricsSnapshot())
);
flushAllRemainingChunks();
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() {
return closing.get() || Thread.currentThread().isInterrupted();
}
@@ -84,19 +84,35 @@ public class IrisProject {
long seed,
StudioOpenCoordinator.StudioOpenKind openKind,
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 {
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(
VolmitSender sender,
long seed,
StudioOpenCoordinator.StudioOpenKind openKind,
Consumer<World> onDone
Consumer<World> onDone,
long requestedAtNanos
) {
AtomicReference<String> stage = new AtomicReference<>("Queued");
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())));
},
onDone
onDone,
requestedAtNanos
)
);
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.tools.IrisCreator;
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.PlatformChunkGenerator;
import art.arcane.iris.util.common.plugin.VolmitSender;
@@ -98,16 +99,11 @@ public final class StudioOpenCoordinator {
public CompletableFuture<Boolean> teleportPlayerToProject(
IrisProject project,
Player player,
AtomicBoolean admission,
long deadlineNanos
Player player
) {
if (project == null || player == null) {
return CompletableFuture.completedFuture(false);
}
AtomicBoolean activeAdmission = Objects.requireNonNull(
admission,
"Studio teleport admission");
PlatformChunkGenerator provider = project.getActiveProvider();
if (provider == null) {
return CompletableFuture.failedFuture(new IllegalStateException(
@@ -123,10 +119,6 @@ public final class StudioOpenCoordinator {
return CompletableFuture.failedFuture(new IllegalStateException(
"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
? WorldRuntimeControlService.get().teleportInMode(player, entry, GameMode.SPECTATOR)
: WorldRuntimeControlService.get().teleport(player, entry);
@@ -134,25 +126,12 @@ public final class StudioOpenCoordinator {
return CompletableFuture.failedFuture(new IllegalStateException(
"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;
}
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) {
World world = null;
PlatformChunkGenerator provider = null;
CompletableFuture<Boolean> nativeTeleportFuture = null;
try {
long openStart = System.nanoTime();
long t = openStart;
@@ -181,6 +160,10 @@ public final class StudioOpenCoordinator {
if (provider == null) {
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);
final World rulesWorld = world;
@@ -204,9 +187,15 @@ public final class StudioOpenCoordinator {
t = logStudioPhase(request, "resolve_entry_anchor", t, openStart);
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);
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;
if (request.openKind().teleportThroughStandardEntry()
@@ -219,7 +208,7 @@ public final class StudioOpenCoordinator {
}
Boolean teleported;
try {
nativeTeleportFuture = WorldRuntimeControlService.get().teleportInMode(
CompletableFuture<Boolean> nativeTeleportFuture = WorldRuntimeControlService.get().teleportInMode(
player,
entryLocation,
GameMode.SPECTATOR);
@@ -227,15 +216,20 @@ public final class StudioOpenCoordinator {
throw new IllegalStateException(
"Studio native teleport returned no completion future.");
}
teleported = nativeTeleportFuture.get(60L, TimeUnit.SECONDS);
} catch (TimeoutException e) {
nativeTeleportFuture.completeExceptionally(e);
throw new IllegalStateException("Studio teleport timed out — destination region may still be generating.");
teleported = nativeTeleportFuture.get();
} catch (ExecutionException e) {
Throwable failure = unwrapFailure(e);
throw new IllegalStateException("Studio teleport failed.", failure);
}
if (!Boolean.TRUE.equals(teleported)) {
throw new IllegalStateException("Studio teleport did not complete successfully.");
}
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);
@@ -861,10 +855,14 @@ public final class StudioOpenCoordinator {
StudioOpenKind openKind,
boolean retainOnFailure,
Consumer<StudioOpenProgress> progressConsumer,
Consumer<World> onDone
Consumer<World> onDone,
long requestedAtNanos
) {
public StudioOpenRequest {
openKind = Objects.requireNonNull(openKind, "Studio open kind");
if (requestedAtNanos <= 0L) {
throw new IllegalArgumentException("Studio request time must be positive.");
}
}
public static StudioOpenRequest studioProject(
@@ -874,6 +872,25 @@ public final class StudioOpenCoordinator {
StudioOpenKind openKind,
Consumer<StudioOpenProgress> progressConsumer,
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;
return new StudioOpenRequest(
@@ -886,7 +903,8 @@ public final class StudioOpenCoordinator {
openKind,
false,
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.engine.platform.BukkitChunkGenerator;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.util.common.scheduling.J;
import io.papermc.lib.PaperLib;
import org.bukkit.Bukkit;
@@ -41,10 +42,13 @@ import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
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_VERTICAL_SEARCH = 64;
private static final int SAFE_ENTRY_UPWARD_ALLOWANCE = 32;
private static final double BLOCK_CENTER = 0.5D;
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(
Material.CACTUS,
Material.CAMPFIRE,
@@ -302,7 +306,7 @@ public final class WorldRuntimeControlService {
CompletableFuture<Location> future = new CompletableFuture<>();
boolean scheduled = J.runRegion(world, chunkX, chunkZ, () -> {
try {
future.complete(findTopSafeLocation(world, source));
future.complete(findTopSafeLocationWithTicket(world, source, chunkX, chunkZ));
} catch (Throwable t) {
future.completeExceptionally(t);
}
@@ -314,6 +318,22 @@ public final class WorldRuntimeControlService {
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) {
return scheduleTeleport(player, location, null);
}
@@ -350,6 +370,7 @@ public final class WorldRuntimeControlService {
CompletableFuture<Boolean> future = new CompletableFuture<>();
GameModeRestore modeRestore = new GameModeRestore(player);
PlayerViewDistanceRestore viewDistanceRestore = new PlayerViewDistanceRestore(player);
AtomicReference<CompletableFuture<Boolean>> activeTeleport = new AtomicReference<>();
future.whenComplete((success, failure) -> {
if (Boolean.TRUE.equals(success)) {
@@ -359,6 +380,7 @@ public final class WorldRuntimeControlService {
if (teleport != null && !teleport.isDone()) {
teleport.cancel(false);
}
viewDistanceRestore.restore();
modeRestore.restore();
});
boolean scheduled = J.runEntity(player, () -> {
@@ -368,7 +390,9 @@ public final class WorldRuntimeControlService {
}
if (gameMode != null) {
modeRestore.apply(gameMode);
viewDistanceRestore.apply();
if (future.isDone()) {
viewDistanceRestore.restore();
modeRestore.restore();
return;
}
@@ -393,6 +417,7 @@ public final class WorldRuntimeControlService {
if (Boolean.TRUE.equals(success)) {
if (future.complete(true)) {
viewDistanceRestore.restore();
J.runEntity(player, () -> IrisServices.get(BoardSVC.class).updatePlayer(player));
}
return;
@@ -479,6 +504,7 @@ public final class WorldRuntimeControlService {
z,
minimumFloorY,
maximumFloorY,
source.getBlockY() - 1,
yaw,
pitch
);
@@ -498,13 +524,14 @@ public final class WorldRuntimeControlService {
int z,
int minimumFloorY,
int maximumFloorY,
int preferredFloorY,
float yaw,
float pitch
) {
int highestY = world.getHighestBlockYAt(x, z, HeightMap.MOTION_BLOCKING_NO_LEAVES);
int startingFloorY = Math.max(minimumFloorY, Math.min(maximumFloorY, highestY));
int lowestFloorY = Math.max(minimumFloorY, startingFloorY - MAX_SAFE_ENTRY_VERTICAL_SEARCH + 1);
for (int floorY = startingFloorY; floorY >= lowestFloorY; floorY--) {
int preferredMaximumY = Math.min(maximumFloorY, preferredFloorY + SAFE_ENTRY_UPWARD_ALLOWANCE);
int startingFloorY = Math.max(minimumFloorY, Math.min(preferredMaximumY, highestY));
for (int floorY = startingFloorY; floorY >= minimumFloorY; floorY--) {
Block floor = world.getBlockAt(x, floorY, z);
if (!isSafeFloor(floor)) {
continue;
@@ -777,6 +804,14 @@ public final class WorldRuntimeControlService {
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
interface TeleportExecutor {
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.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.regex.Pattern;
@@ -95,7 +94,6 @@ import art.arcane.volmlib.util.localization.MessageArgument;
public class StudioSVC implements IrisService {
public static final String WORKSPACE_NAME = "packs";
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 AtomicCache<Integer> counter = new AtomicCache<>();
private final StudioTransitionQueue studioTransitions = new StudioTransitionQueue();
@@ -478,24 +476,14 @@ public class StudioSVC implements IrisService {
public CompletableFuture<Boolean> teleportToActiveProject(Player player) {
Player target = Objects.requireNonNull(player, "Studio teleport player");
AtomicBoolean admission = new AtomicBoolean(true);
long deadlineNanos = System.nanoTime()
+ TimeUnit.SECONDS.toNanos(STUDIO_PLAYER_TELEPORT_TIMEOUT_SECONDS);
CompletableFuture<Boolean> transition = studioTransitions.submit(() -> {
return studioTransitions.submit(() -> {
IrisProject project = activeProject;
if (project == null || !project.isOpen()) {
return CompletableFuture.failedFuture(new IllegalStateException(
"No active Studio project is available for teleport."));
}
return StudioOpenCoordinator.get().teleportPlayerToProject(
project,
target,
admission,
deadlineNanos);
return StudioOpenCoordinator.get().teleportPlayerToProject(project, target);
});
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) {
@@ -565,13 +553,20 @@ public class StudioSVC implements IrisService {
StudioOpenCoordinator.StudioOpenKind openKind,
Consumer<World> onDone
) throws IrisException {
long requestedAtNanos = System.nanoTime();
if (reportPackAdmissionFailure(sender, dimm) != null) {
return;
}
StudioOpenCoordinator.StudioOpenKind requiredOpenKind = Objects.requireNonNull(
openKind,
"Studio open kind");
studioTransitions.submit(() -> replaceActiveProject(sender, seed, dimm, requiredOpenKind, onDone))
studioTransitions.submit(() -> replaceActiveProject(
sender,
seed,
dimm,
requiredOpenKind,
onDone,
requestedAtNanos))
.whenComplete((ignored, throwable) -> {
if (throwable == null) {
return;
@@ -591,6 +586,7 @@ public class StudioSVC implements IrisService {
Runnable beforeOpen,
Consumer<World> onDone
) {
long requestedAtNanos = System.nanoTime();
BrokenPackException failure = reportPackAdmissionFailure(sender, dimension);
if (failure != null) {
return CompletableFuture.failedFuture(failure);
@@ -601,7 +597,8 @@ public class StudioSVC implements IrisService {
dimension,
Objects.requireNonNull(openKind, "Studio open kind"),
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(
@@ -610,7 +607,8 @@ public class StudioSVC implements IrisService {
String dimension,
StudioOpenCoordinator.StudioOpenKind openKind,
Runnable beforeOpen,
Consumer<World> onDone
Consumer<World> onDone,
long requestedAtNanos
) {
return closeActiveProject().thenCompose(closeResult -> {
if (closeResult == null) {
@@ -621,7 +619,13 @@ public class StudioSVC implements IrisService {
return CompletableFuture.failedFuture(closeResult.failureCause());
}
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,
String dimension,
StudioOpenCoordinator.StudioOpenKind openKind,
Consumer<World> onDone
Consumer<World> onDone,
long requestedAtNanos
) {
IrisProject project = new IrisProject(new File(getWorkspaceFolder(), dimension));
activeProject = project;
CompletableFuture<StudioOpenCoordinator.StudioOpenResult> opening;
try {
opening = project.open(sender, seed, openKind, onDone);
opening = project.open(sender, seed, openKind, onDone, requestedAtNanos);
} catch (IrisException exception) {
if (activeProject == project) {
activeProject = null;
@@ -663,7 +668,8 @@ public class StudioSVC implements IrisService {
long seed,
String dimension,
StudioOpenCoordinator.StudioOpenKind openKind,
Consumer<World> onDone
Consumer<World> onDone,
long requestedAtNanos
) {
return closeActiveProjectForReplacement(sender).handle((closeResult, closeThrowable) -> {
if (closeThrowable != null) {
@@ -691,7 +697,13 @@ public class StudioSVC implements IrisService {
}
return true;
}).thenCompose(closed -> closed
? beginStudioOpen(sender, seed, dimension, openKind, onDone)
? beginStudioOpen(
sender,
seed,
dimension,
openKind,
onDone,
requestedAtNanos)
: CompletableFuture.completedFuture(null));
}
@@ -700,7 +712,8 @@ public class StudioSVC implements IrisService {
long seed,
String dimension,
StudioOpenCoordinator.StudioOpenKind openKind,
Consumer<World> onDone
Consumer<World> onDone,
long requestedAtNanos
) {
IrisProject project = new IrisProject(new File(getWorkspaceFolder(), dimension));
activeProject = project;
@@ -710,7 +723,8 @@ public class StudioSVC implements IrisService {
sender,
seed,
openKind,
onDone);
onDone,
requestedAtNanos);
} catch (IrisException e) {
if (activeProject == project) {
activeProject = null;
@@ -306,6 +306,9 @@ public class IrisCreator {
world = J.sfut(() -> INMS.get().createWorldAsync(wc, request))
.thenCompose(Function.identity())
.get(WORLD_CREATE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
if (!studio && !benchmark) {
awaitInitialSpawnPreparation(access, name);
}
} catch (Throwable e) {
done.set(true);
cancelRepeatingTask(createProgressTask);
@@ -603,6 +606,21 @@ public class IrisCreator {
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(
AtomicDouble progress,
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.IrisGenerator;
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.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.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.river.runtime.IrisRiverRuntime;
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.RiverSample;
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.IrisLogging;
import art.arcane.iris.spi.PlatformBiome;
@@ -66,6 +71,7 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;
@@ -132,6 +138,8 @@ public class IrisComplex implements DataProvider {
private ProceduralStream<IrisDecorator> shoreSurfaceDecoration;
private ProceduralStream<PlatformBlockState> rockStream;
private ProceduralStream<PlatformBlockState> fluidStream;
private ProceduralStream<PlatformBlockState> riverFluidStream;
private ProceduralStream<PlatformBlockState> riverDeepPoolFluidStream;
private IrisBiome focusBiome;
private IrisRegion focusRegion;
private Map<IrisInterpolator, IdentityHashMap<IrisBiome, GeneratorBounds>> generatorBounds;
@@ -213,6 +221,29 @@ public class IrisComplex implements DataProvider {
.select(engine.getDimension().getRockPalette().getBlockData(data));
fluidStream = engine.getDimension().getFluidPalette().getLayerGenerator(rng.nextParallelRNG(78), data).stream()
.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()
.zoom(engine.getDimension().getRegionZoom());
regionIdentityStream = regionStyleStream.fit(Integer.MIN_VALUE, Integer.MAX_VALUE);
@@ -303,16 +334,16 @@ public class IrisComplex implements DataProvider {
.cache2D("naturalTrueBiomeStream", engine, cacheSize);
if (engine.getDimension().getRivers() != null && engine.getDimension().getRivers().isEnabled()) {
ProceduralStream<Boolean> naturalOceanStream = createNaturalOceanStream(
naturalHeightStream,
bridgeStream,
focusBiome,
fluidHeight,
engine.getDimension().getRivers().getWater().getMode()
focusBiome
).cache2D("naturalOceanStream", engine, cacheSize);
int riverFluidHeight = engine.getDimension().getRivers().getWater().getFluidHeight()
- engine.getDimension().getMinHeight();
riverRuntime = new IrisRiverRuntime(new IrisRiverRuntimeContext(
engine.getSeedManager().getBodies(),
engine.getDimension().getRivers(),
data,
riverFluidHeight,
(int) Math.round(fluidHeight),
IrisEngineMantle.isRiverHydrologyEnabled(engine.getDimension()),
IrisEngineMantle.isRiverCaveHydrologyEnabled(engine.getDimension()),
@@ -458,23 +489,49 @@ public class IrisComplex implements DataProvider {
}
static ProceduralStream<Boolean> createNaturalOceanStream(
ProceduralStream<Double> naturalHeightStream,
ProceduralStream<InferredType> bridgeStream,
IrisBiome focusBiome,
double fluidHeight,
IrisRiverWaterMode waterMode
IrisBiome focusBiome
) {
if (focusBiome != null) {
boolean ocean = focusBiome.getInferredType() == InferredType.SEA;
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(
(x, z) -> naturalHeightStream.getDouble(x, z) < fluidHeight - 1D,
Interpolated.BOOLEAN
);
for (PlatformBlockState block : blocks) {
if (block == null || !block.isFluid()) {
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) {
@@ -71,6 +71,7 @@ import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
@@ -93,6 +94,7 @@ public class IrisEngine implements Engine {
private final ChronoLatch perSecondLatch;
private final ChronoLatch perSecondBudLatch;
private final EngineMetrics metrics;
private final CompletableFuture<Void> generationCacheWarm;
private final boolean studio;
private final AtomicRollingSequence wallClock;
@Getter(AccessLevel.NONE)
@@ -186,6 +188,7 @@ public class IrisEngine implements Engine {
bud = new AtomicInteger(0);
buds = new AtomicInteger(0);
metrics = new EngineMetrics(32);
generationCacheWarm = new CompletableFuture<>();
cleanLatch = new ChronoLatch(10000);
generatedLast = new AtomicInteger(0);
perSecond = new AtomicDouble(0);
@@ -232,16 +235,17 @@ public class IrisEngine implements Engine {
runtimeBuilder.publishRuntime(initialRuntime, null);
IrisLogging.debug("[IrisEngine timing] setupEngine total=" + (M.ms() - _t0) + "ms");
logStudioInitializationPhase("build_runtime", phaseStartedAt, false);
_t0 = M.ms();
phaseStartedAt = System.nanoTime();
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);
} catch (Throwable e) {
shutdownSequence.cleanupFailedConstruction(e);
@@ -250,6 +254,21 @@ public class IrisEngine implements Engine {
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) {
if (!studio) {
return;
@@ -262,6 +281,32 @@ public class IrisEngine implements Engine {
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() {
if (getEngineData().getSeed() != null && getEngineData().getSeed() != target.getWorld().getRawWorldSeed()) {
target.getWorld().setRawWorldSeed(getEngineData().getSeed());
@@ -323,6 +368,7 @@ public class IrisEngine implements Engine {
@Override
public void generateMatter(int x, int z, boolean multicore, ChunkContext context) {
awaitGenerationCacheWarm();
try (GenerationSessionLease lease = acquireGenerationLease("matter_generate");
IrisContext.Scope ignored = IrisContext.open(this, lease.sessionId(), context)) {
IrisComplex activeComplex = getComplex();
@@ -574,6 +620,7 @@ public class IrisEngine implements Engine {
@BlockCoordinates
@Override
public void generate(int x, int z, Hunk<PlatformBlockState> vblocks, Hunk<PlatformBiome> vbiomes, boolean multicore) throws WrongEngineBroException {
awaitGenerationCacheWarm();
try (GenerationSessionLease lease = acquireGenerationLease("chunk_generate");
IrisContext.Scope generationScope = IrisContext.open(this, lease.sessionId(), null)) {
getEngineData().getStatistics().generatedChunk();
@@ -48,7 +48,6 @@ import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
@EqualsAndHashCode(callSuper = true)
@Data
@@ -78,7 +77,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
final WorldBlockDropRouter blockDropRouter = new WorldBlockDropRouter(this);
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
final WorldTeleportWarmup teleportWarmup = new WorldTeleportWarmup(this);
final WorldTeleportWarmup teleportWarmup = new WorldTeleportWarmup();
private boolean looperStopped;
private volatile boolean cleanupServiceStopped;
volatile int entityCount = 0;
@@ -193,10 +192,6 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
};
}
AtomicBoolean ignoreTeleport() {
return ignoreTP;
}
@Override
public void onTick() {
@@ -18,87 +18,53 @@
package art.arcane.iris.engine;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.RuntimeUiMessages;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
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.entity.Player;
import org.bukkit.event.player.PlayerTeleportEvent;
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 {
private final IrisWorldManager manager;
WorldTeleportWarmup(IrisWorldManager manager) {
this.manager = manager;
}
void teleportAsync(PlayerTeleportEvent e) {
Location destination = e.getTo();
if (destination == null) {
return;
}
Player player = e.getPlayer();
PlayerTeleportEvent.TeleportCause cause = e.getCause();
e.setCancelled(true);
warmupAreaAsync(e.getPlayer(), e.getTo(), () -> J.runEntity(e.getPlayer(), manager.managedTask(
"bukkit_world_manager_teleport",
() -> {
manager.ignoreTeleport().set(true);
e.getPlayer().teleport(e.getTo(), e.getCause());
manager.ignoreTeleport().set(false);
})));
CompletableFuture<Boolean> teleport;
try {
teleport = BukkitPlatform.teleportAsync(
player,
destination.clone(),
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) {
J.a(manager.managedTask("bukkit_world_manager_teleport_warmup", () -> {
int viewDistance = 2;
KList<Future<Chunk>> futures = new KList<>();
for (int i = -viewDistance; i <= viewDistance; i++) {
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);
}));
private void reportFailure(Player player, Location destination, Throwable failure) {
IrisLogging.error("Async teleport into Iris world failed for " + player.getName()
+ " at " + destination.getBlockX() + ", " + destination.getBlockY() + ", "
+ destination.getBlockZ() + ".");
IrisLogging.reportError(failure);
}
}
@@ -102,7 +102,7 @@ public class IrisDecorantActuator extends EngineAssignedActuator<PlatformBlockSt
}
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,
realX, Math.round(i + 1), Math.round(x + i - 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();
ChunkedDataCache<IrisBiome> biomeCache = context.getBiome();
ChunkedDataCache<IrisRegion> regionCache = context.getRegion();
ChunkedDataCache<PlatformBlockState> fluidCache = context.getFluid();
ChunkedDataCache<PlatformBlockState> rockCache = context.getRock();
int realX = xf + x;
UpperDimensionContext upperContext = getEngine().getUpperContext();
@@ -118,7 +117,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
}
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 mappedSurfaceBlock = complex.getImageMapRuntime().sampleSurfaceBlock(realX, realZ);
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.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.Location;
import org.bukkit.Sound;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import org.bukkit.event.world.ChunkLoadEvent;
import org.bukkit.event.world.ChunkUnloadEvent;
import org.bukkit.event.world.WorldSaveEvent;
@@ -45,7 +49,6 @@ public abstract class EngineAssignedWorldManager extends EngineAssignedComponent
private boolean listenerRegistered;
private boolean closeRequested;
private int taskId;
protected AtomicBoolean ignoreTP = new AtomicBoolean(false);
public EngineAssignedWorldManager() {
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
public void on(ChunkLoadEvent e) {
runManagerTask("bukkit_world_manager_chunk_load", () -> {
@@ -38,6 +38,19 @@ public interface MantleComponent extends Comparable<MantleComponent> {
return 0;
}
default int getInputRadius(
int targetChunkX,
int targetChunkZ,
int invocationChunkRadius,
ChunkContext context
) {
return getInputRadius();
}
default boolean isInputGenerationLazy() {
return false;
}
default IrisData getData() {
return getEngineMantle().getData();
}
@@ -66,9 +66,21 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
private final AtomicReferenceArray<MantleChunk<Matter>> window;
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.mantle = mantle;
this.radius = radius * 2;
this.radius = accessRadius;
this.x = x;
this.z = z;
// 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()
&& 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()) {
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.
try {
mantle.getChunks(
x - radius,
x + radius,
z - radius,
z + radius,
x - prefetchRadius,
x + prefetchRadius,
z - prefetchRadius,
z + prefetchRadius,
parallelism,
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) {
Set<IrisPosition> returnset = new HashSet<>();
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.util.common.parallel.MultiBurst;
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.mantle.flag.MantleFlag;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
@@ -43,18 +44,31 @@ public interface MatterGenerator {
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();
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
// close() releases the cached chunks, even when a pass throws, or detached pool threads
// write into released chunks.
List<MatterComponentTask> outstandingTasks = null;
try {
for (MantlePass pass : getComponents()) {
int passRadius = pass.passChunkRadius();
for (MatterPassPlan passPlan : passPlans) {
MantlePass pass = passPlan.pass();
int passRadius = passPlan.passChunkRadius();
List<MantleComponent> passComponents = pass.components();
MantleComponent[] enabledComponents = new MantleComponent[passComponents.size()];
int[] componentPassRadii = new int[passComponents.size()];
@@ -63,7 +77,7 @@ public interface MatterGenerator {
if (component.isEnabled()) {
// 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.
int componentReach = component.getRadius() + pass.downstreamBlockRadius();
int componentReach = component.getRadius() + passPlan.downstreamBlockRadius();
componentPassRadii[enabledComponentCount] = componentReach > 0 ? Math.ceilDiv(componentReach, 16) : 0;
enabledComponents[enabledComponentCount++] = component;
}
@@ -112,7 +126,8 @@ public interface MatterGenerator {
MantleChunk<Matter> chunk = writer.acquireChunk(passX, passZ);
if (chunk == null) {
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)) {
@@ -174,8 +189,9 @@ public interface MatterGenerator {
}
}
for (int i = -getRealRadius(); i <= getRealRadius(); i++) {
for (int j = -getRealRadius(); j <= getRealRadius(); j++) {
int realRadius = passPlans[passPlans.length - 1].passChunkRadius();
for (int i = -realRadius; i <= realRadius; i++) {
for (int j = -realRadius; j <= realRadius; j++) {
int realX = x + i;
int realZ = z + j;
long realKey = chunkKey(realX, realZ);
@@ -195,6 +211,45 @@ public interface MatterGenerator {
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(
MantleChunk<Matter> chunk,
MantleComponent component,
@@ -217,11 +272,21 @@ public interface MatterGenerator {
try {
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);
}
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);
} catch (Throwable throwable) {
IN_FLIGHT_COMPONENTS.remove(key, future);
@@ -329,10 +394,11 @@ public interface MatterGenerator {
MantleWriter writer,
int chunkX,
int chunkZ,
ChunkContext context
ChunkContext context,
IrisContext callerContext
) {
try {
runComponentInline(chunk, component, writer, chunkX, chunkZ, context);
runComponentWithContext(chunk, component, writer, chunkX, chunkZ, context, callerContext);
future.complete(null);
} catch (Throwable 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(
MantleChunk<Matter> chunk,
MantleComponent component,
@@ -360,6 +448,12 @@ public interface MatterGenerator {
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 {
private final Mantle<Matter> mantle;
private final int chunkX;
@@ -27,6 +27,7 @@ final class CaveCarveScratch {
final int[] fluidMaxY = new int[256];
final int[] surfaceBreakFloorY = new int[256];
final boolean[] surfaceBreakColumn = new boolean[256];
final boolean[] surfaceCeilingColumn = new boolean[256];
final double[] columnThreshold = new double[256];
final double[] passThreshold = new double[256];
final double[] fullWeights = new double[256];
@@ -62,6 +63,7 @@ final class CaveCarveScratch {
int activeModulesY = Integer.MIN_VALUE;
int activeModuleCount;
double[] verticalEdgeFade = new double[0];
double[] surfaceClosureThreshold = new double[0];
MatterCavern[] matterByY = new MatterCavern[0];
Matter[] sectionMatter = new Matter[0];
MatterSlice<?>[] sectionSlices = new MatterSlice<?>[0];
@@ -20,13 +20,15 @@ final class ConfiguredRiverGrottoShape implements RiverCaveGrottoShape {
private final CNG warpY;
private final CNG warpZ;
private final double warpStrength;
private final double boundaryVariation;
ConfiguredRiverGrottoShape(
long seed,
IrisData data,
IrisGeneratorStyle shapeStyle,
IrisGeneratorStyle warpStyle,
double warpStrength
double warpStrength,
double boundaryVariation
) {
IrisGeneratorStyle resolvedShape = shapeStyle == null
? new IrisGeneratorStyle(NoiseStyle.FLAT)
@@ -39,6 +41,7 @@ final class ConfiguredRiverGrottoShape implements RiverCaveGrottoShape {
warpY = resolvedWarp.createNoCache(new RNG(seed ^ WARP_Y_SALT), data);
warpZ = resolvedWarp.createNoCache(new RNG(seed ^ WARP_Z_SALT), data);
this.warpStrength = Math.max(0D, warpStrength);
this.boundaryVariation = Math.max(0D, Math.min(0.75D, boundaryVariation));
}
@Override
@@ -60,7 +63,7 @@ final class ConfiguredRiverGrottoShape implements RiverCaveGrottoShape {
double normalized = (warpedX * warpedX / (horizontalRadius * horizontalRadius))
+ (warpedY * warpedY / (verticalRadius * verticalRadius))
+ (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;
}
}
@@ -48,6 +48,8 @@ public class IrisCaveCarver3D {
private static final int ADAPTIVE_DEEP_SAMPLE_STEP = 8;
private static final double ADAPTIVE_LOCAL_RANGE_SCALE = 0.125D;
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 IrisData data;
@@ -250,9 +252,11 @@ public class IrisCaveCarver3D {
int[] fluidMaxY = scratch.fluidMaxY;
int[] surfaceBreakFloorY = scratch.surfaceBreakFloorY;
boolean[] surfaceBreakColumn = scratch.surfaceBreakColumn;
boolean[] surfaceCeilingColumn = scratch.surfaceCeilingColumn;
double[] columnThreshold = scratch.columnThreshold;
double[] clampedWeights = scratch.clampedColumnWeights;
double[] verticalEdgeFade = prepareVerticalEdgeFadeTable(scratch, minY, maxY);
prepareSurfaceClosureThresholdTable(scratch, minY, maxY);
MatterCavern[] matterByY = prepareMatterByYTable(scratch, minY, maxY);
prepareSectionCaches(scratch, minY, maxY);
@@ -284,7 +288,8 @@ public class IrisCaveCarver3D {
} else {
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
&& surfaceBreakDensity.noiseFastSigned2D(x, z) >= surfaceBreakNoiseThreshold;
int columnTopY = breakColumn
@@ -297,6 +302,7 @@ public class IrisCaveCarver3D {
: Integer.MIN_VALUE;
surfaceBreakFloorY[index] = Math.max(minY, columnSurfaceY - surfaceBreakDepth);
surfaceBreakColumn[index] = breakColumn;
surfaceCeilingColumn[index] = !breakColumn && unclampedClearanceTopY <= maxY;
columnThreshold[index] = (thresholdDensity == null
? constantThreshold
: thresholdDensity.fitDouble(thresholdMin, thresholdMax, x, z)) - thresholdBias;
@@ -487,6 +493,7 @@ public class IrisCaveCarver3D {
localThreshold += surfaceBreakThresholdBoost;
}
localThreshold -= verticalEdgeFade[y - minY];
localThreshold = applySurfaceCeilingFade(scratch, localThreshold, columnIndex, y, minY);
planeThresholdLimit[planeCount] = localThreshold * normalizationFactor;
planeCount++;
}
@@ -514,7 +521,7 @@ public class IrisCaveCarver3D {
}
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);
writeCavern(cavernSlice, localX, y, localZ, matter, fluidSupportPlan);
carved++;
@@ -531,7 +538,7 @@ public class IrisCaveCarver3D {
int localX = PowerOfTwoCoordinates.unpackLocal16X(columnIndex);
int localZ = columnIndex & 15;
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);
writeCavern(cavernSlice, localX, y, localZ, matter, fluidSupportPlan);
carved++;
@@ -624,6 +631,7 @@ public class IrisCaveCarver3D {
localThreshold += surfaceBreakThresholdBoost;
}
localThreshold -= verticalEdgeFade[y - minY];
localThreshold = applySurfaceCeilingFade(scratch, localThreshold, columnIndex, y, minY);
planeThresholdLimit[planeCount] = localThreshold * normalizationFactor;
planeCount++;
}
@@ -662,7 +670,7 @@ public class IrisCaveCarver3D {
}
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);
writeCavern(cavernSlice, localX, y, localZ, matter, fluidSupportPlan);
carved++;
@@ -679,7 +687,7 @@ public class IrisCaveCarver3D {
int localX = PowerOfTwoCoordinates.unpackLocal16X(columnIndex);
int localZ = columnIndex & 15;
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);
writeCavern(cavernSlice, localX, y, localZ, matter, fluidSupportPlan);
carved++;
@@ -822,6 +830,7 @@ public class IrisCaveCarver3D {
localThreshold += surfaceBreakThresholdBoost;
}
localThreshold -= verticalEdgeFade[fadeIndex];
localThreshold = applySurfaceCeilingFade(scratch, localThreshold, index, yy, minY);
if (density > localThreshold) {
continue;
}
@@ -830,7 +839,7 @@ public class IrisCaveCarver3D {
int localZ = tileLocalZ[columnIndex];
int worldX = x0 + localX;
int worldZ = z0 + localZ;
MatterCavern matter = resolveMatter(verticalMatter, worldX, yy, worldZ,
MatterCavern matter = resolveMatter(scratch, verticalMatter, worldX, yy, worldZ,
index, fluidMaxY, localThreshold);
if (skipExistingCarved) {
if (cavernSlice.get(localX, localY, localZ) == null) {
@@ -897,23 +906,23 @@ public class IrisCaveCarver3D {
double threshold = columnThreshold[index] + thresholdBoost - ((1D - columnWeight) * thresholdPenalty);
for (int y = minY; y <= columnTopY; y += sampleStep) {
double localThreshold = threshold;
if (breakColumn && y >= breakFloorY) {
localThreshold += surfaceBreakThresholdBoost;
}
localThreshold -= verticalEdgeFade[y - minY];
if (sampleDensityOptimized(scratch, x, y, z) > localThreshold) {
continue;
}
double density = sampleDensityOptimized(scratch, x, y, z);
int carveMaxY = Math.min(columnTopY, y + sampleStep - 1);
for (int yy = y; yy <= carveMaxY; yy++) {
if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaries, index, yy)) {
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 matter = resolveMatter(verticalMatter, x, yy, z,
MatterCavern matter = resolveMatter(scratch, verticalMatter, x, yy, z,
index, fluidMaxY, localThreshold);
MatterSlice<MatterCavern> cavernSlice = resolveCavernSlice(scratch, chunk, PowerOfTwoCoordinates.floorDivPow2(yy, 4));
int localY = yy & 15;
@@ -2191,57 +2200,61 @@ public class IrisCaveCarver3D {
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) {
if (verticalMatter != carveLava
&& y <= fluidMaxY[columnIndex]
&& isAquiferCandidate(x, y, z, localThreshold)) {
&& isAquiferCandidate(scratch, x, y, z, localThreshold)) {
return carveFluid;
}
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 cutoff = 0.35D + (depthFactor * 0.2D);
if (detailDensity.noiseFastSigned3D(x, y * 0.5D, z) <= cutoff) {
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 deepFloorY = Math.max(0, y - 2);
int aboveY = Math.min(aquiferCeilingY, y + 1);
if (!isDensitySolid(x, floorY, z, threshold)) {
if (!isDensitySolid(scratch, x, floorY, z, threshold)) {
return false;
}
if (!isDensitySolid(x, deepFloorY, z, threshold - 0.05D)) {
if (!isDensitySolid(scratch, x, deepFloorY, z, threshold - 0.05D)) {
return false;
}
int support = 0;
if (isDensitySolid(x + 1, y, z, threshold)) {
if (isDensitySolid(scratch, x + 1, y, z, threshold)) {
support++;
}
if (isDensitySolid(x - 1, y, z, threshold)) {
if (isDensitySolid(scratch, x - 1, y, z, threshold)) {
support++;
}
if (isDensitySolid(x, y, z + 1, threshold)) {
if (isDensitySolid(scratch, x, y, z + 1, threshold)) {
support++;
}
if (isDensitySolid(x, y, z - 1, threshold)) {
if (isDensitySolid(scratch, x, y, z - 1, threshold)) {
support++;
}
if (isDensitySolid(x, aboveY, z, threshold)) {
if (isDensitySolid(scratch, x, aboveY, z, threshold)) {
support++;
}
return support >= 4;
}
private boolean isDensitySolid(int x, int y, int z, double threshold) {
return sampleDensityOptimized(x, y, z) > threshold;
private boolean isDensitySolid(CaveCarveScratch scratch, int x, int y, int z, double threshold) {
return sampleDensityOptimized(scratch, x, y, z) > threshold;
}
private void writeCavern(MatterSlice<MatterCavern> cavernSlice, int localX, int y, int localZ,
@@ -2288,6 +2301,52 @@ public class IrisCaveCarver3D {
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) {
int size = Math.max(0, maxY - minY + 1);
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) {
fillFieldHeights(complex.getHeightStream(), startX, startZ, blendScratch.fieldSurfaceHeights);
fillFieldHeights(complex.getRiverWaterSurfaceStream(), startX, startZ, blendScratch.fieldFluidHeights);
fillFieldFluidPresence(complex.getFluidStream(), startX, startZ, blendScratch.fieldSurfaceHeights,
fillFieldFluidPresence(complex, startX, startZ, blendScratch.fieldSurfaceHeights,
blendScratch.fieldFluidHeights, blendScratch.fieldHasFluid);
fillFieldObjects(complex.getRegionStream(), startX, startZ, blendScratch.fieldRegions);
fillFieldObjects(complex.getTrueBiomeStream(), startX, startZ, blendScratch.fieldSurfaceBiomes);
@@ -500,7 +500,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
}
private void fillFieldFluidPresence(
ProceduralStream<PlatformBlockState> stream,
IrisComplex complex,
int startX,
int startZ,
double[] surfaceHeights,
@@ -511,7 +511,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
int worldX = startX + fieldX;
for (int fieldZ = 0; fieldZ < 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]);
}
}
@@ -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.CaveVoxel;
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.data.cache.Cache;
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.MatterCavern;
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import java.util.Objects;
import java.util.function.BiConsumer;
final class MantleRiverCaveVoxelView implements MantleRiverHydrologyComponent.TunnelVoxelView {
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 Function2<Integer, Integer, Integer> surfaceHeight;
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 surfaceHeightCache;
@@ -33,12 +38,17 @@ final class MantleRiverCaveVoxelView implements MantleRiverHydrologyComponent.Tu
Mantle<Matter> mantle,
int worldHeight,
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.worldHeight = worldHeight;
this.surfaceHeight = Objects.requireNonNull(surfaceHeight);
this.compatibleFluid = Objects.requireNonNull(compatibleFluid);
this.planningFluidKind = Objects.requireNonNull(planningFluidKind);
this.chunkLoader = Objects.requireNonNull(chunkLoader);
loadedChunks = new LongOpenHashSet();
openFloorCache = new Long2IntOpenHashMap();
openFloorCache.defaultReturnValue(CACHE_MISS);
surfaceHeightCache = new Long2IntOpenHashMap();
@@ -52,10 +62,18 @@ final class MantleRiverCaveVoxelView implements MantleRiverHydrologyComponent.Tu
@Override
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);
if (cavern != null) {
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) {
return CaveVoxel.COMPATIBLE_FLUID;
@@ -71,13 +89,13 @@ final class MantleRiverCaveVoxelView implements MantleRiverHydrologyComponent.Tu
if (!block.isFluid()) {
return CaveVoxel.SOLID;
}
if (IrisProceduralBlocks.materialKey(block).endsWith(":lava")) {
return CaveVoxel.LAVA;
}
PlatformBlockState expected = compatibleFluid.apply(position.x(), position.z());
return expected != null
&& IrisProceduralBlocks.materialKey(expected).equals(IrisProceduralBlocks.materialKey(block))
? CaveVoxel.COMPATIBLE_FLUID
if (expected != null
&& IrisProceduralBlocks.materialKey(expected).equals(IrisProceduralBlocks.materialKey(block))) {
return CaveVoxel.COMPATIBLE_FLUID;
}
return IrisProceduralBlocks.materialKey(block).endsWith(":lava")
? CaveVoxel.LAVA
: CaveVoxel.INCOMPATIBLE_FLUID;
}
@@ -99,9 +117,8 @@ final class MantleRiverCaveVoxelView implements MantleRiverHydrologyComponent.Tu
}
@Override
public RiverCaveAction riverActionAt(CavePosition position) {
RiverCaveHydrology hydrology = dataIfPresent(position, RiverCaveHydrology.class);
return hydrology == null ? null : hydrology.action();
public RiverCaveHydrology riverHydrologyAt(CavePosition position) {
return dataIfPresent(position, RiverCaveHydrology.class);
}
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) {
int chunkX = position.x() >> 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));
if (plate == null || plate.isClosed()) {
return null;
@@ -1,17 +1,21 @@
package art.arcane.iris.engine.mantle.components;
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.EngineMantle;
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.object.IrisRiverCaveFallback;
import art.arcane.iris.engine.object.IrisRiverCaveMode;
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.IrisDimension;
import art.arcane.iris.engine.object.IrisRiverNetwork;
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.RiverSample;
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.RiverCaveAction;
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.RiverCaveHydrology;
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.volmlib.util.mantle.flag.MantleFlag;
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.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
@@ -47,6 +57,8 @@ import java.util.Set;
@ComponentFlag(ReservedFlag.RIVER_HYDROLOGY)
public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
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;
private static final int[] FALLBACK_X = {0, 1, -1, 0, 0};
private static final int[] FALLBACK_Z = {0, 0, 0, 1, -1};
@@ -73,6 +85,11 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
return PREREQUISITES;
}
@Override
public boolean isInputGenerationLazy() {
return true;
}
@Override
public int getInputRadius() {
if (!getDimension().isCarvingEnabled()
@@ -87,6 +104,122 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
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
public void generateLayer(MantleWriter writer, int chunkX, int chunkZ, ChunkContext context) {
IrisRiverRuntime runtime = context.getComplex().getRiverRuntime();
@@ -96,11 +229,21 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
publishTunnels(writer, context, runtime, chunkX, chunkZ);
IrisRiverCaves caves = runtime.caveSettings();
if (caves.getMode() == IrisRiverCaveMode.SEALED || caves.getMaximumPerReach() <= 0) {
return;
if (caves.getMode() != IrisRiverCaveMode.SEALED && caves.getMaximumPerReach() > 0) {
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 minimumX = (chunkX << 4) - candidateHalo;
int minimumZ = (chunkZ << 4) - candidateHalo;
@@ -146,11 +289,202 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
}
RiverCavePlanningResult result = planner.planAll(view, sources, settings);
MantleRiverCaveVoxelView revalidationView = createView(writer, context);
MantleRiverCaveVoxelView revalidationView = createView(
writer,
context,
RiverCaveFluidKind.RIVER
);
if (!preconditionsHold(revalidationView, result.baselinePreconditions())) {
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
@@ -176,9 +510,15 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
return false;
}
IrisRiverCaves caves = dimension.getRivers().getCaves();
return caves != null
&& caves.getMode() != IrisRiverCaveMode.SEALED
if (caves == null) {
return false;
}
boolean caveConnections = caves.getMode() != IrisRiverCaveMode.SEALED
&& caves.getMaximumPerReach() > 0;
IrisRiverDeepPools deepPools = caves.getDeepPools();
return caveConnections || deepPools != null
&& deepPools.isEnabled()
&& deepPools.getMaximumPerReach() > 0;
}
static int planningHalo(IrisRiverCaves caves) {
@@ -186,16 +526,33 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
}
static int inputRadius(IrisRiverCaves caves, int tunnelRadius) {
if (caves.getMode() == IrisRiverCaveMode.SEALED || caves.getMaximumPerReach() <= 0) {
return tunnelRadius;
int radius = 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) {
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) {
int generatedRadius = generatedGrottoPublicationRadius(caves);
return switch (caves.getMode()) {
@@ -296,13 +653,13 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
boolean changed;
do {
changed = false;
Set<CavePosition> candidateActions = mergeActions(containedColumns).keySet();
Long2ObjectOpenHashMap<TunnelColumn> candidateColumns = indexColumns(containedColumns);
for (int index = containedColumns.size() - 1; index >= 0; index--) {
TunnelColumn column = containedColumns.get(index);
if (!isTunnelColumnContained(
view,
column,
candidateActions,
candidateColumns,
dryHeadroom,
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(
@@ -360,8 +720,9 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
if (sample == null) {
return null;
}
LinkedHashMap<CavePosition, RiverCaveAction> actions = new LinkedHashMap<>();
for (int y = sample.bedY() + 1; y <= sample.ceilingY(); y++) {
int minimumY = sample.bedY() + 1;
int maximumY = sample.ceilingY();
for (int y = minimumY; y <= maximumY; y++) {
CavePosition position = new CavePosition(x, y, z);
RiverCaveAction action = y <= sample.waterHeadY()
? RiverCaveAction.WET_SOURCE
@@ -371,22 +732,24 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
&& !matchesPublishedAction(view, position, action))) {
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(
CaveVoxelView view,
TunnelColumn column,
Set<CavePosition> candidateActions,
Long2ObjectOpenHashMap<TunnelColumn> candidateColumns,
int dryHeadroom,
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) {
CavePosition neighbor = offset(position, offset);
if (candidateActions.contains(neighbor)) {
if (containsAction(candidateColumns, neighbor)) {
continue;
}
if (!view.isInWorld(neighbor)) {
@@ -417,7 +780,9 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
SurfaceSampler surfaceSampler
) {
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;
}
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) {
LinkedHashMap<CavePosition, RiverCaveAction> actions = new LinkedHashMap<>();
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;
}
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(
CaveVoxelView view,
CavePosition position,
RiverCaveAction action
) {
return view instanceof TunnelVoxelView tunnelView
&& tunnelView.riverActionAt(position) == action;
if (!(view instanceof TunnelVoxelView tunnelView)) {
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) {
@@ -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(
writer.getMantle(),
writer.getMantle().getWorldHeight(),
(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(
MantleWriter writer,
ChunkContext context,
@@ -467,7 +886,11 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
int chunkZ
) {
for (int attempt = 0; attempt < 2; attempt++) {
MantleRiverCaveVoxelView view = createView(writer, context);
MantleRiverCaveVoxelView view = createView(
writer,
context,
RiverCaveFluidKind.RIVER
);
TunnelPlan plan = planTunnels(
view,
chunkX,
@@ -478,7 +901,11 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
runtime::sampleTunnel,
runtime::sample
);
MantleRiverCaveVoxelView revalidationView = createView(writer, context);
MantleRiverCaveVoxelView revalidationView = createView(
writer,
context,
RiverCaveFluidKind.RIVER
);
if (preconditionsHold(revalidationView, plan.preconditions())) {
publishTunnelLocal(writer, chunkX, chunkZ, plan);
return;
@@ -505,7 +932,8 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
data,
caves.getGrottoShapeStyle(),
caves.getGrottoWarpStyle(),
caves.getGrottoWarpStrength()
caves.getGrottoWarpStrength(),
0.2D
),
caves.getMaxFloodRadius(),
caves.getMaxFloodDepth()
@@ -673,7 +1101,8 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
int chunkX,
int chunkZ,
RiverCavePlanningResult result,
Map<Long, String> floodedBiomes
Map<Long, String> floodedBiomes,
RiverCaveFluidKind fluidKind
) {
Map<CavePosition, RiverCaveSource> owners = actionOwners(result);
ArrayList<Map.Entry<CavePosition, RiverCaveAction>> actions = new ArrayList<>(result.actions().entrySet());
@@ -692,7 +1121,7 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
position.x(),
position.y(),
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) {
CavePosition position = entry.getKey();
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 {
RiverCaveAction riverActionAt(CavePosition position);
RiverCaveHydrology riverHydrologyAt(CavePosition position);
}
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()) {
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 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,
upperSurfaceHeights,
worldHeightSpan,
caveLavaHeight
caveLavaHeight,
chunkBlockX,
chunkBlockZ
);
CarveResolver carveResolver = new CarveResolver(resolutionContext);
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++) {
int localX = PowerOfTwoCoordinates.unpackLocal16X(columnIndex);
int localZ = columnIndex & 15;
processColumnFromMask(
output,
mantleChunk,
@@ -178,8 +178,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
z,
resolverState,
caveBiomeCache,
customBiomeCache,
context.getFluid().get(localX, localZ)
customBiomeCache
);
}
@@ -279,7 +278,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
}
return switch (hydrology.action()) {
case WET_SOURCE -> fluid;
case FALLING_WATER -> fallingFluidState(fluid);
case FALLING_FLUID -> fallingFluidState(fluid);
case DRY_AIR -> air;
case SEAL_GUARD -> normalizeWaterlogging(current, null);
};
@@ -369,9 +368,16 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
return;
}
PlatformBlockState fluid = isFluidIntent(cavern)
? context.chunkContext().getFluid().get(localX, localZ)
: null;
PlatformBlockState fluid = null;
if (isFluidIntent(cavern)) {
fluid = hydrology == null
? context.chunkContext().getFluid().get(localX, localZ)
: getComplex().resolveRiverCaveFluid(
hydrology.fluidKind(),
context.chunkBlockX() + localX,
context.chunkBlockZ() + localZ
);
}
if (hydrology != null) {
context.output().setRaw(localX, y, localZ,
resolveHydrologyState(hydrology, current, fluid, AIR));
@@ -395,7 +401,9 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
CarveColumnMask[] columnMasks,
int[] upperSurfaceHeights,
int worldHeightSpan,
int caveLavaHeight
int caveLavaHeight,
int chunkBlockX,
int chunkBlockZ
) {
}
@@ -572,8 +580,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
int chunkZ,
IrisDimensionCarvingResolver.State resolverState,
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache,
Map<String, IrisBiome> customBiomeCache,
PlatformBlockState columnFluid
Map<String, IrisBiome> customBiomeCache
) {
if (columnMask == null || columnMask.isEmpty()) {
return;
@@ -601,7 +608,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
} else {
if (zone.isValid(getEngine())) {
processZone(output, mc, mantle, zone, rx, rz, worldX, worldZ, resolverState,
caveBiomeCache, customBiomeCache, columnFluid);
caveBiomeCache, customBiomeCache);
}
zone = new CaveZone();
zone.setFloor(y);
@@ -614,7 +621,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
if (zone.isValid(getEngine())) {
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,
IrisDimensionCarvingResolver.State resolverState,
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache,
Map<String, IrisBiome> customBiomeCache,
PlatformBlockState columnFluid) {
Map<String, IrisBiome> customBiomeCache) {
int maxY = output.getHeight();
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 ceilingBiome = resolveCaveBoundaryBiome(mc, rx, zone.ceiling, rz, xx, zz, resolverState, caveBiomeCache, customBiomeCache);
if (floorBiome == null && ceilingBiome == null) {
normalizeCaveZoneWaterlogging(output, mc, zone, rx, rz, columnFluid);
normalizeCaveZoneWaterlogging(output, mc, zone, rx, rz, xx, zz);
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());
}
normalizeCaveZoneWaterlogging(output, mc, zone, rx, rz, columnFluid);
normalizeCaveZoneWaterlogging(output, mc, zone, rx, rz, xx, zz);
}
private void normalizeCaveZoneWaterlogging(
@@ -874,7 +880,8 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
CaveZone zone,
int localX,
int localZ,
PlatformBlockState columnFluid
int worldX,
int worldZ
) {
int minimumY = Math.max(0, zone.floor - 1);
int maximumY = Math.min(output.getHeight() - 1, zone.ceiling + 1);
@@ -887,6 +894,11 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
MatterCavern baseline = dataIfPresent(
mantleChunk, localX, y, localZ, MatterCavern.class);
PlatformBlockState current = output.getRaw(localX, y, localZ);
PlatformBlockState columnFluid = getComplex().resolveRiverCaveFluid(
hydrology.fluidKind(),
worldX,
worldZ
);
PlatformBlockState normalized = normalizeHydrologyWaterlogging(
current,
baseline,
@@ -10,7 +10,10 @@ import lombok.experimental.Accessors;
@Desc("A two-dimensional image-map coordinate")
@Data
public class IrisImageMapOrigin {
@Desc("X coordinate in world blocks for origin, or source pixels for sourceOrigin")
private double x = 0D;
@Desc("Z coordinate in world blocks for origin, or source image Y pixels for sourceOrigin")
private double z = 0D;
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.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.
*/
@@ -133,6 +138,7 @@ final class IrisObjectTransforms {
VectorMap<PlatformBlockState> b = new VectorMap<>();
IrisPosition min = self.getAABB().min();
IrisPosition max = self.getAABB().max();
NearestBlockIndex nearestBlocks = NearestBlockIndex.create(v);
for (int x = min.getX(); x <= max.getX(); x++) {
for (int y = min.getY(); y <= max.getY(); y++) {
@@ -146,7 +152,7 @@ final class IrisObjectTransforms {
return 1;
}) >= 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 {
b.put(new IrisBlockVector(x, y, z), IrisObject.States.AIR);
}
@@ -169,6 +175,7 @@ final class IrisObjectTransforms {
VectorMap<PlatformBlockState> b = new VectorMap<>();
IrisPosition min = self.getAABB().min();
IrisPosition max = self.getAABB().max();
NearestBlockIndex nearestBlocks = NearestBlockIndex.create(v);
for (int x = min.getX(); x <= max.getX(); x++) {
for (int y = min.getY(); y <= max.getY(); y++) {
@@ -182,7 +189,7 @@ final class IrisObjectTransforms {
return 1;
}) >= 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 {
b.put(new IrisBlockVector(x, y, z), IrisObject.States.AIR);
}
@@ -209,6 +216,7 @@ final class IrisObjectTransforms {
VectorMap<PlatformBlockState> b = new VectorMap<>();
IrisPosition min = self.getAABB().min();
IrisPosition max = self.getAABB().max();
NearestBlockIndex nearestBlocks = NearestBlockIndex.create(v);
for (int x = min.getX(); x <= max.getX(); x++) {
for (int y = min.getY(); y <= max.getY(); y++) {
@@ -222,7 +230,7 @@ final class IrisObjectTransforms {
return 1;
}, 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 {
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);
self.readLock.lock();
try {
PlatformBlockState r = self.blocks.get(vv);
PlatformBlockState direct = blocks.get(vv);
if (!B.isAir(direct)) {
return direct;
}
return nearestBlocks.nearest(x, y, z, direct);
}
if (!B.isAir(r)) {
return r;
static final class NearestBlockIndex {
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 dat = entry.getValue();
if (B.isAir(dat)) {
continue;
}
double dx = entry.getKey().distanceSquared(vv);
if (dx < d) {
d = dx;
r = dat;
}
PlatformBlockState nearest(int x, int y, int z, PlatformBlockState fallback) {
if (root == null) {
return fallback;
}
return r;
} finally {
self.readLock.unlock();
bestState = fallback;
bestDistance = Double.MAX_VALUE;
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.")
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.")
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)
@MaxNumber(2048)
@Desc("The final wet-channel width cap after region, biome, and stream-order scaling.")
@@ -64,7 +69,7 @@ public class IrisRiverTerrain {
@MinNumber(0)
@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;
@Desc("Noise modulating the submerged floor of river tunnels.")
@@ -13,7 +13,15 @@ import lombok.experimental.Accessors;
@Data
public class IrisRiverWater {
@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)
@MaxNumber(4096)
@@ -22,7 +30,7 @@ public class IrisRiverWater {
@MinNumber(0)
@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;
@MinNumber(1)
@@ -4,8 +4,8 @@ import art.arcane.iris.engine.object.annotations.Desc;
@Desc("Selects how a river determines its surface fluid height.")
public enum IrisRiverWaterMode {
@Desc("Use the dimension fluid height for every wet river reach.")
SEA_LEVEL,
@Desc("Use the river water configuration's fixed fluid height for every wet reach.")
FIXED,
@Desc("Use flat pools connected by controlled vertical drops.")
TERRACED
@@ -72,16 +72,21 @@ public class IrisRiverWorm {
@Desc("Depth multiplier for reaches selecting this worm.")
private double depthMultiplier = 1D;
@MinNumber(32)
@MinNumber(8)
@MaxNumber(16384)
@Desc("Primary world-space wavelength controlling longitudinal body swelling and pinching.")
private double bodyWavelength = 512D;
@MinNumber(32)
@MinNumber(8)
@MaxNumber(16384)
@Desc("Detail wavelength adding smaller changes to the longitudinal body profile.")
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)
@MaxNumber(0.875)
@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 volatile long lastChunkGenTime = 0L;
private final CompletableFuture<Integer> spawnChunks = new CompletableFuture<>();
private final CompletableFuture<Void> initialSpawnReady = new CompletableFuture<>();
private final AtomicCache<EngineTarget> targetCache = new AtomicCache<>();
private final AtomicReference<CompletableFuture<Void>> closeFuture = new AtomicReference<>();
private volatile Engine engine;
@@ -194,11 +195,19 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
engine.getPlatformHooks().applyWorldBoundary(engine);
IrisLogging.debug("Injected Iris Biome Source into " + world.getName());
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) {
initializationFailure = e;
spawnChunks.completeExceptionally(e);
initialSpawnReady.completeExceptionally(e);
IrisLogging.reportError(e);
IrisLogging.error("Failed to initialize Iris generator for " + world.getName());
if (e instanceof RuntimeException runtimeException) {
@@ -231,16 +240,58 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
}
private void updateSpawnLocation(World world) {
Location initialSpawn = getInitialSpawnLocation(world);
int chunkX = initialSpawn.getBlockX() >> 4;
int chunkZ = initialSpawn.getBlockZ() >> 4;
CompletableFuture<Chunk> chunkFuture = requestChunkAsync(world, chunkX, chunkZ, true);
if (chunkFuture == null) {
return;
}
try {
Location initialSpawn = getInitialSpawnLocation(world);
int chunkX = initialSpawn.getBlockX() >> 4;
int chunkZ = initialSpawn.getBlockZ() >> 4;
CompletableFuture<Chunk> chunkFuture = requestChunkAsync(world, chunkX, chunkZ, true);
if (chunkFuture == null) {
initialSpawnReady.completeExceptionally(new IllegalStateException(
"Initial spawn chunk request returned no completion future for world \""
+ world.getName() + "\"."));
return;
}
chunkFuture.thenAccept(chunk ->
J.runRegion(chunk.getWorld(), chunk.getX(), chunk.getZ(), () -> applySpawnLocation(chunk.getWorld(), initialSpawn)));
chunkFuture.whenComplete((chunk, failure) -> {
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) {
@@ -255,10 +306,27 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
int minY = world.getMinHeight() + 1;
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()));
}
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")
private CompletableFuture<Chunk> requestChunkAsync(World world, int chunkX, int chunkZ, boolean generate) {
try {
@@ -377,6 +445,11 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
this.hotloader = shouldRunStudioHotload(studio, closing, jigsawStudioActive) ? new Looper() {
@Override
protected long loop() {
Engine activeEngine = engine;
if (activeEngine instanceof IrisEngine irisEngine
&& irisEngine.isGenerationCacheWarmPending()) {
return HOTLOAD_LOOP_DELAY_MS;
}
if (shouldThrottleHotload()) {
return HOTLOAD_MAINTENANCE_DELAY_MS;
}
@@ -76,4 +76,8 @@ public interface PlatformChunkGenerator extends Hotloadable, DataProvider {
}
CompletableFuture<Integer> getSpawnChunks();
default CompletableFuture<Void> getInitialSpawnReady() {
return CompletableFuture.completedFuture(null);
}
}
@@ -119,6 +119,15 @@ public final class RiverBodyProfile {
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
public boolean equals(Object 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_GATE_SALT = 0x2545F4914F6CDD1DL;
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 final RiverNetworkOptions options;
@@ -670,7 +670,8 @@ public final class RiverNetwork {
worm.bodyDetailWavelength(),
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) {
@@ -1263,6 +1264,7 @@ public final class RiverNetwork {
BODY_WIDTH_DETAIL_SALT,
worm.widthVariation()
)
+ options.channelRadiusBonus() * 2D
)
);
double baseBankWidth = nonNegativeOrFallback(
@@ -29,6 +29,7 @@ public record RiverNetworkOptions(
double channelWidth,
double bankWidth,
double depth,
double channelRadiusBonus,
double maxChannelWidth,
double maxBankWidth,
double maxDepth,
@@ -60,6 +61,7 @@ public record RiverNetworkOptions(
requirePositive(channelWidth, "channelWidth");
requireFiniteNonNegative(bankWidth, "bankWidth");
requirePositive(depth, "depth");
requireFiniteNonNegative(channelRadiusBonus, "channelRadiusBonus");
requirePositive(maxChannelWidth, "maxChannelWidth");
requireFiniteNonNegative(maxBankWidth, "maxBankWidth");
requirePositive(maxDepth, "maxDepth");
@@ -215,6 +217,7 @@ public record RiverNetworkOptions(
private double channelWidth;
private double bankWidth;
private double depth;
private double channelRadiusBonus;
private double maxChannelWidth;
private double maxBankWidth;
private double maxDepth;
@@ -269,6 +272,7 @@ public record RiverNetworkOptions(
1D,
512D,
128D,
0.3D,
0D,
0D,
0D,
@@ -397,6 +401,11 @@ public record RiverNetworkOptions(
return this;
}
public Builder channelRadiusBonus(double value) {
channelRadiusBonus = value;
return this;
}
public Builder maxChannelWidth(double value) {
maxChannelWidth = value;
return this;
@@ -461,6 +470,7 @@ public record RiverNetworkOptions(
channelWidth,
bankWidth,
depth,
channelRadiusBonus,
maxChannelWidth,
maxBankWidth,
maxDepth,
@@ -325,25 +325,35 @@ public final class RiverTile {
double additionalRadius
) {
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 radius = reach.widthAt(0D) * 0.5D + reach.bankWidthAt(0D) + additionalRadius;
return distanceSquared <= radius * radius ? new ClosestPoint(distanceSquared, 0D) : null;
}
double nearest = Double.POSITIVE_INFINITY;
double nearestAlong = 0.0;
for (int point = 0; point < polyline.size() - 1; point++) {
double segmentStartAlong = polyline.cumulativeLength(point) / polyline.length();
double segmentEndAlong = polyline.cumulativeLength(point + 1) / polyline.length();
int pointLimit = polyline.size() - 1;
int profileLimit = bodyProfile.size() - 1;
for (int point = 0; point < pointLimit; point++) {
double segmentStartAlong = polyline.cumulativeLength(point) / polylineLength;
double segmentEndAlong = polyline.cumulativeLength(point + 1) / polylineLength;
double segmentAlongSpan = segmentEndAlong - segmentStartAlong;
if (segmentAlongSpan == 0D) {
continue;
}
double deltaX = polyline.x(point + 1) - polyline.x(point);
double deltaZ = polyline.z(point + 1) - polyline.z(point);
for (int profileIndex = 0; profileIndex < reach.bodyProfile().size() - 1; profileIndex++) {
double profileStart = reach.bodyProfile().position(profileIndex);
double profileEnd = reach.bodyProfile().position(profileIndex + 1);
double startX = polyline.x(point);
double startZ = polyline.z(point);
double deltaX = polyline.x(point + 1) - startX;
double deltaZ = polyline.z(point + 1) - startZ;
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 overlapEnd = StrictMath.min(segmentEndAlong, profileEnd);
if (overlapStart > overlapEnd) {
@@ -351,13 +361,16 @@ public final class RiverTile {
}
double intervalStart = (overlapStart - segmentStartAlong) / segmentAlongSpan;
double intervalEnd = (overlapEnd - segmentStartAlong) / segmentAlongSpan;
double widthSlope = (reach.bodyProfile().widthAtIndex(profileIndex + 1)
- reach.bodyProfile().widthAtIndex(profileIndex)) / (profileEnd - profileStart);
double bankSlope = (reach.bodyProfile().bankWidthAtIndex(profileIndex + 1)
- reach.bodyProfile().bankWidthAtIndex(profileIndex)) / (profileEnd - profileStart);
double radiusBase = (reach.bodyProfile().widthAtIndex(profileIndex)
double profileSpan = profileEnd - profileStart;
double profileWidth = bodyProfile.widthAtIndex(profileIndex);
double profileBankWidth = bodyProfile.bankWidthAtIndex(profileIndex);
double widthSlope = (bodyProfile.widthAtIndex(profileIndex + 1)
- profileWidth) / profileSpan;
double bankSlope = (bodyProfile.bankWidthAtIndex(profileIndex + 1)
- profileBankWidth) / profileSpan;
double radiusBase = (profileWidth
+ widthSlope * (segmentStartAlong - profileStart)) * 0.5D
+ reach.bodyProfile().bankWidthAtIndex(profileIndex)
+ profileBankWidth
+ bankSlope * (segmentStartAlong - profileStart)
+ additionalRadius;
double radiusSlope = (widthSlope * 0.5D + bankSlope) * segmentAlongSpan;
@@ -365,9 +378,9 @@ public final class RiverTile {
intervalStart,
intervalEnd,
deltaX,
polyline.x(point) - x,
startX - x,
deltaZ,
polyline.z(point) - z,
startZ - z,
radiusSlope,
radiusBase,
segmentStartAlong,
@@ -390,7 +403,9 @@ public final class RiverTile {
double maximumZ
) {
RiverPolyline polyline = reach.polyline();
if (polyline.length() == 0D) {
RiverBodyProfile bodyProfile = reach.bodyProfile();
double polylineLength = polyline.length();
if (polylineLength == 0D) {
double distanceSquared = pointRectangleDistanceSquared(
polyline.x(0),
polyline.z(0),
@@ -404,9 +419,11 @@ public final class RiverTile {
}
double nearest = Double.POSITIVE_INFINITY;
double nearestAlong = 0.0;
for (int point = 0; point < polyline.size() - 1; point++) {
double segmentStartAlong = polyline.cumulativeLength(point) / polyline.length();
double segmentEndAlong = polyline.cumulativeLength(point + 1) / polyline.length();
int pointLimit = polyline.size() - 1;
int profileLimit = bodyProfile.size() - 1;
for (int point = 0; point < pointLimit; point++) {
double segmentStartAlong = polyline.cumulativeLength(point) / polylineLength;
double segmentEndAlong = polyline.cumulativeLength(point + 1) / polylineLength;
double segmentAlongSpan = segmentEndAlong - segmentStartAlong;
if (segmentAlongSpan == 0D) {
continue;
@@ -415,9 +432,13 @@ public final class RiverTile {
double startZ = polyline.z(point);
double deltaX = polyline.x(point + 1) - startX;
double deltaZ = polyline.z(point + 1) - startZ;
for (int profileIndex = 0; profileIndex < reach.bodyProfile().size() - 1; profileIndex++) {
double profileStart = reach.bodyProfile().position(profileIndex);
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 overlapEnd = StrictMath.min(segmentEndAlong, profileEnd);
if (overlapStart > overlapEnd) {
@@ -425,13 +446,16 @@ public final class RiverTile {
}
double intervalStart = (overlapStart - segmentStartAlong) / segmentAlongSpan;
double intervalEnd = (overlapEnd - segmentStartAlong) / segmentAlongSpan;
double widthSlope = (reach.bodyProfile().widthAtIndex(profileIndex + 1)
- reach.bodyProfile().widthAtIndex(profileIndex)) / (profileEnd - profileStart);
double bankSlope = (reach.bodyProfile().bankWidthAtIndex(profileIndex + 1)
- reach.bodyProfile().bankWidthAtIndex(profileIndex)) / (profileEnd - profileStart);
double radiusBase = (reach.bodyProfile().widthAtIndex(profileIndex)
double profileSpan = profileEnd - profileStart;
double profileWidth = bodyProfile.widthAtIndex(profileIndex);
double profileBankWidth = bodyProfile.bankWidthAtIndex(profileIndex);
double widthSlope = (bodyProfile.widthAtIndex(profileIndex + 1)
- profileWidth) / profileSpan;
double bankSlope = (bodyProfile.bankWidthAtIndex(profileIndex + 1)
- profileBankWidth) / profileSpan;
double radiusBase = (profileWidth
+ widthSlope * (segmentStartAlong - profileStart)) * 0.5D
+ reach.bodyProfile().bankWidthAtIndex(profileIndex)
+ profileBankWidth
+ bankSlope * (segmentStartAlong - profileStart);
double radiusSlope = (widthSlope * 0.5D + bankSlope) * segmentAlongSpan;
double cursor = intervalStart;
@@ -626,8 +650,7 @@ public final class RiverTile {
}
private List<RiverReach> indexedReaches(double x, double z) {
List<RiverReach> indexed = spatialIndex.get(bucketKey(bucket(x), bucket(z)));
return indexed == null ? List.of() : indexed;
return spatialIndex.getOrDefault(bucketKey(bucket(x), bucket(z)), List.of());
}
private List<RiverReach> indexedReaches(
@@ -641,12 +664,15 @@ public final class RiverTile {
int maximumBucketX = bucket(StrictMath.nextDown(queryMaximumX));
int minimumBucketZ = bucket(queryMinimumZ);
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 bucketZ = minimumBucketZ; bucketZ <= maximumBucketZ; bucketZ++) {
List<RiverReach> bucketReaches = spatialIndex.get(bucketKey(bucketX, bucketZ));
if (bucketReaches != null) {
indexed.addAll(bucketReaches);
}
indexed.addAll(spatialIndex.getOrDefault(bucketKey(bucketX, bucketZ), List.of()));
}
}
return List.copyOf(indexed);
@@ -662,26 +688,13 @@ public final class RiverTile {
int maximumBucketX = bucket(queryMaximumX);
int minimumBucketZ = bucket(queryMinimumZ);
int maximumBucketZ = bucket(queryMaximumZ);
if (spatialIndex.isEmpty()) {
return List.of();
}
if (minimumBucketX == maximumBucketX && minimumBucketZ == maximumBucketZ) {
List<RiverReach> bucketReaches = spatialIndex.get(bucketKey(minimumBucketX, minimumBucketZ));
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;
return indexedReaches(queryMinimumX, queryMinimumZ);
}
LinkedHashSet<RiverReach> indexed = new LinkedHashSet<>();
for (int bucketX = minimumBucketX; bucketX <= maximumBucketX; bucketX++) {
for (int bucketZ = minimumBucketZ; bucketZ <= maximumBucketZ; bucketZ++) {
List<RiverReach> bucketReaches = spatialIndex.get(bucketKey(bucketX, bucketZ));
if (bucketReaches != null) {
indexed.addAll(bucketReaches);
}
indexed.addAll(spatialIndex.getOrDefault(bucketKey(bucketX, bucketZ), List.of()));
}
}
return List.copyOf(indexed);
@@ -17,6 +17,7 @@ public record RiverWorm(
double depthMultiplier,
double bodyWavelength,
double bodyDetailWavelength,
double bodyDetailInfluence,
double widthVariation,
double bankVariation,
double depthVariation,
@@ -44,8 +45,9 @@ public record RiverWorm(
requireRange(widthMultiplier, 0.125D, 8D, "widthMultiplier");
requireRange(bankMultiplier, 0.125D, 8D, "bankMultiplier");
requireRange(depthMultiplier, 0.125D, 8D, "depthMultiplier");
requireRange(bodyWavelength, 32D, 16384D, "bodyWavelength");
requireRange(bodyDetailWavelength, 32D, 16384D, "bodyDetailWavelength");
requireRange(bodyWavelength, 8D, 16384D, "bodyWavelength");
requireRange(bodyDetailWavelength, 8D, 16384D, "bodyDetailWavelength");
requireRange(bodyDetailInfluence, 0D, 1D, "bodyDetailInfluence");
requireRange(widthVariation, 0D, 0.875D, "widthVariation");
requireRange(bankVariation, 0D, 0.875D, "bankVariation");
requireRange(depthVariation, 0D, 0.875D, "depthVariation");
@@ -2,7 +2,7 @@ package art.arcane.iris.engine.river.cave;
public enum RiverCaveAction {
WET_SOURCE,
FALLING_WATER,
FALLING_FLUID,
DRY_AIR,
SEAL_GUARD
}
@@ -70,6 +70,7 @@ public final class RiverCaveContainmentPlanner {
settings,
throat.positions()
);
case DEEP_POOL -> planDeepPool(view, source, settings, throat.positions());
};
}
@@ -302,6 +303,111 @@ public final class RiverCaveContainmentPlanner {
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(
CaveVoxelView view,
RiverCaveSource source,
@@ -732,7 +838,7 @@ public final class RiverCaveContainmentPlanner {
action = RiverCaveAction.WET_SOURCE;
} else if (source.mode() == RiverCaveMode.WATERFALL_POOL
|| source.mode() == RiverCaveMode.GENERATED_GROTTO) {
action = RiverCaveAction.FALLING_WATER;
action = RiverCaveAction.FALLING_FLUID;
} else {
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 String floodedBiomeKey;
private final RiverCaveFluidKind fluidKind;
private final MatterCavern cavern;
public RiverCaveHydrology(RiverCaveAction action, String floodedBiomeKey) {
public RiverCaveHydrology(
RiverCaveAction action,
String floodedBiomeKey,
RiverCaveFluidKind fluidKind
) {
this.action = Objects.requireNonNull(action);
this.floodedBiomeKey = floodedBiomeKey == null ? "" : floodedBiomeKey.trim();
this.fluidKind = Objects.requireNonNull(fluidKind);
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 SEAL_GUARD -> null;
};
}
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() {
@@ -36,11 +46,11 @@ public final class RiverCaveHydrology {
}
public boolean isWet() {
return action == RiverCaveAction.WET_SOURCE || action == RiverCaveAction.FALLING_WATER;
return action == RiverCaveAction.WET_SOURCE || action == RiverCaveAction.FALLING_FLUID;
}
public boolean isFalling() {
return action == RiverCaveAction.FALLING_WATER;
return action == RiverCaveAction.FALLING_FLUID;
}
public boolean protectsPlacement() {
@@ -59,6 +69,10 @@ public final class RiverCaveHydrology {
return floodedBiomeKey;
}
public RiverCaveFluidKind fluidKind() {
return fluidKind;
}
@Override
public boolean equals(Object object) {
if (this == object) {
@@ -67,16 +81,19 @@ public final class RiverCaveHydrology {
if (!(object instanceof RiverCaveHydrology hydrology)) {
return false;
}
return action == hydrology.action && floodedBiomeKey.equals(hydrology.floodedBiomeKey);
return action == hydrology.action
&& floodedBiomeKey.equals(hydrology.floodedBiomeKey)
&& fluidKind == hydrology.fluidKind;
}
@Override
public int hashCode() {
return (31 * action.hashCode()) + floodedBiomeKey.hashCode();
return (31 * ((31 * action.hashCode()) + floodedBiomeKey.hashCode())) + fluidKind.hashCode();
}
@Override
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,
GENERATED_GROTTO,
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.IrisRiverCaveMode;
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.IrisRiverRoutingPolicy;
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.volmlib.util.collection.KList;
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.HashSet;
@@ -48,6 +51,7 @@ import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReferenceArray;
public final class IrisRiverRuntime implements AutoCloseable {
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 CAVE_ENTRY_NOISE_SALT = 0xB8E1AFED6A267E96L;
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_ROOF_NOISE_SALT = 0xDB4F0B9175AE2165L;
private static final long TUNNEL_WIDTH_NOISE_SALT = 0xC6EF372FE94F82BEL;
private static final long FLOODED_CAVE_BIOME_SALT = 0x24A19947B3916CF7L;
private static final long TERMINAL_CAVE_ANCHOR_SALT = 0x9E3779B97F4A7C15L;
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 IrisRiverNetwork configuration;
private final IrisData data;
private final int fluidHeight;
private final int riverFluidHeight;
private final int dimensionFluidHeight;
private final boolean boreMantleActive;
private final boolean caveHydrologyActive;
private final boolean blockingRoutingPossible;
@@ -101,12 +110,14 @@ public final class IrisRiverRuntime implements AutoCloseable {
private final CNG bedNoise;
private final CNG biomeNoise;
private final CNG caveEntryNoise;
private final CNG deepPoolReachNoise;
private final CNG tunnelFloorNoise;
private final CNG tunnelRoofNoise;
private final CNG tunnelWidthNoise;
private final art.arcane.iris.engine.river.RiverNetwork network;
private final RuntimeTerrainSampler terrainSampler;
private final RiverTileCache tileCache;
private final Cache<Long, TunnelSampleChunk> tunnelSampleCache;
private final ConcurrentHashMap<IdentitySettingsKey, EffectiveRiverSettings> settingsCache;
private final ConcurrentHashMap<BiomePoolKey, List<IrisBiome>> biomePoolCache;
@@ -115,7 +126,8 @@ public final class IrisRiverRuntime implements AutoCloseable {
seed = context.seed();
configuration = context.configuration();
data = context.data();
fluidHeight = context.fluidHeight();
riverFluidHeight = context.riverFluidHeight();
dimensionFluidHeight = context.dimensionFluidHeight();
boreMantleActive = context.boreMantleActive();
caveHydrologyActive = context.caveHydrologyActive();
blockingRoutingPossible = context.blockingRoutingPossible();
@@ -146,6 +158,11 @@ public final class IrisRiverRuntime implements AutoCloseable {
bedNoise = noise(terrain.getBedRoughnessStyle(), BED_NOISE_SALT);
biomeNoise = noise(configuration.getBiomes().getSelectionStyle(), BIOME_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);
tunnelRoofNoise = noise(terrain.getTunnelRoofStyle(), TUNNEL_ROOF_NOISE_SALT);
tunnelWidthNoise = noise(terrain.getTunnelWidthMultiplier(), TUNNEL_WIDTH_NOISE_SALT);
@@ -158,12 +175,15 @@ public final class IrisRiverRuntime implements AutoCloseable {
TILE_CACHE_SIZE,
(tileX, tileZ) -> network.buildTile(tileX, tileZ, terrainSampler)
);
tunnelSampleCache = Caffeine.newBuilder()
.maximumSize(TUNNEL_SAMPLE_CHUNK_CACHE_SIZE)
.build();
}
public IrisRiverSurfaceSample sample(double x, double z) {
ResolvedRiverColumn column = resolveColumn(x, z);
if (column == null) {
return IrisRiverSurfaceSample.none(naturalHeight.get(x, z), fluidHeight);
return IrisRiverSurfaceSample.none(naturalHeight.get(x, z), dimensionFluidHeight);
}
if (column.subterranean()) {
return new IrisRiverSurfaceSample(
@@ -229,9 +249,8 @@ public final class IrisRiverRuntime implements AutoCloseable {
if (!column.subterranean()) {
return null;
}
if (isTunnelMouth(column, mouthBlend)) {
channelRadius += mouthBlend;
}
double mouthFactor = tunnelMouthFactor(column, mouthBlend);
channelRadius += mouthBlend * mouthFactor;
if (column.river().distance() > channelRadius) {
return null;
}
@@ -255,13 +274,20 @@ public final class IrisRiverRuntime implements AutoCloseable {
);
int ceilingY = shapedTunnelCeilingY(
waterHeadY,
caves.getDryHeadroom() * column.reach().roofScaleAt(column.river().alongReach()),
caves.getDryHeadroom() * column.reach().roofScaleAt(column.river().alongReach())
+ mouthBlend * mouthFactor,
profile,
roofOffset
);
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(
int waterHeadY,
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) {
return false;
return 0D;
}
double length = column.reach().polyline().length();
if (length <= 0D) {
return false;
return 0D;
}
double offset = mouthBlend / length;
double alongReach = column.river().alongReach();
return !isCenterlineSubterranean(column.reach(), clamp01(alongReach - offset))
|| !isCenterlineSubterranean(column.reach(), clamp01(alongReach + offset));
return Math.max(
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) {
@@ -401,6 +453,35 @@ public final class IrisRiverRuntime implements AutoCloseable {
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(
int minimumX,
int minimumZ,
@@ -481,7 +562,9 @@ public final class IrisRiverRuntime implements AutoCloseable {
}
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) {
@@ -535,6 +618,52 @@ public final class IrisRiverRuntime implements AutoCloseable {
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) {
IrisRiverSurfaceSample surface = sample(x, z);
if (surface.river().present()
@@ -603,6 +732,7 @@ public final class IrisRiverRuntime implements AutoCloseable {
@Override
public void close() {
tileCache.close();
tunnelSampleCache.invalidateAll();
settingsCache.clear();
biomePoolCache.clear();
}
@@ -620,7 +750,7 @@ public final class IrisRiverRuntime implements AutoCloseable {
.routingDeviationScaleCells(topology.getRoutingDeviationScaleCells())
.routingDeviationStrengthCells(topology.getRoutingDeviationStrengthCells())
.routingPlateauHeight(topology.getRoutingPlateauHeight())
.hydraulicBaseHeight(fluidHeight)
.hydraulicBaseHeight(riverFluidHeight)
.requireOcean(topology.isRequireOcean())
.sourceChance(chance(topology.getSource()))
.reachChance(chance(topology.getContinuation()))
@@ -633,6 +763,7 @@ public final class IrisRiverRuntime implements AutoCloseable {
.channelWidth(mid(riverTerrain.getChannelWidth(), 12D))
.bankWidth(mid(riverTerrain.getBankWidth(), 8D))
.depth(mid(riverTerrain.getDepth(), 4D))
.channelRadiusBonus(riverTerrain.getChannelRadiusBonus())
.maxChannelWidth(riverTerrain.getMaxChannelWidth())
.maxBankWidth(riverTerrain.getMaxBankWidth())
.maxDepth(riverTerrain.getMaxDepth())
@@ -683,6 +814,7 @@ public final class IrisRiverRuntime implements AutoCloseable {
configured.getDepthMultiplier(),
configured.getBodyWavelength(),
configured.getBodyDetailWavelength(),
configured.getBodyDetailInfluence(),
configured.getWidthVariation(),
configured.getBankVariation(),
configured.getDepthVariation(),
@@ -757,6 +889,24 @@ public final class IrisRiverRuntime implements AutoCloseable {
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) {
if (!caveHydrologyActive || !reach.terminal() || reach.state() != RiverRouteState.WET) {
return null;
@@ -809,8 +959,11 @@ public final class IrisRiverRuntime implements AutoCloseable {
}
double waterSurface(RiverReach reach, double alongReach, boolean naturalOcean) {
if (reach == null || water.getMode() == IrisRiverWaterMode.SEA_LEVEL || naturalOcean) {
return fluidHeight;
if (naturalOcean) {
return dimensionFluidHeight;
}
if (reach == null || water.getMode() == IrisRiverWaterMode.FIXED) {
return riverFluidHeight;
}
return terracedWaterSurface(
reach.from().hydraulicHeight(),
@@ -883,10 +1036,10 @@ public final class IrisRiverRuntime implements AutoCloseable {
private int nodeWaterHead(double naturalNodeHeight, int dropHeight) {
int availableRise = Math.max(0, water.getMaximumPoolRise());
int maximumHead = fluidHeight + availableRise;
int maximumHead = riverFluidHeight + availableRise;
int naturalHead = (int) StrictMath.floor(naturalNodeHeight - 1D);
int clamped = Math.max(fluidHeight, Math.min(maximumHead, naturalHead));
return fluidHeight + Math.floorDiv(clamped - fluidHeight, dropHeight) * dropHeight;
int clamped = Math.max(riverFluidHeight, Math.min(maximumHead, naturalHead));
return riverFluidHeight + Math.floorDiv(clamped - riverFluidHeight, dropHeight) * dropHeight;
}
private static boolean isNaturalOcean(IrisBiome biome) {
@@ -1011,6 +1164,25 @@ public final class IrisRiverRuntime implements AutoCloseable {
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 IrisRiverTopology topology;
@@ -1022,11 +1194,11 @@ public final class IrisRiverRuntime implements AutoCloseable {
public RiverTerrainNodeSample sampleNode(int blockX, int blockZ) {
boolean oceanIntent = Boolean.TRUE.equals(naturalOcean.get(blockX, blockZ));
boolean naturalHeightRequired = topology.getTerrainHeightWeight() > 0D
|| water.getMode() != IrisRiverWaterMode.SEA_LEVEL
|| water.getMode() != IrisRiverWaterMode.FIXED
|| oceanIntent;
double sampledNaturalHeight = naturalHeightRequired
? naturalHeight.get(blockX, blockZ)
: fluidHeight;
: dimensionFluidHeight;
boolean sampledOcean = oceanIntent && isSubmergedOutlet(sampledNaturalHeight);
IrisRegion sampledRegion = region.get(blockX, blockZ);
IrisBiome sampledBiome = biomeRiverOverridesPossible ? naturalBiome.get(blockX, blockZ) : null;
@@ -1068,7 +1240,7 @@ public final class IrisRiverRuntime implements AutoCloseable {
}
private boolean isSubmergedOutlet(double sampledNaturalHeight) {
return Math.round(sampledNaturalHeight) < Math.round(fluidHeight);
return Math.round(sampledNaturalHeight) < Math.round(dimensionFluidHeight);
}
@Override
@@ -1150,8 +1322,8 @@ public final class IrisRiverRuntime implements AutoCloseable {
}
maximumIncision *= settings.maxIncisionMultiplier();
}
double head = configuration.getWater().getMode() == IrisRiverWaterMode.SEA_LEVEL
? fluidHeight
double head = configuration.getWater().getMode() == IrisRiverWaterMode.FIXED
? riverFluidHeight
: terracedWaterSurface(
context.from().hydraulicHeight(),
context.to().hydraulicHeight(),
@@ -12,7 +12,8 @@ public record IrisRiverRuntimeContext(
long seed,
IrisRiverNetwork configuration,
IrisData data,
int fluidHeight,
int riverFluidHeight,
int dimensionFluidHeight,
boolean boreMantleActive,
boolean caveHydrologyActive,
boolean blockingRoutingPossible,
@@ -52,6 +52,7 @@ import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.CreatureSpawnEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.Plugin;
import org.bukkit.util.Vector;
@@ -236,6 +237,11 @@ public final class BukkitPlatform implements IrisPlatform {
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() {
return io.papermc.lib.PaperLib.isPaper();
}
@@ -19,12 +19,55 @@ public class SlimJar {
private static final ReentrantLock lock = new ReentrantLock();
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() {
if (loaded.get()) return;
if (loaded.get()) {
return;
}
lock.lock();
try {
if (loaded.getAndSet(true)) return;
if (loaded.get()) {
return;
}
VolmitPlugin plugin = BukkitPlatform.volmitPlugin();
Path downloadPath = plugin.getDataFolder("cache", "libraries").toPath();
debug(plugin, "Loading libraries...");
@@ -58,6 +101,7 @@ public class SlimJar {
})
.build();
}
loaded.set(true);
debug(plugin, "Libraries loaded successfully!");
} finally {
lock.unlock();
@@ -69,4 +113,12 @@ public class SlimJar {
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;
}
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) {
if (runnable == null) {
return false;
@@ -1,6 +1,7 @@
package art.arcane.iris.util.project.matter.slices;
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.volmlib.util.data.palette.Palette;
import art.arcane.volmlib.util.matter.Sliced;
@@ -28,19 +29,21 @@ public final class RiverCaveHydrologyMatter extends RawMatter<RiverCaveHydrology
@Override
public void writeNode(RiverCaveHydrology hydrology, DataOutputStream output) throws IOException {
output.writeByte(actionCode(hydrology.action()));
output.writeByte(fluidKindCode(hydrology.fluidKind()));
output.writeUTF(hydrology.floodedBiomeKey());
}
@Override
public RiverCaveHydrology readNode(DataInputStream input) throws IOException {
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) {
return switch (action) {
case WET_SOURCE -> 1;
case FALLING_WATER -> 2;
case FALLING_FLUID -> 2;
case DRY_AIR -> 3;
case SEAL_GUARD -> 4;
};
@@ -49,10 +52,25 @@ public final class RiverCaveHydrologyMatter extends RawMatter<RiverCaveHydrology
private RiverCaveAction actionFromCode(int code) throws IOException {
return switch (code) {
case 1 -> RiverCaveAction.WET_SOURCE;
case 2 -> RiverCaveAction.FALLING_WATER;
case 2 -> RiverCaveAction.FALLING_FLUID;
case 3 -> RiverCaveAction.DRY_AIR;
case 4 -> RiverCaveAction.SEAL_GUARD;
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(
loaded,
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
@@ -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.TextValue;
import art.arcane.volmlib.util.localization.VolmitLocales;
import com.google.gson.JsonArray;
import org.junit.After;
import org.junit.Before;
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
public void bundledServerResourcesExactlyMatchNonEnglishManifest() throws Exception {
Set<String> expected = VolmitLocales.nonEnglish().stream()
@@ -651,6 +651,35 @@ public class PackDownloaderTest {
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
public void forceOverwriteReplacesEnginelessLoadedPackData() throws Exception {
// A registered loader with no engines is a stale catalog registration (startup
@@ -127,8 +127,9 @@ public class PackRiverValidatorTest {
{
"id": "trunk",
"seed": 17,
"bodyWavelength": 31,
"bodyWavelength": 7,
"bodyDetailWavelength": 16385,
"bodyDetailInfluence": 1.1,
"widthVariation": -0.1,
"bankVariation": 0.876,
"depthVariation": -0.1,
@@ -150,8 +151,9 @@ public class PackRiverValidatorTest {
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].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].bankVariation must be at most 0.875");
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");
}
@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
public void rejectsPathologicalCombinedTopologyComplexity() throws Exception {
File pack = pack("""
@@ -3,11 +3,17 @@ package art.arcane.iris.core.pregenerator.methods;
import org.junit.Test;
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 static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public class AsyncPregenMethodConcurrencyCapTest {
@Test
@@ -77,4 +83,31 @@ public class AsyncPregenMethodConcurrencyCapTest {
assertEquals(0, slowRequests.get());
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