mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-27 04:37:47 +00:00
Way too much
This commit is contained in:
@@ -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/
|
||||
|
||||
+29
-4
@@ -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);
|
||||
|
||||
+4
-1
@@ -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);
|
||||
}
|
||||
|
||||
+2
-1
@@ -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;
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -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 {
|
||||
|
||||
+32
-65
@@ -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:"));
|
||||
|
||||
+40
-131
@@ -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();
|
||||
|
||||
+285
-46
@@ -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();
|
||||
|
||||
+25
-3
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
-4
@@ -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;
|
||||
}
|
||||
|
||||
+3
-2
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+303
-110
@@ -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) {
|
||||
|
||||
+58
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+27
-5
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+27
@@ -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);
|
||||
}
|
||||
}
|
||||
+91
@@ -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);
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -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
|
||||
|
||||
+8
@@ -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;
|
||||
|
||||
+1
-1
@@ -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"));
|
||||
}
|
||||
|
||||
+89
@@ -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);
|
||||
}
|
||||
}
|
||||
+70
@@ -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
@@ -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();
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
+28
-12
@@ -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();
|
||||
|
||||
+27
-1
@@ -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];
|
||||
|
||||
+5
-2
@@ -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) {
|
||||
|
||||
+3
-3
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
+33
-12
@@ -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;
|
||||
|
||||
+474
-34
@@ -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
|
||||
}
|
||||
|
||||
+107
-1
@@ -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(),
|
||||
|
||||
+2
-1
@@ -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;
|
||||
|
||||
+21
-3
@@ -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
|
||||
|
||||
+106
@@ -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("""
|
||||
|
||||
+33
@@ -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
Reference in New Issue
Block a user