mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-30 05:50:48 +00:00
Way too much
This commit is contained in:
@@ -53,6 +53,8 @@ service-account*.json
|
|||||||
|
|
||||||
.qa/
|
.qa/
|
||||||
.repro/
|
.repro/
|
||||||
|
.perf/
|
||||||
|
.release-gate/
|
||||||
|
|
||||||
# Throwaway worktree copies used by the API / PlaceholderAPI rebuild lanes. Generated, never source.
|
# Throwaway worktree copies used by the API / PlaceholderAPI rebuild lanes. Generated, never source.
|
||||||
.apiwt/
|
.apiwt/
|
||||||
|
|||||||
+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.IrisBiome;
|
||||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
|
import art.arcane.iris.engine.object.IrisDimensionCarvingResolver;
|
||||||
import art.arcane.iris.util.project.context.IrisContext;
|
import art.arcane.iris.util.project.context.IrisContext;
|
||||||
import art.arcane.volmlib.util.collection.KMap;
|
import art.arcane.volmlib.util.collection.KMap;
|
||||||
import art.arcane.volmlib.util.math.RNG;
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
@@ -521,6 +522,16 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
int y,
|
int y,
|
||||||
int z,
|
int z,
|
||||||
Climate.Sampler sampler
|
Climate.Sampler sampler
|
||||||
|
) {
|
||||||
|
return getVisibleNoiseBiomeWithActiveGenerationLease(x, y, z, sampler, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
Holder<Biome> getVisibleNoiseBiomeWithActiveGenerationLease(
|
||||||
|
int x,
|
||||||
|
int y,
|
||||||
|
int z,
|
||||||
|
Climate.Sampler sampler,
|
||||||
|
IrisDimensionCarvingResolver.State resolverState
|
||||||
) {
|
) {
|
||||||
long cacheKey = packNoiseKey(x, y, z);
|
long cacheKey = packNoiseKey(x, y, z);
|
||||||
Holder<Biome> cachedHolder = noiseBiomeCache.get(cacheKey);
|
Holder<Biome> cachedHolder = noiseBiomeCache.get(cacheKey);
|
||||||
@@ -528,7 +539,7 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
return cachedHolder;
|
return cachedHolder;
|
||||||
}
|
}
|
||||||
|
|
||||||
Holder<Biome> resolvedHolder = resolveVisibleBiomeHolder(x, y, z);
|
Holder<Biome> resolvedHolder = resolveVisibleBiomeHolder(x, y, z, resolverState);
|
||||||
Holder<Biome> existingHolder = noiseBiomeCache.putIfAbsent(cacheKey, resolvedHolder);
|
Holder<Biome> existingHolder = noiseBiomeCache.putIfAbsent(cacheKey, resolvedHolder);
|
||||||
if (existingHolder != null) {
|
if (existingHolder != null) {
|
||||||
return existingHolder;
|
return existingHolder;
|
||||||
@@ -602,8 +613,13 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
return holder;
|
return holder;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Holder<Biome> resolveVisibleBiomeHolder(int x, int y, int z) {
|
private Holder<Biome> resolveVisibleBiomeHolder(
|
||||||
BiomeResolution resolution = resolveBiomeResolution(x, y, z);
|
int x,
|
||||||
|
int y,
|
||||||
|
int z,
|
||||||
|
IrisDimensionCarvingResolver.State resolverState
|
||||||
|
) {
|
||||||
|
BiomeResolution resolution = resolveBiomeResolution(x, y, z, resolverState);
|
||||||
if (resolution == null) {
|
if (resolution == null) {
|
||||||
return getFallbackBiome();
|
return getFallbackBiome();
|
||||||
}
|
}
|
||||||
@@ -636,6 +652,15 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private BiomeResolution resolveBiomeResolution(int x, int y, int z) {
|
private BiomeResolution resolveBiomeResolution(int x, int y, int z) {
|
||||||
|
return resolveBiomeResolution(x, y, z, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BiomeResolution resolveBiomeResolution(
|
||||||
|
int x,
|
||||||
|
int y,
|
||||||
|
int z,
|
||||||
|
IrisDimensionCarvingResolver.State resolverState
|
||||||
|
) {
|
||||||
if (engine == null || engine.isClosed()) {
|
if (engine == null || engine.isClosed()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -657,7 +682,7 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
int surfaceInternalY = engine.getComplex().getHeightStream().get(blockX, blockZ).intValue();
|
int surfaceInternalY = engine.getComplex().getHeightStream().get(blockX, blockZ).intValue();
|
||||||
underground = internalY <= surfaceInternalY - 8;
|
underground = internalY <= surfaceInternalY - 8;
|
||||||
irisBiome = underground
|
irisBiome = underground
|
||||||
? engine.getCaveBiome(blockX, internalY, blockZ)
|
? engine.getCaveBiome(blockX, internalY, blockZ, resolverState)
|
||||||
: engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
: engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
||||||
} else {
|
} else {
|
||||||
irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
||||||
|
|||||||
+4
-1
@@ -10,6 +10,7 @@ import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
|||||||
import art.arcane.iris.engine.IrisEngine;
|
import art.arcane.iris.engine.IrisEngine;
|
||||||
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
|
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
|
import art.arcane.iris.engine.object.IrisDimensionCarvingResolver;
|
||||||
import art.arcane.iris.engine.object.IrisMaterialPalette;
|
import art.arcane.iris.engine.object.IrisMaterialPalette;
|
||||||
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
||||||
import art.arcane.iris.nativegen.NativeStructureGenerationException;
|
import art.arcane.iris.nativegen.NativeStructureGenerationException;
|
||||||
@@ -643,8 +644,10 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_create_biomes");
|
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_create_biomes");
|
||||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
customBiomeSource.prepareVisibleBiomeBatch();
|
customBiomeSource.prepareVisibleBiomeBatch();
|
||||||
|
IrisDimensionCarvingResolver.State resolverState = new IrisDimensionCarvingResolver.State();
|
||||||
ichunkaccess.fillBiomesFromNoise(
|
ichunkaccess.fillBiomesFromNoise(
|
||||||
customBiomeSource::getVisibleNoiseBiomeWithActiveGenerationLease,
|
(x, y, z, sampler) -> customBiomeSource.getVisibleNoiseBiomeWithActiveGenerationLease(
|
||||||
|
x, y, z, sampler, resolverState),
|
||||||
randomstate.sampler());
|
randomstate.sampler());
|
||||||
return CompletableFuture.completedFuture(ichunkaccess);
|
return CompletableFuture.completedFuture(ichunkaccess);
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -258,7 +258,8 @@ public class IrisChunkGeneratorFailureContractTest {
|
|||||||
|
|
||||||
assertTrue(createBiomes.contains("requireGenerationStage(\"bukkit_nms_create_biomes\")"));
|
assertTrue(createBiomes.contains("requireGenerationStage(\"bukkit_nms_create_biomes\")"));
|
||||||
assertTrue(createBiomes.contains("customBiomeSource.prepareVisibleBiomeBatch()"));
|
assertTrue(createBiomes.contains("customBiomeSource.prepareVisibleBiomeBatch()"));
|
||||||
assertTrue(createBiomes.contains("customBiomeSource::getVisibleNoiseBiomeWithActiveGenerationLease"));
|
assertTrue(createBiomes.contains("new IrisDimensionCarvingResolver.State()"));
|
||||||
|
assertTrue(createBiomes.contains("sampler, resolverState"));
|
||||||
assertTrue(buildSurface.contains("delegate.buildSurface("));
|
assertTrue(buildSurface.contains("delegate.buildSurface("));
|
||||||
assertTrue(carvers.contains("delegate.applyCarvers("));
|
assertTrue(carvers.contains("delegate.applyCarvers("));
|
||||||
assertTrue(noise.contains("requireNoiseGenerationStage("));
|
assertTrue(noise.contains("requireNoiseGenerationStage("));
|
||||||
|
|||||||
@@ -573,7 +573,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static String logPrefix(Iris plugin) {
|
private static String logPrefix(Iris plugin) {
|
||||||
return plugin == null ? "[Iris] " : plugin.getTag();
|
return plugin == null ? ComponentLog.discriminator("Iris", "&a") : plugin.getTag();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import art.arcane.iris.core.lifecycle.WorldReplacementBootstrap;
|
|||||||
import art.arcane.iris.core.lifecycle.WorldReplacementBootstrapMarker;
|
import art.arcane.iris.core.lifecycle.WorldReplacementBootstrapMarker;
|
||||||
import art.arcane.iris.core.pack.DefaultPackBootstrapProvisioner;
|
import art.arcane.iris.core.pack.DefaultPackBootstrapProvisioner;
|
||||||
import art.arcane.iris.core.pack.DefaultPackBootstrapProvisioner.ProvisionResult;
|
import art.arcane.iris.core.pack.DefaultPackBootstrapProvisioner.ProvisionResult;
|
||||||
|
import art.arcane.iris.util.common.misc.SlimJar;
|
||||||
import io.papermc.paper.plugin.bootstrap.BootstrapContext;
|
import io.papermc.paper.plugin.bootstrap.BootstrapContext;
|
||||||
import io.papermc.paper.plugin.bootstrap.PluginBootstrap;
|
import io.papermc.paper.plugin.bootstrap.PluginBootstrap;
|
||||||
import io.papermc.paper.plugin.lifecycle.event.LifecycleEvent;
|
import io.papermc.paper.plugin.lifecycle.event.LifecycleEvent;
|
||||||
@@ -26,6 +27,7 @@ public final class IrisBootstrap implements PluginBootstrap {
|
|||||||
public void bootstrap(BootstrapContext context) {
|
public void bootstrap(BootstrapContext context) {
|
||||||
WorldReplacementBootstrapMarker.markBootstrapped();
|
WorldReplacementBootstrapMarker.markBootstrapped();
|
||||||
try {
|
try {
|
||||||
|
loadRuntimeLibraries(context);
|
||||||
BukkitStartupPaths startupPaths = BukkitStartupPaths.resolveCurrent();
|
BukkitStartupPaths startupPaths = BukkitStartupPaths.resolveCurrent();
|
||||||
reconcilePendingWorldReplacements(context, startupPaths);
|
reconcilePendingWorldReplacements(context, startupPaths);
|
||||||
quarantineWorthlessHusks(startupPaths, message -> context.getLogger().warn(message));
|
quarantineWorthlessHusks(startupPaths, message -> context.getLogger().warn(message));
|
||||||
@@ -38,6 +40,27 @@ public final class IrisBootstrap implements PluginBootstrap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void loadRuntimeLibraries(BootstrapContext context) {
|
||||||
|
SlimJar.loadBootstrap(
|
||||||
|
context.getDataDirectory().resolve("cache").resolve("libraries"),
|
||||||
|
new SlimJar.BootstrapLogger() {
|
||||||
|
@Override
|
||||||
|
public void info(String message) {
|
||||||
|
context.getLogger().info(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void error(String message) {
|
||||||
|
context.getLogger().error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void debug(String message) {
|
||||||
|
context.getLogger().debug(message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private static void reconcilePendingWorldReplacements(
|
private static void reconcilePendingWorldReplacements(
|
||||||
BootstrapContext context,
|
BootstrapContext context,
|
||||||
BukkitStartupPaths startupPaths
|
BukkitStartupPaths startupPaths
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package art.arcane.iris;
|
||||||
|
|
||||||
|
import art.arcane.iris.util.common.misc.SlimJar;
|
||||||
|
import io.papermc.paper.plugin.bootstrap.PluginProviderContext;
|
||||||
|
import io.papermc.paper.plugin.loader.PluginClasspathBuilder;
|
||||||
|
import io.papermc.paper.plugin.loader.PluginLoader;
|
||||||
|
import io.papermc.paper.plugin.loader.library.impl.JarLibrary;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
@SuppressWarnings("UnstableApiUsage")
|
||||||
|
public final class IrisPluginLoader implements PluginLoader {
|
||||||
|
@Override
|
||||||
|
public void classloader(PluginClasspathBuilder classpathBuilder) {
|
||||||
|
PluginProviderContext context = classpathBuilder.getContext();
|
||||||
|
Path libraryRoot = context.getDataDirectory().resolve("cache").resolve("libraries");
|
||||||
|
SlimJar.loadBootstrap(libraryRoot, new SlimJar.BootstrapLogger() {
|
||||||
|
@Override
|
||||||
|
public void info(String message) {
|
||||||
|
context.getLogger().info(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void error(String message) {
|
||||||
|
context.getLogger().error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void debug(String message) {
|
||||||
|
context.getLogger().debug(message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
for (Path library : relocatedLibraries(libraryRoot)) {
|
||||||
|
classpathBuilder.addLibrary(new JarLibrary(library));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<Path> relocatedLibraries(Path libraryRoot) {
|
||||||
|
try (Stream<Path> paths = Files.walk(libraryRoot)) {
|
||||||
|
List<Path> libraries = paths
|
||||||
|
.filter(Files::isRegularFile)
|
||||||
|
.filter(path -> path.getFileName().toString().endsWith(".jar"))
|
||||||
|
.filter(IrisPluginLoader::isRelocatedLibrary)
|
||||||
|
.sorted()
|
||||||
|
.toList();
|
||||||
|
if (libraries.isEmpty()) {
|
||||||
|
throw new IllegalStateException("Iris runtime library provisioning produced no relocated libraries.");
|
||||||
|
}
|
||||||
|
return libraries;
|
||||||
|
} catch (IOException failure) {
|
||||||
|
throw new IllegalStateException("Unable to enumerate provisioned Iris runtime libraries.", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isRelocatedLibrary(Path path) {
|
||||||
|
int count = path.getNameCount();
|
||||||
|
for (int i = 0; i < count - 2; i++) {
|
||||||
|
if ("relocated".equals(path.getName(i).toString())
|
||||||
|
&& "Iris".equals(path.getName(i + 1).toString())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-1
@@ -96,6 +96,7 @@ import java.time.temporal.ChronoUnit;
|
|||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.UUID;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
@@ -412,7 +413,8 @@ public class CommandStudio implements DirectorExecutor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
VisionGUI.launch(IrisToolbelt.access(world).getEngine());
|
UUID openerId = sender().isPlayer() ? player().getUniqueId() : null;
|
||||||
|
VisionGUI.launch(IrisToolbelt.access(world).getEngine(), openerId);
|
||||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_MAP));
|
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_MAP));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import org.bukkit.event.Listener;
|
|||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
public final class BukkitGuiHost implements GuiHost.Provider {
|
public final class BukkitGuiHost implements GuiHost.Provider {
|
||||||
@@ -72,8 +73,8 @@ public final class BukkitGuiHost implements GuiHost.Provider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public GuiOverlay overlayFor(Engine engine) {
|
public GuiOverlay overlayFor(Engine engine, UUID openerId) {
|
||||||
return engine == null ? null : new BukkitVisionOverlay(engine);
|
return engine == null ? null : new BukkitVisionOverlay(engine, openerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class HotloadListener implements Listener {
|
private static final class HotloadListener implements Listener {
|
||||||
|
|||||||
+28
-61
@@ -18,7 +18,6 @@
|
|||||||
|
|
||||||
package art.arcane.iris.core.gui;
|
package art.arcane.iris.core.gui;
|
||||||
|
|
||||||
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
|
|
||||||
import art.arcane.iris.engine.IrisComplex;
|
import art.arcane.iris.engine.IrisComplex;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
import art.arcane.iris.engine.framework.render.RenderType;
|
import art.arcane.iris.engine.framework.render.RenderType;
|
||||||
@@ -28,7 +27,6 @@ import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
|||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
import art.arcane.iris.util.common.scheduling.J;
|
import art.arcane.iris.util.common.scheduling.J;
|
||||||
import art.arcane.volmlib.util.format.Form;
|
import art.arcane.volmlib.util.format.Form;
|
||||||
import org.bukkit.Chunk;
|
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.World;
|
import org.bukkit.World;
|
||||||
import org.bukkit.entity.LivingEntity;
|
import org.bukkit.entity.LivingEntity;
|
||||||
@@ -39,6 +37,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.UUID;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
@@ -50,14 +49,16 @@ import static art.arcane.iris.util.common.data.registry.Attributes.MAX_HEALTH;
|
|||||||
|
|
||||||
public final class BukkitVisionOverlay implements GuiOverlay {
|
public final class BukkitVisionOverlay implements GuiOverlay {
|
||||||
private final Engine engine;
|
private final Engine engine;
|
||||||
|
private final UUID openerId;
|
||||||
private final AtomicBoolean nativeTeleportActive = new AtomicBoolean();
|
private final AtomicBoolean nativeTeleportActive = new AtomicBoolean();
|
||||||
private final AtomicBoolean playerRefreshQueued = new AtomicBoolean();
|
private final AtomicBoolean playerRefreshQueued = new AtomicBoolean();
|
||||||
private final AtomicLong teleportSequence = new AtomicLong();
|
private final AtomicLong teleportSequence = new AtomicLong();
|
||||||
private final AtomicReference<VisionTeleportRequest> latestTeleport = new AtomicReference<>();
|
private final AtomicReference<VisionTeleportRequest> latestTeleport = new AtomicReference<>();
|
||||||
private volatile List<GuiMarker> playerMarkers = List.of();
|
private volatile List<GuiMarker> playerMarkers = List.of();
|
||||||
|
|
||||||
public BukkitVisionOverlay(Engine engine) {
|
public BukkitVisionOverlay(Engine engine, UUID openerId) {
|
||||||
this.engine = engine;
|
this.engine = engine;
|
||||||
|
this.openerId = openerId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -177,67 +178,20 @@ public final class BukkitVisionOverlay implements GuiOverlay {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
List<Player> players = BukkitWorldBinding.players(target);
|
List<Player> players = BukkitWorldBinding.players(target);
|
||||||
if (players.isEmpty()) {
|
Player player = selectPlayer(players);
|
||||||
|
if (player == null) {
|
||||||
finish(request);
|
finish(request);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Player player = players.get(0);
|
|
||||||
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 blockX = request.blockX;
|
||||||
int blockZ = request.blockZ;
|
int blockZ = request.blockZ;
|
||||||
int chunkX = blockX >> 4;
|
|
||||||
int chunkZ = blockZ >> 4;
|
|
||||||
CompletableFuture<Chunk> requested;
|
|
||||||
try {
|
try {
|
||||||
requested = WorldRuntimeControlService.get().requestChunkAsync(
|
int blockY = engine.getMinHeight() + engine.getHeight(blockX, blockZ, false) + 2;
|
||||||
|
Location destination = new Location(
|
||||||
world,
|
world,
|
||||||
chunkX,
|
blockX + 0.5D,
|
||||||
chunkZ,
|
blockY,
|
||||||
true,
|
blockZ + 0.5D);
|
||||||
true
|
|
||||||
);
|
|
||||||
} catch (Throwable failure) {
|
|
||||||
fail(request, target, world, failure);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (requested == null) {
|
|
||||||
fail(request, target, world, new IllegalStateException(
|
|
||||||
"Vision destination chunk request returned no future."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
requested.whenComplete((chunk, failure) -> {
|
|
||||||
if (!isCurrent(request, target)) {
|
|
||||||
finish(request);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (failure != null) {
|
|
||||||
fail(request, target, world, failure);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (chunk == null || chunk.getWorld() != world) {
|
|
||||||
fail(request, target, world, new IllegalStateException(
|
|
||||||
"Vision destination chunk request returned no chunk."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
boolean scheduled = J.runRegion(world, chunkX, chunkZ, () -> {
|
|
||||||
if (!isCurrent(request, target)) {
|
|
||||||
finish(request);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
int yy = world.getHighestBlockYAt(blockX, blockZ) + 1;
|
|
||||||
Location destination = new Location(world, blockX, yy, blockZ);
|
|
||||||
if (!J.runEntity(player, () -> delegateTeleport(
|
if (!J.runEntity(player, () -> delegateTeleport(
|
||||||
request,
|
request,
|
||||||
target,
|
target,
|
||||||
@@ -247,12 +201,25 @@ public final class BukkitVisionOverlay implements GuiOverlay {
|
|||||||
fail(request, target, world, new IllegalStateException(
|
fail(request, target, world, new IllegalStateException(
|
||||||
"Failed to schedule the Vision teleport on the player entity."));
|
"Failed to schedule the Vision teleport on the player entity."));
|
||||||
}
|
}
|
||||||
});
|
} catch (Throwable failure) {
|
||||||
if (!scheduled) {
|
fail(request, target, world, failure);
|
||||||
fail(request, target, world, new IllegalStateException(
|
|
||||||
"Failed to schedule the Vision surface lookup on its owning region."));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if (!scheduled) {
|
||||||
|
finish(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Player selectPlayer(List<Player> players) {
|
||||||
|
if (openerId == null) {
|
||||||
|
return players.isEmpty() ? null : players.get(0);
|
||||||
|
}
|
||||||
|
for (Player player : players) {
|
||||||
|
if (openerId.equals(player.getUniqueId())) {
|
||||||
|
return player;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void delegateTeleport(
|
private void delegateTeleport(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ name: ${name}
|
|||||||
version: ${version}
|
version: ${version}
|
||||||
main: ${main}
|
main: ${main}
|
||||||
bootstrapper: ${bootstrapper}
|
bootstrapper: ${bootstrapper}
|
||||||
|
loader: art.arcane.iris.IrisPluginLoader
|
||||||
folia-supported: true
|
folia-supported: true
|
||||||
api-version: '${apiVersion}'
|
api-version: '${apiVersion}'
|
||||||
load: STARTUP
|
load: STARTUP
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package art.arcane.iris;
|
||||||
|
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
public class IrisPluginLoaderTest {
|
||||||
|
@Rule
|
||||||
|
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void exposesOnlyRelocatedRuntimeLibrariesInStableOrder() throws Exception {
|
||||||
|
Path root = temporaryFolder.newFolder("libraries").toPath();
|
||||||
|
Path original = Files.createDirectories(root.resolve("gson/2.14.0"))
|
||||||
|
.resolve("gson.jar");
|
||||||
|
Path relocatedGson = Files.createDirectories(root.resolve("gson/2.14.0/relocated/Iris"))
|
||||||
|
.resolve("gson.jar");
|
||||||
|
Path relocatedCaffeine = Files.createDirectories(root.resolve("caffeine/3.2.4/relocated/Iris"))
|
||||||
|
.resolve("caffeine.jar");
|
||||||
|
Files.createFile(original);
|
||||||
|
Files.createFile(relocatedGson);
|
||||||
|
Files.createFile(relocatedCaffeine);
|
||||||
|
|
||||||
|
List<Path> libraries = IrisPluginLoader.relocatedLibraries(root);
|
||||||
|
|
||||||
|
assertEquals(List.of(relocatedCaffeine, relocatedGson), libraries);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,6 +53,7 @@ public class PaperPluginMetadataTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
assertTrue(metadata.contains("bootstrapper: " + IrisBootstrap.class.getName()));
|
assertTrue(metadata.contains("bootstrapper: " + IrisBootstrap.class.getName()));
|
||||||
|
assertTrue(metadata.contains("loader: " + IrisPluginLoader.class.getName()));
|
||||||
assertTrue(metadata.contains("folia-supported: true"));
|
assertTrue(metadata.contains("folia-supported: true"));
|
||||||
assertTrue(metadata.contains("load: STARTUP"));
|
assertTrue(metadata.contains("load: STARTUP"));
|
||||||
assertFalse(metadata.contains("commands:"));
|
assertFalse(metadata.contains("commands:"));
|
||||||
|
|||||||
+40
-131
@@ -1,108 +1,67 @@
|
|||||||
package art.arcane.iris.core.gui;
|
package art.arcane.iris.core.gui;
|
||||||
|
|
||||||
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
|
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
import art.arcane.iris.engine.object.IrisWorld;
|
import art.arcane.iris.engine.object.IrisWorld;
|
||||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||||
import art.arcane.iris.util.common.scheduling.J;
|
import art.arcane.iris.util.common.scheduling.J;
|
||||||
import org.bukkit.Chunk;
|
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.World;
|
import org.bukkit.World;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.mockito.MockedStatic;
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertTrue;
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyInt;
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.ArgumentMatchers.same;
|
import static org.mockito.ArgumentMatchers.same;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.mockStatic;
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
public class BukkitVisionOverlayFoliaContractTest {
|
public class BukkitVisionOverlayFoliaContractTest {
|
||||||
@Test
|
@Test
|
||||||
public void teleportLoadsTheDestinationChunkBeforeItsOwningRegionReadsTheSurface() throws Exception {
|
public void teleportDelegatesImmediatelyToTheNativeAsyncPath() {
|
||||||
String source = Files.readString(Path.of(
|
|
||||||
"src/main/java/art/arcane/iris/core/gui/BukkitVisionOverlay.java"
|
|
||||||
)).replace("\r\n", "\n");
|
|
||||||
String request = method(source, "private void requestTeleportChunk(");
|
|
||||||
|
|
||||||
assertBefore(request, "requestChunkAsync(", "requested.whenComplete(");
|
|
||||||
assertBefore(request, "requested.whenComplete(", "J.runRegion(");
|
|
||||||
assertBefore(request, "J.runRegion(", "world.getHighestBlockYAt(");
|
|
||||||
assertBefore(request, "world.getHighestBlockYAt(", "J.runEntity(");
|
|
||||||
assertTrue(request.contains("chunkX,\n chunkZ,\n true,\n true"));
|
|
||||||
assertEquals(1, occurrences(request, "world.getHighestBlockYAt("));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void teleportReportsAsyncAndSchedulingFailuresWithDestinationContext() throws Exception {
|
|
||||||
String source = Files.readString(Path.of(
|
|
||||||
"src/main/java/art/arcane/iris/core/gui/BukkitVisionOverlay.java"
|
|
||||||
)).replace("\r\n", "\n");
|
|
||||||
String request = method(source, "private void requestTeleportChunk(");
|
|
||||||
String reporter = method(source, "private void reportTeleportFailure(");
|
|
||||||
|
|
||||||
assertTrue(request.contains("if (requested == null)"));
|
|
||||||
assertTrue(request.contains("if (failure != null)"));
|
|
||||||
assertTrue(request.contains("if (chunk == null ||"));
|
|
||||||
assertTrue(request.contains("if (!J.runEntity("));
|
|
||||||
assertTrue(request.contains("if (!scheduled)"));
|
|
||||||
assertTrue(reporter.contains("IrisLogging.reportError("));
|
|
||||||
assertTrue(reporter.contains("world.getName()"));
|
|
||||||
assertTrue(reporter.contains("blockX + \",\" + blockZ"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void teleportObservesNativeCompletionAndRejectsFalseSettlement() throws Exception {
|
|
||||||
String source = Files.readString(Path.of(
|
|
||||||
"src/main/java/art/arcane/iris/core/gui/BukkitVisionOverlay.java"
|
|
||||||
)).replace("\r\n", "\n");
|
|
||||||
String delegate = method(source, "private void delegateTeleport(");
|
|
||||||
|
|
||||||
assertTrue(delegate.contains("teleport.whenComplete("));
|
|
||||||
assertTrue(delegate.contains("!Boolean.TRUE.equals(success)"));
|
|
||||||
assertTrue(delegate.contains("restartLatest(request)"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void staleChunkCompletionCannotTeleportOverTheLatestRequest() {
|
|
||||||
VisionHarness harness = new VisionHarness();
|
VisionHarness harness = new VisionHarness();
|
||||||
CompletableFuture<Chunk> firstChunk = new CompletableFuture<>();
|
|
||||||
CompletableFuture<Chunk> secondChunk = new CompletableFuture<>();
|
|
||||||
harness.stubChunk(0, 0, firstChunk);
|
|
||||||
harness.stubChunk(2, 2, secondChunk);
|
|
||||||
|
|
||||||
try (harness) {
|
try (harness) {
|
||||||
harness.overlay.teleport(1.5D, 1.5D);
|
|
||||||
harness.overlay.teleport(33.5D, 33.5D);
|
harness.overlay.teleport(33.5D, 33.5D);
|
||||||
|
|
||||||
firstChunk.complete(harness.chunk);
|
assertEquals(1, harness.destinations.size());
|
||||||
assertEquals(0, harness.destinations.size());
|
Location destination = harness.destinations.get(0);
|
||||||
secondChunk.complete(harness.chunk);
|
assertEquals(33.5D, destination.getX(), 0D);
|
||||||
|
assertEquals(76D, destination.getY(), 0D);
|
||||||
|
assertEquals(33.5D, destination.getZ(), 0D);
|
||||||
|
verify(harness.engine).getHeight(33, 33, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void teleportFloorsNegativeCoordinatesBeforeCenteringTheDestination() {
|
||||||
|
VisionHarness harness = new VisionHarness();
|
||||||
|
|
||||||
|
try (harness) {
|
||||||
|
harness.overlay.teleport(-0.25D, -16.01D);
|
||||||
|
|
||||||
assertEquals(1, harness.destinations.size());
|
assertEquals(1, harness.destinations.size());
|
||||||
assertEquals(33, harness.destinations.get(0).getBlockX());
|
Location destination = harness.destinations.get(0);
|
||||||
assertEquals(33, harness.destinations.get(0).getBlockZ());
|
assertEquals(-0.5D, destination.getX(), 0D);
|
||||||
|
assertEquals(-16.5D, destination.getZ(), 0D);
|
||||||
|
verify(harness.engine).getHeight(-1, -17, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void latestRequestRunsAfterAnOlderNativeTeleportSettles() {
|
public void latestRequestRunsAfterAnOlderNativeTeleportSettles() {
|
||||||
VisionHarness harness = new VisionHarness();
|
VisionHarness harness = new VisionHarness();
|
||||||
harness.stubChunk(0, 0, CompletableFuture.completedFuture(harness.chunk));
|
|
||||||
harness.stubChunk(2, 2, CompletableFuture.completedFuture(harness.chunk));
|
|
||||||
CompletableFuture<Boolean> firstTeleport = new CompletableFuture<>();
|
CompletableFuture<Boolean> firstTeleport = new CompletableFuture<>();
|
||||||
CompletableFuture<Boolean> secondTeleport = new CompletableFuture<>();
|
CompletableFuture<Boolean> secondTeleport = new CompletableFuture<>();
|
||||||
harness.nativeTeleports.add(firstTeleport);
|
harness.nativeTeleports.add(firstTeleport);
|
||||||
@@ -121,54 +80,28 @@ public class BukkitVisionOverlayFoliaContractTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void assertBefore(String source, String first, String second) {
|
@Test
|
||||||
int firstIndex = source.indexOf(first);
|
public void missingOpenerDoesNotTeleportAnotherPlayer() {
|
||||||
int secondIndex = source.indexOf(second);
|
VisionHarness harness = new VisionHarness();
|
||||||
assertTrue("Missing source contract token: " + first, firstIndex >= 0);
|
harness.binding.when(() -> BukkitWorldBinding.players(harness.target))
|
||||||
assertTrue("Missing source contract token: " + second, secondIndex >= 0);
|
.thenReturn(List.of(harness.otherPlayer));
|
||||||
assertTrue(first + " must occur before " + second, firstIndex < secondIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int occurrences(String source, String match) {
|
try (harness) {
|
||||||
int count = 0;
|
harness.overlay.teleport(1.5D, 1.5D);
|
||||||
int offset = 0;
|
|
||||||
while ((offset = source.indexOf(match, offset)) >= 0) {
|
|
||||||
count++;
|
|
||||||
offset += match.length();
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String method(String source, String signature) {
|
assertEquals(0, harness.destinations.size());
|
||||||
int start = source.indexOf(signature);
|
|
||||||
assertTrue("Missing source contract signature: " + signature, start >= 0);
|
|
||||||
int openBrace = source.indexOf('{', start);
|
|
||||||
assertTrue("Missing source contract method body: " + signature, openBrace >= 0);
|
|
||||||
int depth = 0;
|
|
||||||
for (int index = openBrace; index < source.length(); index++) {
|
|
||||||
char current = source.charAt(index);
|
|
||||||
if (current == '{') {
|
|
||||||
depth++;
|
|
||||||
} else if (current == '}') {
|
|
||||||
depth--;
|
|
||||||
if (depth == 0) {
|
|
||||||
return source.substring(start, index + 1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
throw new IllegalArgumentException("Unclosed source contract method: " + signature);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class VisionHarness implements AutoCloseable {
|
private static final class VisionHarness implements AutoCloseable {
|
||||||
private final Engine engine;
|
private final Engine engine;
|
||||||
private final IrisWorld target;
|
private final IrisWorld target;
|
||||||
private final World world;
|
private final World world;
|
||||||
private final Player player;
|
private final Player player;
|
||||||
private final Chunk chunk;
|
private final Player otherPlayer;
|
||||||
private final WorldRuntimeControlService runtime;
|
private final UUID openerId;
|
||||||
private final MockedStatic<J> scheduling;
|
private final MockedStatic<J> scheduling;
|
||||||
private final MockedStatic<BukkitWorldBinding> binding;
|
private final MockedStatic<BukkitWorldBinding> binding;
|
||||||
private final MockedStatic<WorldRuntimeControlService> runtimeAccess;
|
|
||||||
private final MockedStatic<BukkitPlatform> platform;
|
private final MockedStatic<BukkitPlatform> platform;
|
||||||
private final List<Location> destinations;
|
private final List<Location> destinations;
|
||||||
private final List<CompletableFuture<Boolean>> nativeTeleports;
|
private final List<CompletableFuture<Boolean>> nativeTeleports;
|
||||||
@@ -180,33 +113,26 @@ public class BukkitVisionOverlayFoliaContractTest {
|
|||||||
target = mock(IrisWorld.class);
|
target = mock(IrisWorld.class);
|
||||||
world = mock(World.class);
|
world = mock(World.class);
|
||||||
player = mock(Player.class);
|
player = mock(Player.class);
|
||||||
chunk = mock(Chunk.class);
|
otherPlayer = mock(Player.class);
|
||||||
runtime = mock(WorldRuntimeControlService.class);
|
openerId = UUID.randomUUID();
|
||||||
destinations = new ArrayList<>();
|
destinations = new ArrayList<>();
|
||||||
nativeTeleports = new ArrayList<>();
|
nativeTeleports = new ArrayList<>();
|
||||||
nativeTeleportIndex = new AtomicInteger();
|
nativeTeleportIndex = new AtomicInteger();
|
||||||
|
|
||||||
when(engine.getWorld()).thenReturn(target);
|
when(engine.getWorld()).thenReturn(target);
|
||||||
|
when(engine.getMinHeight()).thenReturn(-64);
|
||||||
|
when(engine.getHeight(anyInt(), anyInt(), eq(false))).thenReturn(138);
|
||||||
when(target.hasPlatformWorld()).thenReturn(true);
|
when(target.hasPlatformWorld()).thenReturn(true);
|
||||||
when(player.isOnline()).thenReturn(true);
|
when(player.isOnline()).thenReturn(true);
|
||||||
when(player.getWorld()).thenReturn(world);
|
when(player.getWorld()).thenReturn(world);
|
||||||
when(chunk.getWorld()).thenReturn(world);
|
when(player.getUniqueId()).thenReturn(openerId);
|
||||||
when(world.getHighestBlockYAt(anyInt(), anyInt())).thenReturn(70);
|
when(otherPlayer.getUniqueId()).thenReturn(UUID.randomUUID());
|
||||||
|
|
||||||
scheduling = mockStatic(J.class);
|
scheduling = mockStatic(J.class);
|
||||||
scheduling.when(() -> J.runGlobal(any(Runnable.class))).thenAnswer(invocation -> {
|
scheduling.when(() -> J.runGlobal(any(Runnable.class))).thenAnswer(invocation -> {
|
||||||
invocation.getArgument(0, Runnable.class).run();
|
invocation.getArgument(0, Runnable.class).run();
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
scheduling.when(() -> J.runRegion(
|
|
||||||
same(world),
|
|
||||||
anyInt(),
|
|
||||||
anyInt(),
|
|
||||||
any(Runnable.class)))
|
|
||||||
.thenAnswer(invocation -> {
|
|
||||||
invocation.getArgument(3, Runnable.class).run();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
scheduling.when(() -> J.runEntity(same(player), any(Runnable.class))).thenAnswer(invocation -> {
|
scheduling.when(() -> J.runEntity(same(player), any(Runnable.class))).thenAnswer(invocation -> {
|
||||||
invocation.getArgument(1, Runnable.class).run();
|
invocation.getArgument(1, Runnable.class).run();
|
||||||
return true;
|
return true;
|
||||||
@@ -214,10 +140,7 @@ public class BukkitVisionOverlayFoliaContractTest {
|
|||||||
|
|
||||||
binding = mockStatic(BukkitWorldBinding.class);
|
binding = mockStatic(BukkitWorldBinding.class);
|
||||||
binding.when(() -> BukkitWorldBinding.world(target)).thenReturn(world);
|
binding.when(() -> BukkitWorldBinding.world(target)).thenReturn(world);
|
||||||
binding.when(() -> BukkitWorldBinding.players(target)).thenReturn(List.of(player));
|
binding.when(() -> BukkitWorldBinding.players(target)).thenReturn(List.of(otherPlayer, player));
|
||||||
|
|
||||||
runtimeAccess = mockStatic(WorldRuntimeControlService.class);
|
|
||||||
runtimeAccess.when(WorldRuntimeControlService::get).thenReturn(runtime);
|
|
||||||
|
|
||||||
platform = mockStatic(BukkitPlatform.class);
|
platform = mockStatic(BukkitPlatform.class);
|
||||||
platform.when(() -> BukkitPlatform.teleportAsync(same(player), any(Location.class)))
|
platform.when(() -> BukkitPlatform.teleportAsync(same(player), any(Location.class)))
|
||||||
@@ -228,26 +151,12 @@ public class BukkitVisionOverlayFoliaContractTest {
|
|||||||
? nativeTeleports.get(index)
|
? nativeTeleports.get(index)
|
||||||
: CompletableFuture.completedFuture(true);
|
: CompletableFuture.completedFuture(true);
|
||||||
});
|
});
|
||||||
overlay = new BukkitVisionOverlay(engine);
|
overlay = new BukkitVisionOverlay(engine, openerId);
|
||||||
}
|
|
||||||
|
|
||||||
private void stubChunk(
|
|
||||||
int chunkX,
|
|
||||||
int chunkZ,
|
|
||||||
CompletableFuture<Chunk> requested
|
|
||||||
) {
|
|
||||||
when(runtime.requestChunkAsync(
|
|
||||||
same(world),
|
|
||||||
eq(chunkX),
|
|
||||||
eq(chunkZ),
|
|
||||||
eq(true),
|
|
||||||
eq(true))).thenReturn(requested);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
platform.close();
|
platform.close();
|
||||||
runtimeAccess.close();
|
|
||||||
binding.close();
|
binding.close();
|
||||||
scheduling.close();
|
scheduling.close();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ package art.arcane.iris.fabric;
|
|||||||
import art.arcane.iris.modded.ModdedLoader;
|
import art.arcane.iris.modded.ModdedLoader;
|
||||||
import art.arcane.iris.modded.service.ModdedTreeFellerService;
|
import art.arcane.iris.modded.service.ModdedTreeFellerService;
|
||||||
import net.fabricmc.api.EnvType;
|
import net.fabricmc.api.EnvType;
|
||||||
|
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLevelEvents;
|
||||||
import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents;
|
import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents;
|
||||||
import net.fabricmc.fabric.api.permission.v1.PermissionContextOwner;
|
import net.fabricmc.fabric.api.permission.v1.PermissionContextOwner;
|
||||||
import net.fabricmc.loader.api.FabricLoader;
|
import net.fabricmc.loader.api.FabricLoader;
|
||||||
@@ -71,6 +72,16 @@ public final class FabricModdedLoader implements ModdedLoader {
|
|||||||
// map). Off-thread readers rely on the ModdedServerLevels snapshot, which the caller republishes.
|
// map). Off-thread readers rely on the ModdedServerLevels snapshot, which the caller republishes.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void fireDynamicLevelLoad(MinecraftServer server, ServerLevel level) {
|
||||||
|
ServerLevelEvents.LOAD.invoker().onLevelLoad(server, level);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void fireDynamicLevelUnload(MinecraftServer server, ServerLevel level) {
|
||||||
|
ServerLevelEvents.UNLOAD.invoker().onLevelUnload(server, level);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean clientEnvironment() {
|
public boolean clientEnvironment() {
|
||||||
return FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT;
|
return FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT;
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import net.minecraft.server.level.ServerPlayer;
|
|||||||
import net.minecraft.world.level.block.state.BlockState;
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
import net.minecraftforge.common.util.Result;
|
import net.minecraftforge.common.util.Result;
|
||||||
import net.minecraftforge.event.level.BlockEvent;
|
import net.minecraftforge.event.level.BlockEvent;
|
||||||
|
import net.minecraftforge.event.level.LevelEvent;
|
||||||
import net.minecraftforge.fml.ModList;
|
import net.minecraftforge.fml.ModList;
|
||||||
import net.minecraftforge.fml.loading.FMLEnvironment;
|
import net.minecraftforge.fml.loading.FMLEnvironment;
|
||||||
import net.minecraftforge.fml.loading.FMLLoader;
|
import net.minecraftforge.fml.loading.FMLLoader;
|
||||||
@@ -77,6 +78,16 @@ public final class ForgeModdedLoader implements ModdedLoader {
|
|||||||
server.markWorldsDirty();
|
server.markWorldsDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void fireDynamicLevelLoad(MinecraftServer server, ServerLevel level) {
|
||||||
|
LevelEvent.Load.BUS.post(new LevelEvent.Load(level));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void fireDynamicLevelUnload(MinecraftServer server, ServerLevel level) {
|
||||||
|
LevelEvent.Unload.BUS.post(new LevelEvent.Unload(level));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean clientEnvironment() {
|
public boolean clientEnvironment() {
|
||||||
return FMLEnvironment.dist.isClient();
|
return FMLEnvironment.dist.isClient();
|
||||||
|
|||||||
+283
-44
@@ -56,13 +56,16 @@ import java.util.concurrent.CompletableFuture;
|
|||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
public final class ModdedDimensionManager {
|
public final class ModdedDimensionManager {
|
||||||
|
private static final int TELEPORT_WARM_RADIUS = 0;
|
||||||
private static final Object LOCK = new Object();
|
private static final Object LOCK = new Object();
|
||||||
private static final ConcurrentHashMap<String, Handle> HANDLES = new ConcurrentHashMap<>();
|
private static final ConcurrentHashMap<String, Handle> HANDLES = new ConcurrentHashMap<>();
|
||||||
private static final TicketType TELEPORT_WARM_TICKET = new TicketType(TicketType.NO_TIMEOUT,
|
private static final TicketType TELEPORT_WARM_TICKET = new TicketType(TicketType.NO_TIMEOUT,
|
||||||
TicketType.FLAG_LOADING | TicketType.FLAG_KEEP_DIMENSION_ACTIVE);
|
TicketType.FLAG_LOADING | TicketType.FLAG_KEEP_DIMENSION_ACTIVE);
|
||||||
private static final long TELEPORT_WARM_TIMEOUT_SECONDS = 30L;
|
private static final long TELEPORT_TIMEOUT_SECONDS = 10L;
|
||||||
private static volatile ModdedServerAccess access;
|
private static volatile ModdedServerAccess access;
|
||||||
|
|
||||||
private ModdedDimensionManager() {
|
private ModdedDimensionManager() {
|
||||||
@@ -213,15 +216,16 @@ public final class ModdedDimensionManager {
|
|||||||
instanceof IrisModdedChunkGenerator irisGenerator
|
instanceof IrisModdedChunkGenerator irisGenerator
|
||||||
? irisGenerator
|
? irisGenerator
|
||||||
: null;
|
: null;
|
||||||
boolean generatorUnbound = false;
|
boolean unloadEventStarted = false;
|
||||||
try {
|
try {
|
||||||
evacuate(server, level);
|
evacuate(server, level);
|
||||||
|
level.save(null, true, false);
|
||||||
|
unloadEventStarted = true;
|
||||||
|
ModdedEngineBootstrap.loader().fireDynamicLevelUnload(server, level);
|
||||||
if (generator != null) {
|
if (generator != null) {
|
||||||
generator.unbindEngine(level);
|
generator.unbindEngine(level);
|
||||||
generatorUnbound = true;
|
|
||||||
}
|
}
|
||||||
ModdedWorldEngines.evictOrThrow(level);
|
ModdedWorldEngines.evictOrThrow(level);
|
||||||
level.save(null, true, false);
|
|
||||||
serverAccess.removeLevel(server, key);
|
serverAccess.removeLevel(server, key);
|
||||||
// Undo snapshots pin the ServerLevel and could replay into the dead level.
|
// Undo snapshots pin the ServerLevel and could replay into the dead level.
|
||||||
art.arcane.iris.modded.command.ModdedObjectUndo.forget(level);
|
art.arcane.iris.modded.command.ModdedObjectUndo.forget(level);
|
||||||
@@ -233,7 +237,7 @@ public final class ModdedDimensionManager {
|
|||||||
ModdedIrisLog.info("Iris removed runtime dimension '{}'", dimensionId);
|
ModdedIrisLog.info("Iris removed runtime dimension '{}'", dimensionId);
|
||||||
return true;
|
return true;
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
rollbackRemoval(server, serverAccess, key, level, generator, generatorUnbound, e);
|
rollbackRemoval(server, serverAccess, key, level, generator, unloadEventStarted, e);
|
||||||
ModdedIrisLog.error("Iris failed to remove runtime dimension '{}'", dimensionId, e);
|
ModdedIrisLog.error("Iris failed to remove runtime dimension '{}'", dimensionId, e);
|
||||||
throw new IllegalStateException("Iris runtime dimension removal failed for " + dimensionId, e);
|
throw new IllegalStateException("Iris runtime dimension removal failed for " + dimensionId, e);
|
||||||
}
|
}
|
||||||
@@ -242,13 +246,16 @@ public final class ModdedDimensionManager {
|
|||||||
|
|
||||||
private static void rollbackRemoval(MinecraftServer server, ModdedServerAccess serverAccess,
|
private static void rollbackRemoval(MinecraftServer server, ModdedServerAccess serverAccess,
|
||||||
ResourceKey<Level> key, ServerLevel level,
|
ResourceKey<Level> key, ServerLevel level,
|
||||||
IrisModdedChunkGenerator generator, boolean generatorUnbound,
|
IrisModdedChunkGenerator generator, boolean unloadEventStarted,
|
||||||
Throwable failure) {
|
Throwable failure) {
|
||||||
try {
|
try {
|
||||||
if (!generatorUnbound || generator == null || !serverAccess.hasLevel(server, key)) {
|
if (!unloadEventStarted || !serverAccess.hasLevel(server, key)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (generator != null) {
|
||||||
generator.bindLevel(level);
|
generator.bindLevel(level);
|
||||||
|
}
|
||||||
|
ModdedEngineBootstrap.loader().fireDynamicLevelLoad(server, level);
|
||||||
} catch (Throwable rollbackFailure) {
|
} catch (Throwable rollbackFailure) {
|
||||||
if (rollbackFailure != failure) {
|
if (rollbackFailure != failure) {
|
||||||
failure.addSuppressed(rollbackFailure);
|
failure.addSuppressed(rollbackFailure);
|
||||||
@@ -258,44 +265,263 @@ public final class ModdedDimensionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean teleport(ServerPlayer player, MinecraftServer server, String dimensionId, double x, double y, double z) {
|
public static CompletableFuture<Boolean> teleportAsync(
|
||||||
ServerLevel level = level(server, dimensionId);
|
ServerPlayer player,
|
||||||
if (level == null) {
|
MinecraftServer server,
|
||||||
return false;
|
String dimensionId,
|
||||||
}
|
double x,
|
||||||
int blockX = (int) Math.floor(x);
|
double y,
|
||||||
int blockZ = (int) Math.floor(z);
|
double z
|
||||||
ChunkPos chunkPos = new ChunkPos(blockX >> 4, blockZ >> 4);
|
) {
|
||||||
if (level.getChunkSource().hasChunk(chunkPos.x(), chunkPos.z())) {
|
return teleportAsync(player, server, dimensionId, x, y, z,
|
||||||
completeTeleport(player, level, x, y, z, blockX, blockZ);
|
System.nanoTime() + TimeUnit.SECONDS.toNanos(TELEPORT_TIMEOUT_SECONDS));
|
||||||
return true;
|
|
||||||
}
|
|
||||||
UUID playerId = player.getUUID();
|
|
||||||
CompletableFuture
|
|
||||||
.supplyAsync(() -> level.getChunkSource().addTicketAndLoadWithRadius(TELEPORT_WARM_TICKET, chunkPos, 1), server)
|
|
||||||
.thenCompose((CompletableFuture<?> inner) -> inner)
|
|
||||||
// The ticket has no timeout of its own: bound the wait so the release below always runs.
|
|
||||||
.orTimeout(TELEPORT_WARM_TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
|
||||||
.whenComplete((Object result, Throwable error) -> server.execute(() -> {
|
|
||||||
level.getChunkSource().removeTicketWithRadius(TELEPORT_WARM_TICKET, chunkPos, 1);
|
|
||||||
if (error != null) {
|
|
||||||
ModdedIrisLog.warn("Iris chunk warm for teleport into '{}' at {},{} failed: {}", dimensionId, chunkPos.x(), chunkPos.z(), error.toString());
|
|
||||||
}
|
|
||||||
ServerPlayer target = server.getPlayerList().getPlayer(playerId);
|
|
||||||
if (target == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
completeTeleport(target, level, x, y, z, blockX, blockZ);
|
|
||||||
}));
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void completeTeleport(ServerPlayer player, ServerLevel level, double x, double y, double z, int blockX, int blockZ) {
|
public static CompletableFuture<Boolean> teleportAsync(
|
||||||
double targetY = y;
|
ServerPlayer player,
|
||||||
if (y == Double.MIN_VALUE) {
|
MinecraftServer server,
|
||||||
targetY = level.getHeight(Heightmap.Types.MOTION_BLOCKING, blockX, blockZ);
|
String dimensionId,
|
||||||
|
double x,
|
||||||
|
double y,
|
||||||
|
double z,
|
||||||
|
long deadlineNanos
|
||||||
|
) {
|
||||||
|
ServerLevel level = level(server, dimensionId);
|
||||||
|
if (level == null) {
|
||||||
|
return CompletableFuture.completedFuture(false);
|
||||||
}
|
}
|
||||||
player.teleportTo(level, x, targetY, z, Set.<Relative>of(), player.getYRot(), player.getXRot(), false);
|
return teleportAsync(player, server, level, x, y, z, deadlineNanos);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CompletableFuture<Boolean> teleportAsync(
|
||||||
|
ServerPlayer player,
|
||||||
|
MinecraftServer server,
|
||||||
|
ServerLevel level,
|
||||||
|
double x,
|
||||||
|
double y,
|
||||||
|
double z
|
||||||
|
) {
|
||||||
|
return teleportAsync(player, server, level, x, y, z, 0L);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CompletableFuture<Boolean> teleportAsync(
|
||||||
|
ServerPlayer player,
|
||||||
|
MinecraftServer server,
|
||||||
|
ServerLevel level,
|
||||||
|
double x,
|
||||||
|
double y,
|
||||||
|
double z,
|
||||||
|
long deadlineNanos
|
||||||
|
) {
|
||||||
|
CompletableFuture<Boolean> result = new CompletableFuture<>();
|
||||||
|
if (player == null || server == null || level == null || level.getServer() != server) {
|
||||||
|
result.complete(false);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (!Double.isFinite(x) || !Double.isFinite(z)
|
||||||
|
|| (y != Double.MIN_VALUE && !Double.isFinite(y))) {
|
||||||
|
result.completeExceptionally(new IllegalArgumentException("Teleport coordinates must be finite."));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (deadlineNanos != 0L) {
|
||||||
|
long remainingNanos = deadlineNanos - System.nanoTime();
|
||||||
|
if (remainingNanos <= 0L) {
|
||||||
|
result.completeExceptionally(teleportTimeout(level, x, z));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
result.orTimeout(remainingNanos, TimeUnit.NANOSECONDS);
|
||||||
|
}
|
||||||
|
UUID playerId = player.getUUID();
|
||||||
|
runOnServer(server, () -> beginTeleport(
|
||||||
|
result,
|
||||||
|
playerId,
|
||||||
|
server,
|
||||||
|
level,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
z,
|
||||||
|
deadlineNanos));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void beginTeleport(
|
||||||
|
CompletableFuture<Boolean> result,
|
||||||
|
UUID playerId,
|
||||||
|
MinecraftServer server,
|
||||||
|
ServerLevel level,
|
||||||
|
double x,
|
||||||
|
double y,
|
||||||
|
double z,
|
||||||
|
long deadlineNanos
|
||||||
|
) {
|
||||||
|
if (result.isDone()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (deadlineNanos != 0L && System.nanoTime() >= deadlineNanos) {
|
||||||
|
result.completeExceptionally(teleportTimeout(level, x, z));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ServerLevel active = level(server, level.dimension().identifier().toString());
|
||||||
|
if (active != level) {
|
||||||
|
result.complete(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int blockX = ModdedTeleportBounds.blockCoordinate(x);
|
||||||
|
int blockZ = ModdedTeleportBounds.blockCoordinate(z);
|
||||||
|
ChunkPos chunkPos = new ChunkPos(blockX >> 4, blockZ >> 4);
|
||||||
|
if (level.getChunkSource().hasChunk(chunkPos.x(), chunkPos.z())) {
|
||||||
|
completeTeleport(result, playerId, server, level, x, y, z, blockX, blockZ, deadlineNanos);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
warmAndTeleport(result, playerId, server, level, x, y, z, blockX, blockZ, chunkPos, deadlineNanos);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void warmAndTeleport(
|
||||||
|
CompletableFuture<Boolean> result,
|
||||||
|
UUID playerId,
|
||||||
|
MinecraftServer server,
|
||||||
|
ServerLevel level,
|
||||||
|
double x,
|
||||||
|
double y,
|
||||||
|
double z,
|
||||||
|
int blockX,
|
||||||
|
int blockZ,
|
||||||
|
ChunkPos chunkPos,
|
||||||
|
long deadlineNanos
|
||||||
|
) {
|
||||||
|
AtomicBoolean ticketReleased = new AtomicBoolean();
|
||||||
|
CompletableFuture<?> chunkLoad;
|
||||||
|
try {
|
||||||
|
chunkLoad = level.getChunkSource().addTicketAndLoadWithRadius(
|
||||||
|
TELEPORT_WARM_TICKET,
|
||||||
|
chunkPos,
|
||||||
|
TELEPORT_WARM_RADIUS);
|
||||||
|
} catch (Throwable failure) {
|
||||||
|
result.completeExceptionally(failure);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (chunkLoad == null) {
|
||||||
|
releaseTeleportTicket(level, chunkPos, ticketReleased);
|
||||||
|
result.completeExceptionally(new IllegalStateException(
|
||||||
|
"Chunk warm returned no completion future for " + level.dimension().identifier()
|
||||||
|
+ " at " + chunkPos.x() + "," + chunkPos.z() + "."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
result.whenComplete((success, failure) -> runOnServer(server,
|
||||||
|
() -> releaseTeleportTicket(level, chunkPos, ticketReleased)));
|
||||||
|
chunkLoad.whenComplete((ignored, failure) -> runOnServer(server, () -> {
|
||||||
|
releaseTeleportTicket(level, chunkPos, ticketReleased);
|
||||||
|
if (result.isDone()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (failure != null) {
|
||||||
|
result.completeExceptionally(failure);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
completeTeleport(result, playerId, server, level, x, y, z, blockX, blockZ, deadlineNanos);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void releaseTeleportTicket(
|
||||||
|
ServerLevel level,
|
||||||
|
ChunkPos chunkPos,
|
||||||
|
AtomicBoolean ticketReleased
|
||||||
|
) {
|
||||||
|
if (ticketReleased.compareAndSet(false, true)) {
|
||||||
|
level.getChunkSource().removeTicketWithRadius(
|
||||||
|
TELEPORT_WARM_TICKET,
|
||||||
|
chunkPos,
|
||||||
|
TELEPORT_WARM_RADIUS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void completeTeleport(
|
||||||
|
CompletableFuture<Boolean> result,
|
||||||
|
UUID playerId,
|
||||||
|
MinecraftServer server,
|
||||||
|
ServerLevel level,
|
||||||
|
double x,
|
||||||
|
double y,
|
||||||
|
double z,
|
||||||
|
int blockX,
|
||||||
|
int blockZ,
|
||||||
|
long deadlineNanos
|
||||||
|
) {
|
||||||
|
if (result.isDone()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (deadlineNanos != 0L && System.nanoTime() >= deadlineNanos) {
|
||||||
|
result.completeExceptionally(teleportTimeout(level, x, z));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ServerLevel active = level(server, level.dimension().identifier().toString());
|
||||||
|
ServerPlayer player = server.getPlayerList().getPlayer(playerId);
|
||||||
|
if (active != level || player == null) {
|
||||||
|
result.complete(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
int targetY = resolveSafeTeleportY(level, blockX, blockZ, y);
|
||||||
|
boolean teleported = player.teleportTo(
|
||||||
|
level,
|
||||||
|
x,
|
||||||
|
targetY,
|
||||||
|
z,
|
||||||
|
Set.<Relative>of(),
|
||||||
|
player.getYRot(),
|
||||||
|
player.getXRot(),
|
||||||
|
false);
|
||||||
|
result.complete(teleported);
|
||||||
|
} catch (Throwable failure) {
|
||||||
|
result.completeExceptionally(failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int resolveSafeTeleportY(ServerLevel level, int blockX, int blockZ, double requestedY) {
|
||||||
|
int initialY = requestedY == Double.MIN_VALUE
|
||||||
|
? level.getHeight(Heightmap.Types.MOTION_BLOCKING, blockX, blockZ)
|
||||||
|
: (int) Math.floor(requestedY);
|
||||||
|
int startY = ModdedTeleportBounds.clampY(level.getMinY(), level.getMaxY(), initialY);
|
||||||
|
int maximumY = ModdedTeleportBounds.maximumY(level.getMinY(), level.getMaxY());
|
||||||
|
int minimumY = ModdedTeleportBounds.minimumY(level.getMinY(), level.getMaxY());
|
||||||
|
for (int candidateY = startY; candidateY <= maximumY; candidateY++) {
|
||||||
|
if (isSafeStandingPosition(level, blockX, candidateY, blockZ)) {
|
||||||
|
return candidateY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int candidateY = startY - 1; candidateY >= minimumY; candidateY--) {
|
||||||
|
if (isSafeStandingPosition(level, blockX, candidateY, blockZ)) {
|
||||||
|
return candidateY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("No safe teleport position exists in "
|
||||||
|
+ level.dimension().identifier() + " at " + blockX + "," + blockZ + ".");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isSafeStandingPosition(ServerLevel level, int blockX, int blockY, int blockZ) {
|
||||||
|
BlockPos feet = new BlockPos(blockX, blockY, blockZ);
|
||||||
|
BlockPos head = feet.above();
|
||||||
|
BlockPos support = feet.below();
|
||||||
|
return level.getBlockState(feet).getCollisionShape(level, feet).isEmpty()
|
||||||
|
&& level.getBlockState(feet).getFluidState().isEmpty()
|
||||||
|
&& level.getBlockState(head).getCollisionShape(level, head).isEmpty()
|
||||||
|
&& level.getBlockState(head).getFluidState().isEmpty()
|
||||||
|
&& !level.getBlockState(support).getCollisionShape(level, support).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TimeoutException teleportTimeout(ServerLevel level, double x, double z) {
|
||||||
|
return new TimeoutException("Teleport into " + level.dimension().identifier()
|
||||||
|
+ " at " + ModdedTeleportBounds.blockCoordinate(x) + ","
|
||||||
|
+ ModdedTeleportBounds.blockCoordinate(z)
|
||||||
|
+ " exceeded " + TELEPORT_TIMEOUT_SECONDS + " seconds.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void runOnServer(MinecraftServer server, Runnable task) {
|
||||||
|
if (server.isSameThread()) {
|
||||||
|
task.run();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
server.execute(task);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Holder<DimensionType> resolveDimensionType(RegistryAccess registryAccess, String pack, String packDimensionKey) {
|
private static Holder<DimensionType> resolveDimensionType(RegistryAccess registryAccess, String pack, String packDimensionKey) {
|
||||||
@@ -362,6 +588,7 @@ public final class ModdedDimensionManager {
|
|||||||
List.of(),
|
List.of(),
|
||||||
false);
|
false);
|
||||||
|
|
||||||
|
boolean loadEventStarted = false;
|
||||||
try {
|
try {
|
||||||
generator.bindLevel(level);
|
generator.bindLevel(level);
|
||||||
Handle handle = new Handle(dimensionId, pack, packDimensionKey, seed, level, generator);
|
Handle handle = new Handle(dimensionId, pack, packDimensionKey, seed, level, generator);
|
||||||
@@ -371,9 +598,11 @@ public final class ModdedDimensionManager {
|
|||||||
+ "': the level was registered concurrently");
|
+ "': the level was registered concurrently");
|
||||||
}
|
}
|
||||||
server.getPlayerList().addWorldborderListener(level);
|
server.getPlayerList().addWorldborderListener(level);
|
||||||
|
loadEventStarted = true;
|
||||||
|
ModdedEngineBootstrap.loader().fireDynamicLevelLoad(server, level);
|
||||||
return handle;
|
return handle;
|
||||||
} catch (Throwable error) {
|
} catch (Throwable error) {
|
||||||
rollbackInjection(server, serverAccess, key, level, generator, error);
|
rollbackInjection(server, serverAccess, key, level, generator, loadEventStarted, error);
|
||||||
if (error instanceof RuntimeException runtimeException) {
|
if (error instanceof RuntimeException runtimeException) {
|
||||||
throw runtimeException;
|
throw runtimeException;
|
||||||
}
|
}
|
||||||
@@ -386,7 +615,17 @@ public final class ModdedDimensionManager {
|
|||||||
|
|
||||||
private static void rollbackInjection(MinecraftServer server, ModdedServerAccess serverAccess,
|
private static void rollbackInjection(MinecraftServer server, ModdedServerAccess serverAccess,
|
||||||
ResourceKey<Level> key, ServerLevel level,
|
ResourceKey<Level> key, ServerLevel level,
|
||||||
IrisModdedChunkGenerator generator, Throwable failure) {
|
IrisModdedChunkGenerator generator, boolean loadEventStarted,
|
||||||
|
Throwable failure) {
|
||||||
|
if (loadEventStarted) {
|
||||||
|
try {
|
||||||
|
ModdedEngineBootstrap.loader().fireDynamicLevelUnload(server, level);
|
||||||
|
} catch (Throwable cleanupError) {
|
||||||
|
failure.addSuppressed(cleanupError);
|
||||||
|
ModdedIrisLog.error("Iris failed to publish rollback unload for {}",
|
||||||
|
key.identifier(), cleanupError);
|
||||||
|
}
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
if (serverAccess.hasLevel(server, key)) {
|
if (serverAccess.hasLevel(server, key)) {
|
||||||
ServerLevel removed = serverAccess.removeLevel(server, key);
|
ServerLevel removed = serverAccess.removeLevel(server, key);
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ public interface ModdedLoader {
|
|||||||
|
|
||||||
void invalidateLevelCache(MinecraftServer server);
|
void invalidateLevelCache(MinecraftServer server);
|
||||||
|
|
||||||
|
void fireDynamicLevelLoad(MinecraftServer server, ServerLevel level);
|
||||||
|
|
||||||
|
void fireDynamicLevelUnload(MinecraftServer server, ServerLevel level);
|
||||||
|
|
||||||
boolean clientEnvironment();
|
boolean clientEnvironment();
|
||||||
|
|
||||||
Path configDir();
|
Path configDir();
|
||||||
|
|||||||
+24
-2
@@ -26,12 +26,14 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
public final class ModdedPrimaryWorldRouter {
|
public final class ModdedPrimaryWorldRouter {
|
||||||
private static final int TICK_INTERVAL = 20;
|
private static final int TICK_INTERVAL = 20;
|
||||||
|
|
||||||
private static final Set<UUID> routed = ConcurrentHashMap.newKeySet();
|
private static final Set<UUID> routed = ConcurrentHashMap.newKeySet();
|
||||||
|
private static final Set<UUID> inFlight = ConcurrentHashMap.newKeySet();
|
||||||
private static int tickCounter = 0;
|
private static int tickCounter = 0;
|
||||||
|
|
||||||
private ModdedPrimaryWorldRouter() {
|
private ModdedPrimaryWorldRouter() {
|
||||||
@@ -39,6 +41,7 @@ public final class ModdedPrimaryWorldRouter {
|
|||||||
|
|
||||||
public static void clear() {
|
public static void clear() {
|
||||||
routed.clear();
|
routed.clear();
|
||||||
|
inFlight.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,6 +51,7 @@ public final class ModdedPrimaryWorldRouter {
|
|||||||
public static void forget(UUID player) {
|
public static void forget(UUID player) {
|
||||||
if (player != null) {
|
if (player != null) {
|
||||||
routed.remove(player);
|
routed.remove(player);
|
||||||
|
inFlight.remove(player);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,17 +86,35 @@ public final class ModdedPrimaryWorldRouter {
|
|||||||
List<ServerPlayer> players = new ArrayList<>(server.getPlayerList().getPlayers());
|
List<ServerPlayer> players = new ArrayList<>(server.getPlayerList().getPlayers());
|
||||||
for (ServerPlayer player : players) {
|
for (ServerPlayer player : players) {
|
||||||
UUID id = player.getUUID();
|
UUID id = player.getUUID();
|
||||||
if (routed.contains(id)) {
|
if (routed.contains(id) || !inFlight.add(id)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (player.level() != overworld) {
|
if (player.level() != overworld) {
|
||||||
|
inFlight.remove(id);
|
||||||
routed.add(id);
|
routed.add(id);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
ModdedDimensionManager.teleport(player, server, primary, player.getX(), Double.MIN_VALUE, player.getZ());
|
CompletableFuture<Boolean> teleport = ModdedDimensionManager.teleportAsync(
|
||||||
|
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);
|
routed.add(id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (failure != null) {
|
||||||
|
ModdedIrisLog.error("Iris failed to route player {} to primary world '{}'",
|
||||||
|
id, primary, failure);
|
||||||
|
}
|
||||||
|
});
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
|
inFlight.remove(id);
|
||||||
ModdedIrisLog.error("Iris failed to route player {} to primary world '{}'", id, primary, e);
|
ModdedIrisLog.error("Iris failed to route player {} to primary world '{}'", id, primary, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* Iris is a World Generator for Minecraft Servers
|
||||||
|
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package art.arcane.iris.modded;
|
||||||
|
|
||||||
|
final class ModdedTeleportBounds {
|
||||||
|
private ModdedTeleportBounds() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static int blockCoordinate(double coordinate) {
|
||||||
|
return (int) Math.floor(coordinate);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int clampY(int minY, int maxY, int requestedY) {
|
||||||
|
int minimumY = minimumY(minY, maxY);
|
||||||
|
int maximumY = maximumY(minY, maxY);
|
||||||
|
return Math.max(minimumY, Math.min(maximumY, requestedY));
|
||||||
|
}
|
||||||
|
|
||||||
|
static int minimumY(int minY, int maxY) {
|
||||||
|
requireStandingSpace(minY, maxY);
|
||||||
|
return minY + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int maximumY(int minY, int maxY) {
|
||||||
|
requireStandingSpace(minY, maxY);
|
||||||
|
return maxY - 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void requireStandingSpace(int minY, int maxY) {
|
||||||
|
if (maxY - minY < 3) {
|
||||||
|
throw new IllegalArgumentException("Level height must provide support, feet, and head space.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
-3
@@ -131,10 +131,26 @@ public final class IrisModdedCommands {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
String dimensionId = level.dimension().identifier().toString();
|
String dimensionId = level.dimension().identifier().toString();
|
||||||
if (!ModdedDimensionManager.teleport(player, source.getServer(), dimensionId, 8.5D, Double.MIN_VALUE, 8.5D)) {
|
MinecraftServer server = source.getServer();
|
||||||
fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_TELEPORT_FAILED_DIMENSION_IS_NOT_LOADED, MessageArgument.untrusted("dimensionId", dimensionId)));
|
CompletableFuture<Boolean> teleport = ModdedDimensionManager.teleportAsync(
|
||||||
return 0;
|
player,
|
||||||
|
server,
|
||||||
|
dimensionId,
|
||||||
|
8.5D,
|
||||||
|
Double.MIN_VALUE,
|
||||||
|
8.5D);
|
||||||
|
teleport.whenComplete((success, failure) -> {
|
||||||
|
if (Boolean.TRUE.equals(success) && failure == null) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
if (failure != null) {
|
||||||
|
ModdedIrisLog.error("Iris teleport into '{}' failed for {}",
|
||||||
|
dimensionId, player.getUUID(), failure);
|
||||||
|
}
|
||||||
|
server.execute(() -> fail(source, IrisLanguage.plain(
|
||||||
|
ModdedCommandMessages.IRIS_MODDED_COMMANDS_TELEPORT_FAILED_DIMENSION_IS_NOT_LOADED,
|
||||||
|
MessageArgument.untrusted("dimensionId", dimensionId))));
|
||||||
|
});
|
||||||
ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_TELEPORTING, MessageArgument.untrusted("value", player.getScoreboardName()), MessageArgument.untrusted("dimensionId", dimensionId)));
|
ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_TELEPORTING, MessageArgument.untrusted("value", player.getScoreboardName()), MessageArgument.untrusted("dimensionId", dimensionId)));
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-2
@@ -107,7 +107,7 @@ public final class ModdedGuiHost implements GuiHost.Provider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public GuiOverlay overlayFor(Engine engine) {
|
public GuiOverlay overlayFor(Engine engine, UUID openerId) {
|
||||||
if (engine == null) {
|
if (engine == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -115,6 +115,7 @@ public final class ModdedGuiHost implements GuiHost.Provider {
|
|||||||
if (level == null || server == null) {
|
if (level == null || server == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return new ModdedVisionOverlay(server, level, engine, openers.get(engine));
|
UUID resolvedOpenerId = openerId == null ? openers.get(engine) : openerId;
|
||||||
|
return new ModdedVisionOverlay(server, level, engine, resolvedOpenerId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+285
-92
@@ -44,6 +44,7 @@ import art.arcane.iris.engine.object.IrisSpawner;
|
|||||||
import art.arcane.iris.engine.object.IrisStructurePlacement;
|
import art.arcane.iris.engine.object.IrisStructurePlacement;
|
||||||
import art.arcane.iris.modded.ModdedDimensionManager;
|
import art.arcane.iris.modded.ModdedDimensionManager;
|
||||||
import art.arcane.iris.modded.ModdedEngineBootstrap;
|
import art.arcane.iris.modded.ModdedEngineBootstrap;
|
||||||
|
import art.arcane.iris.modded.ModdedScheduler;
|
||||||
import art.arcane.iris.modded.ModdedWorkspaceGenerator;
|
import art.arcane.iris.modded.ModdedWorkspaceGenerator;
|
||||||
import art.arcane.iris.util.common.parallel.BurstExecutor;
|
import art.arcane.iris.util.common.parallel.BurstExecutor;
|
||||||
import art.arcane.iris.util.common.parallel.MultiBurst;
|
import art.arcane.iris.util.common.parallel.MultiBurst;
|
||||||
@@ -67,7 +68,6 @@ import net.minecraft.commands.SharedSuggestionProvider;
|
|||||||
import net.minecraft.server.MinecraftServer;
|
import net.minecraft.server.MinecraftServer;
|
||||||
import net.minecraft.server.level.ServerLevel;
|
import net.minecraft.server.level.ServerLevel;
|
||||||
import net.minecraft.server.level.ServerPlayer;
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
import net.minecraft.world.entity.Relative;
|
|
||||||
import org.zeroturnaround.zip.ZipUtil;
|
import org.zeroturnaround.zip.ZipUtil;
|
||||||
|
|
||||||
import java.awt.Desktop;
|
import java.awt.Desktop;
|
||||||
@@ -79,6 +79,8 @@ import java.util.List;
|
|||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.CompletionException;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
@@ -96,6 +98,7 @@ public final class ModdedStudioCommands {
|
|||||||
private static final String DEFAULT_TEMPLATE = "example";
|
private static final String DEFAULT_TEMPLATE = "example";
|
||||||
private static final UUID CONSOLE_OWNER = new UUID(0L, 0L);
|
private static final UUID CONSOLE_OWNER = new UUID(0L, 0L);
|
||||||
private static final Map<UUID, String> STUDIOS = new ConcurrentHashMap<>();
|
private static final Map<UUID, String> STUDIOS = new ConcurrentHashMap<>();
|
||||||
|
private static final ModdedStudioTransitionQueue TRANSITIONS = new ModdedStudioTransitionQueue();
|
||||||
private static final SuggestionProvider<CommandSourceStack> GENERATOR_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> {
|
private static final SuggestionProvider<CommandSourceStack> GENERATOR_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> {
|
||||||
ModdedCommandFeedback.tab(context.getSource());
|
ModdedCommandFeedback.tab(context.getSource());
|
||||||
try {
|
try {
|
||||||
@@ -192,6 +195,7 @@ public final class ModdedStudioCommands {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void clear() {
|
public static void clear() {
|
||||||
|
TRANSITIONS.clear();
|
||||||
STUDIOS.clear();
|
STUDIOS.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,7 +276,7 @@ public final class ModdedStudioCommands {
|
|||||||
}
|
}
|
||||||
ServerPlayer player = source.getPlayer();
|
ServerPlayer player = source.getPlayer();
|
||||||
ModdedGuiHost.bindContext(source.getServer(), level, engine, player == null ? null : player.getUUID());
|
ModdedGuiHost.bindContext(source.getServer(), level, engine, player == null ? null : player.getUUID());
|
||||||
VisionGUI.launch(engine);
|
VisionGUI.launch(engine, player == null ? null : player.getUUID());
|
||||||
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_OPENING_VISION_MAP_ON_SERVER_DISPLAY, MessageArgument.untrusted("value", level.dimension().identifier())));
|
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_OPENING_VISION_MAP_ON_SERVER_DISPLAY, MessageArgument.untrusted("value", level.dimension().identifier())));
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@@ -402,124 +406,242 @@ public final class ModdedStudioCommands {
|
|||||||
String dimensionId = player == null ? studioConsoleDimensionId() : studioDimensionId(player);
|
String dimensionId = player == null ? studioConsoleDimensionId() : studioDimensionId(player);
|
||||||
MinecraftServer server = source.getServer();
|
MinecraftServer server = source.getServer();
|
||||||
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_OPENING_STUDIO_SEED, MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("seed", seed)));
|
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_OPENING_STUDIO_SEED, MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("seed", seed)));
|
||||||
Thread thread = new Thread(() -> openAsync(source, server, owner, dimensionId, pack, seed), "Iris Studio Open");
|
CompletableFuture<Void> transition = TRANSITIONS.submit(
|
||||||
thread.setDaemon(true);
|
owner,
|
||||||
thread.start();
|
() -> openTransition(source, server, owner, dimensionId, pack, seed));
|
||||||
|
reportOpenFailure(source, server, dimensionId, pack, transition);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void openAsync(CommandSourceStack source, MinecraftServer server, UUID owner, String dimensionId, String pack, long seed) {
|
private static CompletableFuture<Void> openTransition(
|
||||||
|
CommandSourceStack source,
|
||||||
|
MinecraftServer server,
|
||||||
|
UUID owner,
|
||||||
|
String dimensionId,
|
||||||
|
String pack,
|
||||||
|
long seed
|
||||||
|
) {
|
||||||
|
CompletableFuture<Void> transition = new CompletableFuture<>();
|
||||||
|
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
|
||||||
|
if (scheduler == null) {
|
||||||
|
transition.completeExceptionally(new IllegalStateException(
|
||||||
|
"Iris modded scheduler is unavailable for Studio open."));
|
||||||
|
return transition;
|
||||||
|
}
|
||||||
|
scheduler.asyncIfRunning(() -> prepareStudioOpen(
|
||||||
|
source,
|
||||||
|
server,
|
||||||
|
owner,
|
||||||
|
dimensionId,
|
||||||
|
pack,
|
||||||
|
seed,
|
||||||
|
transition),
|
||||||
|
() -> transition.completeExceptionally(new IllegalStateException(
|
||||||
|
"Iris modded scheduler rejected Studio open.")));
|
||||||
|
return transition;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void prepareStudioOpen(
|
||||||
|
CommandSourceStack source,
|
||||||
|
MinecraftServer server,
|
||||||
|
UUID owner,
|
||||||
|
String dimensionId,
|
||||||
|
String pack,
|
||||||
|
long seed,
|
||||||
|
CompletableFuture<Void> transition
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
File packFolder = new File(ModdedPackCommands.packsRoot(), pack);
|
File packFolder = new File(ModdedPackCommands.packsRoot(), pack);
|
||||||
if (!new File(packFolder, "dimensions/" + pack + ".json").isFile()) {
|
if (!new File(packFolder, "dimensions/" + pack + ".json").isFile()) {
|
||||||
server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(
|
throw new IllegalStateException("Required Studio pack '" + pack
|
||||||
ModdedCommandMessages.MODDED_STUDIO_COMMANDS_REQUIRED_PACK_IS_NOT_INSTALLED_INSTALL_THEN_RESTART,
|
+ "' is not installed with dimensions/" + pack + ".json.");
|
||||||
MessageArgument.untrusted("pack", pack))));
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
IrisData data = IrisData.get(packFolder);
|
IrisData data = IrisData.get(packFolder);
|
||||||
IrisDimension dimension = data.getDimensionLoader().load(pack);
|
IrisDimension dimension = data.getDimensionLoader().load(pack);
|
||||||
if (dimension == null) {
|
if (dimension == null) {
|
||||||
server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_PACK_HAS_NO_DIMENSIONS_JSON, MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("pack2", pack))));
|
throw new IllegalStateException("Studio pack '" + pack
|
||||||
return;
|
+ "' has no dimensions/" + pack + ".json definition.");
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
ModdedWorkspaceGenerator.writeWorkspace(data, packFolder, true);
|
ModdedWorkspaceGenerator.writeWorkspace(data, packFolder, true);
|
||||||
} catch (Throwable workspaceError) {
|
server.execute(() -> executeStudioOpen(
|
||||||
ModdedIrisLog.error("Iris workspace write failed for {}", packFolder, workspaceError);
|
source,
|
||||||
server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(
|
server,
|
||||||
ModdedCommandMessages.MODDED_STUDIO_COMMANDS_FAILED_WRITE_WORKSPACE,
|
owner,
|
||||||
MessageArgument.untrusted("value", packFolder.getAbsolutePath()),
|
dimensionId,
|
||||||
MessageArgument.untrusted("value2", String.valueOf(workspaceError.getMessage())))));
|
pack,
|
||||||
}
|
seed,
|
||||||
server.execute(() -> {
|
transition));
|
||||||
if (owner.equals(CONSOLE_OWNER)) {
|
} catch (Throwable failure) {
|
||||||
injectConsole(source, server, dimensionId, pack, seed);
|
transition.completeExceptionally(failure);
|
||||||
} else {
|
|
||||||
injectAndTeleport(source, server, owner, dimensionId, pack, seed);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (Throwable e) {
|
|
||||||
ModdedIrisLog.error("Iris studio open failed for {}", pack, e);
|
|
||||||
server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_STUDIO_OPEN_FAILED, MessageArgument.untrusted("value", e.getClass().getSimpleName()), MessageArgument.trusted("errorMessage", IrisLanguage.errorDetail(e)))));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void injectConsole(CommandSourceStack source, MinecraftServer server, String dimensionId, String pack, long seed) {
|
private static void executeStudioOpen(
|
||||||
ModdedDimensionManager.Handle handle;
|
CommandSourceStack source,
|
||||||
|
MinecraftServer server,
|
||||||
|
UUID owner,
|
||||||
|
String dimensionId,
|
||||||
|
String pack,
|
||||||
|
long seed,
|
||||||
|
CompletableFuture<Void> transition
|
||||||
|
) {
|
||||||
|
ModdedDimensionManager.Handle handle = null;
|
||||||
try {
|
try {
|
||||||
|
replaceExistingStudio(server, owner, dimensionId);
|
||||||
handle = ModdedDimensionManager.create(server, dimensionId, pack, pack, seed);
|
handle = ModdedDimensionManager.create(server, dimensionId, pack, pack, seed);
|
||||||
} catch (Throwable e) {
|
STUDIOS.put(owner, dimensionId);
|
||||||
ModdedIrisLog.error("Iris console studio injection failed for {} ({})", dimensionId, pack, e);
|
if (owner.equals(CONSOLE_OWNER)) {
|
||||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_STUDIO_INJECTION_FAILED, MessageArgument.untrusted("value", e.getClass().getSimpleName()), MessageArgument.trusted("errorMessage", IrisLanguage.errorDetail(e))));
|
completeConsoleOpen(source, dimensionId, pack, seed, handle);
|
||||||
|
transition.complete(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
STUDIOS.put(CONSOLE_OWNER, dimensionId);
|
ServerPlayer player = server.getPlayerList().getPlayer(owner);
|
||||||
|
if (player == null) {
|
||||||
|
throw new IllegalStateException("Studio owner disconnected before teleport.");
|
||||||
|
}
|
||||||
ServerLevel studio = handle.level();
|
ServerLevel studio = handle.level();
|
||||||
int surface = studio.getMaxY();
|
|
||||||
try {
|
|
||||||
Engine engine = IrisModdedCommands.engineFor(studio);
|
Engine engine = IrisModdedCommands.engineFor(studio);
|
||||||
if (engine != null) {
|
if (engine == null) {
|
||||||
surface = engine.getMinHeight() + engine.getHeight(8, 8, false) + 2;
|
throw new IllegalStateException("Studio engine is unavailable for " + dimensionId + ".");
|
||||||
}
|
}
|
||||||
} catch (Throwable e) {
|
int surfaceY = engine.getMinHeight() + engine.getHeight(8, 8, false) + 2;
|
||||||
ModdedIrisLog.error("Iris console studio surface probe failed for {}", dimensionId, e);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void replaceExistingStudio(MinecraftServer server, UUID owner, String dimensionId) {
|
||||||
|
if (ModdedDimensionManager.level(server, dimensionId) != null
|
||||||
|
|| ModdedDimensionManager.handle(dimensionId) != null) {
|
||||||
|
ModdedDimensionManager.remove(server, dimensionId, true);
|
||||||
|
}
|
||||||
|
STUDIOS.remove(owner, dimensionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void cleanupFailedOpen(
|
||||||
|
MinecraftServer server,
|
||||||
|
UUID owner,
|
||||||
|
String dimensionId,
|
||||||
|
Throwable failure
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
ModdedDimensionManager.remove(server, dimensionId, true);
|
||||||
|
STUDIOS.remove(owner, dimensionId);
|
||||||
|
} catch (Throwable cleanupFailure) {
|
||||||
|
failure.addSuppressed(cleanupFailure);
|
||||||
|
ModdedIrisLog.error("Iris failed to clean up Studio '{}' after open failure",
|
||||||
|
dimensionId, cleanupFailure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void completeConsoleOpen(
|
||||||
|
CommandSourceStack source,
|
||||||
|
String dimensionId,
|
||||||
|
String pack,
|
||||||
|
long seed,
|
||||||
|
ModdedDimensionManager.Handle handle
|
||||||
|
) {
|
||||||
|
ServerLevel studio = handle.level();
|
||||||
|
Engine engine = IrisModdedCommands.engineFor(studio);
|
||||||
|
int surface = engine == null
|
||||||
|
? studio.getMinY() + 1
|
||||||
|
: engine.getMinHeight() + engine.getHeight(8, 8, false) + 2;
|
||||||
|
surface = Math.max(studio.getMinY() + 1, Math.min(studio.getMaxY() - 2, surface));
|
||||||
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_CONSOLE_STUDIO_OPEN_NOW_RUNS_SEED_TRANSIENT_NOT_WRITTEN_IRIS, MessageArgument.untrusted("dimensionId", dimensionId), MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("seed", seed)));
|
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_CONSOLE_STUDIO_OPEN_NOW_RUNS_SEED_TRANSIENT_NOT_WRITTEN_IRIS, MessageArgument.untrusted("dimensionId", dimensionId), MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("seed", seed)));
|
||||||
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_ENTER_IT_WITH_EXECUTE_RUN_TP_S_8_5_8, MessageArgument.untrusted("dimensionId", dimensionId), MessageArgument.untrusted("surface", surface)));
|
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_ENTER_IT_WITH_EXECUTE_RUN_TP_S_8_5_8, MessageArgument.untrusted("dimensionId", dimensionId), MessageArgument.untrusted("surface", surface)));
|
||||||
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_PREGEN_IT_WITH_IRIS_PREGEN_START_RADIUS, MessageArgument.untrusted("dimensionId", dimensionId)));
|
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_PREGEN_IT_WITH_IRIS_PREGEN_START_RADIUS, MessageArgument.untrusted("dimensionId", dimensionId)));
|
||||||
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_REMOVE_IT_WITH_IRIS_STUDIO_CLOSE));
|
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_REMOVE_IT_WITH_IRIS_STUDIO_CLOSE));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void injectAndTeleport(CommandSourceStack source, MinecraftServer server, UUID owner, String dimensionId, String pack, long seed) {
|
private static void reportOpenFailure(
|
||||||
ServerPlayer player = server.getPlayerList().getPlayer(owner);
|
CommandSourceStack source,
|
||||||
if (player == null) {
|
MinecraftServer server,
|
||||||
|
String dimensionId,
|
||||||
|
String pack,
|
||||||
|
CompletableFuture<Void> transition
|
||||||
|
) {
|
||||||
|
transition.whenComplete((ignored, failure) -> {
|
||||||
|
if (failure == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ModdedDimensionManager.Handle handle;
|
Throwable cause = unwrapFailure(failure);
|
||||||
try {
|
ModdedIrisLog.error("Iris Studio open failed for '{}' ({})", dimensionId, pack, cause);
|
||||||
handle = ModdedDimensionManager.create(server, dimensionId, pack, pack, seed);
|
server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(
|
||||||
} catch (Throwable e) {
|
ModdedCommandMessages.MODDED_STUDIO_COMMANDS_STUDIO_OPEN_FAILED,
|
||||||
ModdedIrisLog.error("Iris studio injection failed for {} ({})", dimensionId, pack, e);
|
MessageArgument.untrusted("value", cause.getClass().getSimpleName()),
|
||||||
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))));
|
MessageArgument.trusted("errorMessage", IrisLanguage.errorDetail(cause)))));
|
||||||
return;
|
});
|
||||||
}
|
|
||||||
STUDIOS.put(owner, dimensionId);
|
|
||||||
ServerLevel studio = handle.level();
|
|
||||||
int surface = studio.getMaxY();
|
|
||||||
try {
|
|
||||||
Engine engine = IrisModdedCommands.engineFor(studio);
|
|
||||||
if (engine != null) {
|
|
||||||
surface = engine.getMinHeight() + engine.getHeight(8, 8, false) + 2;
|
|
||||||
}
|
|
||||||
} catch (Throwable e) {
|
|
||||||
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)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int close(CommandSourceStack source) {
|
private static int close(CommandSourceStack source) {
|
||||||
ServerPlayer player = source.getPlayer();
|
ServerPlayer player = source.getPlayer();
|
||||||
MinecraftServer server = source.getServer();
|
MinecraftServer server = source.getServer();
|
||||||
UUID owner = player == null ? CONSOLE_OWNER : player.getUUID();
|
UUID owner = player == null ? CONSOLE_OWNER : player.getUUID();
|
||||||
// Commit the ownership drop only after removal succeeds: dropping it first orphaned a
|
TRANSITIONS.submit(owner, () -> closeTransition(source, server, owner));
|
||||||
// still-registered studio that no command could ever remove again.
|
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);
|
String dimensionId = STUDIOS.get(owner);
|
||||||
if (dimensionId == null) {
|
if (dimensionId == null) {
|
||||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_YOU_DO_NOT_HAVE_OPEN_STUDIO_USE_IRIS_STUDIO_OPEN));
|
IrisModdedCommands.fail(source, IrisLanguage.plain(
|
||||||
return 0;
|
ModdedCommandMessages.MODDED_STUDIO_COMMANDS_YOU_DO_NOT_HAVE_OPEN_STUDIO_USE_IRIS_STUDIO_OPEN));
|
||||||
|
transition.complete(null);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
ModdedDimensionManager.remove(server, dimensionId, true);
|
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);
|
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)));
|
IrisModdedCommands.ok(source, IrisLanguage.plain(
|
||||||
return 1;
|
ModdedCommandMessages.MODDED_STUDIO_COMMANDS_STUDIO_CLOSED_WAS_EVACUATED_UNLOADED_ITS_REGION_DATA_DELETED,
|
||||||
|
MessageArgument.untrusted("dimensionId", dimensionId)));
|
||||||
|
transition.complete(null);
|
||||||
|
} catch (Throwable failure) {
|
||||||
|
ModdedIrisLog.error("Iris Studio close failed for {}", dimensionId, failure);
|
||||||
|
IrisModdedCommands.fail(source, IrisLanguage.plain(
|
||||||
|
ModdedCommandMessages.MODDED_STUDIO_COMMANDS_STUDIO_CLOSE_FAILED,
|
||||||
|
MessageArgument.untrusted("value", failure.getClass().getSimpleName()),
|
||||||
|
MessageArgument.trusted("errorMessage", IrisLanguage.errorDetail(failure))));
|
||||||
|
transition.completeExceptionally(failure);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return transition;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int status(CommandSourceStack source) {
|
private static int status(CommandSourceStack source) {
|
||||||
@@ -568,29 +690,100 @@ public final class ModdedStudioCommands {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
MinecraftServer server = source.getServer();
|
MinecraftServer server = source.getServer();
|
||||||
String dimensionId = STUDIOS.get(player.getUUID());
|
UUID owner = player.getUUID();
|
||||||
|
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) {
|
if (dimensionId == null) {
|
||||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_YOU_DO_NOT_HAVE_OPEN_STUDIO_USE_IRIS_STUDIO_OPEN_2));
|
IrisModdedCommands.fail(source, IrisLanguage.plain(
|
||||||
return 0;
|
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);
|
ServerLevel studio = ModdedDimensionManager.level(server, dimensionId);
|
||||||
if (studio == null) {
|
if (studio == null) {
|
||||||
STUDIOS.remove(player.getUUID());
|
STUDIOS.remove(owner, dimensionId);
|
||||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_YOUR_STUDIO_DIMENSION_IS_NO_LONGER_LOADED_USE_IRIS_STUDIO));
|
IrisModdedCommands.fail(source, IrisLanguage.plain(
|
||||||
return 0;
|
ModdedCommandMessages.MODDED_STUDIO_COMMANDS_YOUR_STUDIO_DIMENSION_IS_NO_LONGER_LOADED_USE_IRIS_STUDIO));
|
||||||
|
transition.complete(null);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
int surface = studio.getMaxY();
|
ServerPlayer activePlayer = server.getPlayerList().getPlayer(owner);
|
||||||
try {
|
|
||||||
Engine engine = IrisModdedCommands.engineFor(studio);
|
Engine engine = IrisModdedCommands.engineFor(studio);
|
||||||
if (engine != null) {
|
if (activePlayer == null || engine == null) {
|
||||||
surface = engine.getMinHeight() + engine.getHeight(8, 8, false) + 2;
|
transition.completeExceptionally(new IllegalStateException(
|
||||||
|
"Studio player or engine is unavailable for teleport."));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
} catch (Throwable e) {
|
int surfaceY = engine.getMinHeight() + engine.getHeight(8, 8, false) + 2;
|
||||||
ModdedIrisLog.error("Iris tpstudio surface probe failed", e);
|
CompletableFuture<Boolean> teleport = ModdedDimensionManager.teleportAsync(
|
||||||
|
activePlayer,
|
||||||
|
server,
|
||||||
|
studio,
|
||||||
|
8.5D,
|
||||||
|
surfaceY,
|
||||||
|
8.5D);
|
||||||
|
teleport.whenComplete((success, failure) -> server.execute(() -> {
|
||||||
|
if (transition.isDone()) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
player.teleportTo(studio, 8.5D, surface, 8.5D, java.util.Set.<Relative>of(), player.getYRot(), player.getXRot(), false);
|
if (failure != null) {
|
||||||
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_TELEPORTED_YOUR_STUDIO, MessageArgument.untrusted("dimensionId", dimensionId)));
|
transition.completeExceptionally(failure);
|
||||||
return 1;
|
return;
|
||||||
|
}
|
||||||
|
if (!Boolean.TRUE.equals(success)) {
|
||||||
|
transition.completeExceptionally(new IllegalStateException(
|
||||||
|
"Studio native teleport did not complete successfully."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
IrisModdedCommands.ok(source, IrisLanguage.plain(
|
||||||
|
ModdedCommandMessages.MODDED_STUDIO_COMMANDS_TELEPORTED_YOUR_STUDIO,
|
||||||
|
MessageArgument.untrusted("dimensionId", dimensionId)));
|
||||||
|
transition.complete(null);
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
return transition;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void reportTeleportFailure(
|
||||||
|
CommandSourceStack source,
|
||||||
|
MinecraftServer server,
|
||||||
|
CompletableFuture<Void> transition
|
||||||
|
) {
|
||||||
|
transition.whenComplete((ignored, failure) -> {
|
||||||
|
if (failure == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Throwable cause = unwrapFailure(failure);
|
||||||
|
ModdedIrisLog.error("Iris Studio teleport failed", cause);
|
||||||
|
server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(
|
||||||
|
ModdedCommandMessages.MODDED_STUDIO_COMMANDS_STUDIO_OPEN_FAILED,
|
||||||
|
MessageArgument.untrusted("value", cause.getClass().getSimpleName()),
|
||||||
|
MessageArgument.trusted("errorMessage", IrisLanguage.errorDetail(cause)))));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Throwable unwrapFailure(Throwable failure) {
|
||||||
|
Throwable cause = failure;
|
||||||
|
while (cause instanceof CompletionException && cause.getCause() != null) {
|
||||||
|
cause = cause.getCause();
|
||||||
|
}
|
||||||
|
return cause;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int version(CommandSourceStack source, String pack) {
|
private static int version(CommandSourceStack source, String pack) {
|
||||||
|
|||||||
+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.IrisComplex;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
import art.arcane.iris.engine.framework.render.RenderType;
|
import art.arcane.iris.engine.framework.render.RenderType;
|
||||||
|
import art.arcane.iris.modded.ModdedDimensionManager;
|
||||||
|
import art.arcane.iris.modded.ModdedIrisLog;
|
||||||
import net.minecraft.server.MinecraftServer;
|
import net.minecraft.server.MinecraftServer;
|
||||||
import net.minecraft.server.level.ServerLevel;
|
import net.minecraft.server.level.ServerLevel;
|
||||||
import net.minecraft.server.level.ServerPlayer;
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
import net.minecraft.world.entity.Entity;
|
import net.minecraft.world.entity.Entity;
|
||||||
import net.minecraft.world.entity.LivingEntity;
|
import net.minecraft.world.entity.LivingEntity;
|
||||||
import net.minecraft.world.level.levelgen.Heightmap;
|
|
||||||
import net.minecraft.world.phys.Vec3;
|
import net.minecraft.world.phys.Vec3;
|
||||||
|
|
||||||
import java.awt.Desktop;
|
import java.awt.Desktop;
|
||||||
@@ -37,6 +38,8 @@ import java.io.File;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
public final class ModdedVisionOverlay implements GuiOverlay {
|
public final class ModdedVisionOverlay implements GuiOverlay {
|
||||||
@@ -80,11 +83,14 @@ public final class ModdedVisionOverlay implements GuiOverlay {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void teleport(double worldX, double worldZ) {
|
public void teleport(double worldX, double worldZ) {
|
||||||
int blockX = (int) worldX;
|
int blockX = (int) Math.floor(worldX);
|
||||||
int blockZ = (int) worldZ;
|
int blockZ = (int) Math.floor(worldZ);
|
||||||
server.execute(() -> {
|
server.execute(() -> {
|
||||||
ServerPlayer player = opener == null ? null : server.getPlayerList().getPlayer(opener);
|
ServerPlayer player = opener == null ? null : server.getPlayerList().getPlayer(opener);
|
||||||
if (player == null) {
|
if (player == null) {
|
||||||
|
if (opener != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
List<ServerPlayer> players = level.players();
|
List<ServerPlayer> players = level.players();
|
||||||
if (players.isEmpty()) {
|
if (players.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
@@ -92,8 +98,24 @@ public final class ModdedVisionOverlay implements GuiOverlay {
|
|||||||
player = players.get(0);
|
player = players.get(0);
|
||||||
}
|
}
|
||||||
int surfaceY = engine.getMinHeight() + engine.getHeight(blockX, blockZ, false) + 2;
|
int surfaceY = engine.getMinHeight() + engine.getHeight(blockX, blockZ, false) + 2;
|
||||||
int safeY = Math.max(surfaceY, level.getHeight(Heightmap.Types.MOTION_BLOCKING, blockX, blockZ) + 1);
|
CompletableFuture<Boolean> teleport = ModdedDimensionManager.teleportAsync(
|
||||||
player.teleportTo(level, blockX + 0.5D, safeY, blockZ + 0.5D, java.util.Set.of(), player.getYRot(), player.getXRot(), false);
|
player,
|
||||||
|
server,
|
||||||
|
level,
|
||||||
|
blockX + 0.5D,
|
||||||
|
surfaceY,
|
||||||
|
blockZ + 0.5D,
|
||||||
|
System.nanoTime() + TimeUnit.SECONDS.toNanos(10L));
|
||||||
|
UUID playerId = player.getUUID();
|
||||||
|
teleport.whenComplete((success, failure) -> {
|
||||||
|
if (failure != null) {
|
||||||
|
ModdedIrisLog.error("Iris Vision teleport failed for {} at {},{}",
|
||||||
|
playerId, blockX, blockZ, failure);
|
||||||
|
} else if (!Boolean.TRUE.equals(success)) {
|
||||||
|
ModdedIrisLog.warn("Iris Vision teleport did not complete for {} at {},{}",
|
||||||
|
playerId, blockX, blockZ);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+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 reinjection = method(source, "private static void reinjectPersistentDimensions(");
|
||||||
String failure = catchBlock(reinjection);
|
String failure = catchBlock(reinjection);
|
||||||
|
|
||||||
assertTrue(failure.contains("LOGGER.error("));
|
assertTrue(failure.contains("ModdedIrisLog.error("));
|
||||||
assertFalse(failure.contains("e.toString()"));
|
assertFalse(failure.contains("e.toString()"));
|
||||||
assertFalse(failure.contains("throw new IllegalStateException("));
|
assertFalse(failure.contains("throw new IllegalStateException("));
|
||||||
assertTrue(reinjection.contains("injected++;"));
|
assertTrue(reinjection.contains("injected++;"));
|
||||||
@@ -152,7 +152,7 @@ public class ModdedLifecycleFailureContractTest {
|
|||||||
assertBefore(stop, "\"world engines\"", "\"dimension manager\"");
|
assertBefore(stop, "\"world engines\"", "\"dimension manager\"");
|
||||||
assertBefore(stop, "\"server state\"", "if (failure != null)");
|
assertBefore(stop, "\"server state\"", "if (failure != null)");
|
||||||
assertFalse(stop.contains("throw"));
|
assertFalse(stop.contains("throw"));
|
||||||
assertTrue(stop.contains("LOGGER.error(\"Iris modded shutdown completed with failures\", failure);"));
|
assertTrue(stop.contains("ModdedIrisLog.error(\"Iris modded shutdown completed with failures\", failure);"));
|
||||||
|
|
||||||
String runStage = method(source, "private static Throwable runStopStage(");
|
String runStage = method(source, "private static Throwable runStopStage(");
|
||||||
assertTrue(runStage.contains("catch (Throwable stageFailure)"));
|
assertTrue(runStage.contains("catch (Throwable stageFailure)"));
|
||||||
@@ -207,21 +207,21 @@ public class ModdedLifecycleFailureContractTest {
|
|||||||
String bootstrapSource = source("ModdedEngineBootstrap.java");
|
String bootstrapSource = source("ModdedEngineBootstrap.java");
|
||||||
String unload = method(bootstrapSource, "public static void levelUnloaded(ServerLevel level)");
|
String unload = method(bootstrapSource, "public static void levelUnloaded(ServerLevel level)");
|
||||||
String failure = catchBlock(unload);
|
String failure = catchBlock(unload);
|
||||||
assertTrue(failure.contains("LOGGER.error("));
|
assertTrue(failure.contains("ModdedIrisLog.error("));
|
||||||
assertTrue(failure.contains("throw "));
|
assertTrue(failure.contains("throw "));
|
||||||
|
|
||||||
String managerSource = source("ModdedDimensionManager.java");
|
String managerSource = source("ModdedDimensionManager.java");
|
||||||
String remove = method(managerSource, "public static boolean remove(MinecraftServer server, String dimensionId, boolean wipeStorage)");
|
String remove = method(managerSource, "public static boolean remove(MinecraftServer server, String dimensionId, boolean wipeStorage)");
|
||||||
assertTrue(remove.contains("ModdedWorldEngines.evictOrThrow(level);"));
|
assertTrue(remove.contains("ModdedWorldEngines.evictOrThrow(level);"));
|
||||||
assertFalse(remove.contains("ModdedWorldEngines.evict(level);"));
|
assertFalse(remove.contains("ModdedWorldEngines.evict(level);"));
|
||||||
assertTrue(remove.contains("generatorUnbound = true;"));
|
assertTrue(remove.contains("unloadEventStarted = true;"));
|
||||||
assertTrue(remove.contains("rollbackRemoval(server, serverAccess, key, level, generator, generatorUnbound, e);"));
|
assertTrue(remove.contains("rollbackRemoval(server, serverAccess, key, level, generator, unloadEventStarted, e);"));
|
||||||
|
|
||||||
String rollback = method(managerSource, "private static void rollbackRemoval(");
|
String rollback = method(managerSource, "private static void rollbackRemoval(");
|
||||||
assertTrue(rollback.contains("serverAccess.hasLevel(server, key)"));
|
assertTrue(rollback.contains("serverAccess.hasLevel(server, key)"));
|
||||||
assertTrue(rollback.contains("generator.bindLevel(level);"));
|
assertTrue(rollback.contains("generator.bindLevel(level);"));
|
||||||
assertTrue(rollback.contains("failure.addSuppressed(rollbackFailure);"));
|
assertTrue(rollback.contains("failure.addSuppressed(rollbackFailure);"));
|
||||||
assertTrue(rollback.contains("LOGGER.error("));
|
assertTrue(rollback.contains("ModdedIrisLog.error("));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
+8
@@ -45,6 +45,14 @@ public class ModdedPlatformPathsTest {
|
|||||||
public void invalidateLevelCache(MinecraftServer server) {
|
public void invalidateLevelCache(MinecraftServer server) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void fireDynamicLevelLoad(MinecraftServer server, ServerLevel level) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void fireDynamicLevelUnload(MinecraftServer server, ServerLevel level) {
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean clientEnvironment() {
|
public boolean clientEnvironment() {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
+1
-1
@@ -186,7 +186,7 @@ public class IrisModdedStructureCommandTest {
|
|||||||
assertTrue(command.contains("ModdedLocateCommands.registeredStructureUnavailableMessage("));
|
assertTrue(command.contains("ModdedLocateCommands.registeredStructureUnavailableMessage("));
|
||||||
assertTrue(command.contains("engine.getData().getStructureLoader().getPossibleKeys()"));
|
assertTrue(command.contains("engine.getData().getStructureLoader().getPossibleKeys()"));
|
||||||
assertTrue(command.contains("IrisStructureLocator.hasLocatableEditablePlacement(engine, key)"));
|
assertTrue(command.contains("IrisStructureLocator.hasLocatableEditablePlacement(engine, key)"));
|
||||||
assertTrue(command.contains("LOGGER.info(\"[Iris goto unregistered] [{}] {} - {}\""));
|
assertTrue(command.contains("ModdedIrisLog.info(\"[Iris goto unregistered] [{}] {} - {}\""));
|
||||||
assertTrue(command.contains("UNREGISTERED(\"unregistered\")"));
|
assertTrue(command.contains("UNREGISTERED(\"unregistered\")"));
|
||||||
assertFalse(command.contains("DatapackIngestService"));
|
assertFalse(command.contains("DatapackIngestService"));
|
||||||
}
|
}
|
||||||
|
|||||||
+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.server.level.ServerPlayer;
|
||||||
import net.minecraft.world.level.block.state.BlockState;
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
import net.neoforged.neoforge.common.NeoForge;
|
import net.neoforged.neoforge.common.NeoForge;
|
||||||
|
import net.neoforged.neoforge.event.level.LevelEvent;
|
||||||
import net.neoforged.neoforge.event.level.block.BreakBlockEvent;
|
import net.neoforged.neoforge.event.level.block.BreakBlockEvent;
|
||||||
import net.neoforged.fml.ModList;
|
import net.neoforged.fml.ModList;
|
||||||
import net.neoforged.fml.loading.FMLEnvironment;
|
import net.neoforged.fml.loading.FMLEnvironment;
|
||||||
@@ -77,6 +78,16 @@ public final class NeoForgeModdedLoader implements ModdedLoader {
|
|||||||
server.markWorldsDirty();
|
server.markWorldsDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void fireDynamicLevelLoad(MinecraftServer server, ServerLevel level) {
|
||||||
|
NeoForge.EVENT_BUS.post(new LevelEvent.Load(level));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void fireDynamicLevelUnload(MinecraftServer server, ServerLevel level) {
|
||||||
|
NeoForge.EVENT_BUS.post(new LevelEvent.Unload(level));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean clientEnvironment() {
|
public boolean clientEnvironment() {
|
||||||
return FMLEnvironment.getDist().isClient();
|
return FMLEnvironment.getDist().isClient();
|
||||||
|
|||||||
+72
-4
@@ -4,6 +4,8 @@ import org.gradle.api.tasks.Copy
|
|||||||
import org.gradle.api.tasks.compile.JavaCompile
|
import org.gradle.api.tasks.compile.JavaCompile
|
||||||
import org.gradle.jvm.tasks.Jar
|
import org.gradle.jvm.tasks.Jar
|
||||||
import org.gradle.jvm.toolchain.JavaLanguageVersion
|
import org.gradle.jvm.toolchain.JavaLanguageVersion
|
||||||
|
import groovy.json.JsonOutput
|
||||||
|
import groovy.json.JsonSlurper
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||||
@@ -73,6 +75,7 @@ String bukkitArtifactName = irisArtifactName('CraftBukkit', bukkitMinecraftRange
|
|||||||
String fabricArtifactName = irisArtifactName('Fabric', "${minecraftVersion}+${loaderDisplayVersion(fabricLoaderVersion)}")
|
String fabricArtifactName = irisArtifactName('Fabric', "${minecraftVersion}+${loaderDisplayVersion(fabricLoaderVersion)}")
|
||||||
String forgeArtifactName = irisArtifactName('Forge', "${minecraftVersion}+${loaderDisplayVersion(forgeVersion)}")
|
String forgeArtifactName = irisArtifactName('Forge', "${minecraftVersion}+${loaderDisplayVersion(forgeVersion)}")
|
||||||
String neoForgeArtifactName = irisArtifactName('NeoForge', "${minecraftVersion}+${loaderDisplayVersion(neoForgeVersion)}")
|
String neoForgeArtifactName = irisArtifactName('NeoForge', "${minecraftVersion}+${loaderDisplayVersion(neoForgeVersion)}")
|
||||||
|
long maximumBukkitArtifactBytes = 7_000_000L
|
||||||
apply plugin: ApiGenerator
|
apply plugin: ApiGenerator
|
||||||
|
|
||||||
// Where `buildAll` drops the per-platform jars for a local test server. Use the approved sibling
|
// Where `buildAll` drops the per-platform jars for a local test server. Use the approved sibling
|
||||||
@@ -145,6 +148,16 @@ nmsBindings.each { key, value ->
|
|||||||
|
|
||||||
def included = configurations.create('included')
|
def included = configurations.create('included')
|
||||||
def jarJar = configurations.create('jarJar')
|
def jarJar = configurations.create('jarJar')
|
||||||
|
def bukkitLanguagesDirectory = layout.buildDirectory.dir('generated/bukkit-languages')
|
||||||
|
Set<String> sharedNonEnglishBukkitMessages = [
|
||||||
|
'iris.desktop.vision.fps',
|
||||||
|
'iris.desktop.vision.tiles',
|
||||||
|
'iris.bukkit.runtime.commandpack.message_2',
|
||||||
|
'iris.bukkit.commanddatapack.message_2',
|
||||||
|
'iris.runtime.chunk_job.bossbar.progress',
|
||||||
|
'iris.runtime.studio.action.progress',
|
||||||
|
'iris.runtime.chunk_job.action.progress'
|
||||||
|
] as Set
|
||||||
dependencies {
|
dependencies {
|
||||||
nmsBindings.keySet().each { key ->
|
nmsBindings.keySet().each { key ->
|
||||||
add('included', project(path: ":adapters:bukkit:nms:${key}", configuration: 'runtimeElements'))
|
add('included', project(path: ":adapters:bukkit:nms:${key}", configuration: 'runtimeElements'))
|
||||||
@@ -154,14 +167,66 @@ dependencies {
|
|||||||
add('jarJar', project(':core:agent'))
|
add('jarJar', project(':core:agent'))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def prepareBukkitLanguages = tasks.register('prepareBukkitLanguages') {
|
||||||
|
inputs.files(fileTree(project(':core').file('src/main/resources/languages')) {
|
||||||
|
include('*.json')
|
||||||
|
})
|
||||||
|
outputs.dir(bukkitLanguagesDirectory)
|
||||||
|
doLast {
|
||||||
|
File outputDirectory = bukkitLanguagesDirectory.get().asFile
|
||||||
|
delete(outputDirectory)
|
||||||
|
outputDirectory.mkdirs()
|
||||||
|
List<File> sources = fileTree(project(':core').file('src/main/resources/languages')) {
|
||||||
|
include('*.json')
|
||||||
|
}.files.sort { File left, File right -> left.name <=> right.name }
|
||||||
|
List<Map<String, Object>> catalogs = sources.collect { File source ->
|
||||||
|
(Map<String, Object>) new JsonSlurper().parse(source)
|
||||||
|
}
|
||||||
|
Map<String, Object> referenceMessages = (Map<String, Object>) catalogs.first().get('messages')
|
||||||
|
List<String> messageIds = referenceMessages.keySet()
|
||||||
|
.findAll { String key -> !key.startsWith('iris.modded.') }
|
||||||
|
.sort()
|
||||||
|
Set<String> sharedFallbackIds = new LinkedHashSet<>(messageIds)
|
||||||
|
catalogs.each { Map<String, Object> catalog ->
|
||||||
|
Map<String, Object> messages = (Map<String, Object>) catalog.get('messages')
|
||||||
|
if (!messages.keySet().containsAll(messageIds)) {
|
||||||
|
throw new GradleException("Bundled locale ${catalog.get('locale')} is missing Bukkit messages")
|
||||||
|
}
|
||||||
|
sharedFallbackIds.removeIf { String key -> messages.get(key) != referenceMessages.get(key) }
|
||||||
|
}
|
||||||
|
sharedFallbackIds.removeAll(sharedNonEnglishBukkitMessages)
|
||||||
|
|
||||||
|
sources.eachWithIndex { File source, int index ->
|
||||||
|
Map<String, Object> catalog = catalogs.get(index)
|
||||||
|
Map<String, Object> messages = (Map<String, Object>) catalog.get('messages')
|
||||||
|
List<Object> compactMessages = messageIds.collect { String key ->
|
||||||
|
sharedFallbackIds.contains(key) ? null : messages.get(key)
|
||||||
|
}
|
||||||
|
new File(outputDirectory, source.name).setText(
|
||||||
|
JsonOutput.toJson([catalog.get('locale'), compactMessages]),
|
||||||
|
'UTF-8')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tasks.named('jar', Jar).configure {
|
tasks.named('jar', Jar).configure {
|
||||||
inputs.files(included)
|
inputs.files(included)
|
||||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||||
from(jarJar, provider { included.resolve().collect { zipTree(it) } })
|
includeEmptyDirs = false
|
||||||
|
from(jarJar)
|
||||||
|
from(provider { included.resolve().collect { zipTree(it) } }) {
|
||||||
|
exclude('languages/**')
|
||||||
|
}
|
||||||
|
from(prepareBukkitLanguages) {
|
||||||
|
into('languages')
|
||||||
|
}
|
||||||
doFirst {
|
doFirst {
|
||||||
delete(layout.buildDirectory.file("libs/Iris-${project.version}.jar"))
|
delete(layout.buildDirectory.file("libs/Iris-${project.version}.jar"))
|
||||||
}
|
}
|
||||||
archiveFileName.set(bukkitArtifactName)
|
archiveFileName.set(bukkitArtifactName)
|
||||||
|
doLast {
|
||||||
|
JarCompactor.compact(archiveFile.get().asFile)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register('iris', Copy) {
|
tasks.register('iris', Copy) {
|
||||||
@@ -537,10 +602,8 @@ List<String> requiredBukkitArtifactEntries = [
|
|||||||
'art/arcane/iris/core/lifecycle/WorldLifecycleStaging.class',
|
'art/arcane/iris/core/lifecycle/WorldLifecycleStaging.class',
|
||||||
'art/arcane/iris/util/simd/VectorSimdKernels.class',
|
'art/arcane/iris/util/simd/VectorSimdKernels.class',
|
||||||
'art/arcane/iris/util/simd/VectorNoiseKernels2D.class',
|
'art/arcane/iris/util/simd/VectorNoiseKernels2D.class',
|
||||||
'art/arcane/iris/util/paralithic/functions/Function.class',
|
|
||||||
'art/arcane/iris/util/project/agent/Agent.class',
|
'art/arcane/iris/util/project/agent/Agent.class',
|
||||||
'art/arcane/iris/util/common/misc/getHardware.class',
|
'art/arcane/iris/util/common/misc/getHardware.class',
|
||||||
'art/arcane/iris/util/caffeine/cache/Caffeine.class',
|
|
||||||
'art/arcane/volmlib/util/io/JarScanner.class',
|
'art/arcane/volmlib/util/io/JarScanner.class',
|
||||||
'art/arcane/volmlib/util/director/runtime/DirectorRuntimeEngine.class',
|
'art/arcane/volmlib/util/director/runtime/DirectorRuntimeEngine.class',
|
||||||
'art/arcane/volmlib/util/mantle/io/Lz4IOWorkerCodecSupport.class',
|
'art/arcane/volmlib/util/mantle/io/Lz4IOWorkerCodecSupport.class',
|
||||||
@@ -553,7 +616,12 @@ tasks.register('verifyBukkitArtifact') {
|
|||||||
File artifact = layout.buildDirectory.file("libs/${bukkitArtifactName}").get().asFile
|
File artifact = layout.buildDirectory.file("libs/${bukkitArtifactName}").get().asFile
|
||||||
inputs.file(artifact)
|
inputs.file(artifact)
|
||||||
doLast {
|
doLast {
|
||||||
BukkitArtifactVerifier.verify(artifact, requiredBukkitArtifactEntries, 17, 400, 16)
|
BukkitArtifactVerifier.verify(
|
||||||
|
artifact,
|
||||||
|
requiredBukkitArtifactEntries,
|
||||||
|
17,
|
||||||
|
16,
|
||||||
|
maximumBukkitArtifactBytes)
|
||||||
logger.lifecycle("Verified ${artifact.name} packaging and class reference graph")
|
logger.lifecycle("Verified ${artifact.name} packaging and class reference graph")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,9 +21,7 @@ public final class BukkitArtifactVerifier {
|
|||||||
// class under one of these is a NoClassDefFoundError waiting for the right code path.
|
// class under one of these is a NoClassDefFoundError waiting for the right code path.
|
||||||
private static final List<String> SHIPPED_PREFIXES = List.of(
|
private static final List<String> SHIPPED_PREFIXES = List.of(
|
||||||
"art/arcane/iris/",
|
"art/arcane/iris/",
|
||||||
"art/arcane/volmlib/",
|
"art/arcane/volmlib/"
|
||||||
"com/google/gson/",
|
|
||||||
"com/googlecode/concurrentlinkedhashmap/"
|
|
||||||
);
|
);
|
||||||
// Relocation targets for the libraries slimjar downloads and relocates at runtime. Compiled
|
// Relocation targets for the libraries slimjar downloads and relocates at runtime. Compiled
|
||||||
// references to them are correct and the classes are correctly absent from the jar.
|
// references to them are correct and the classes are correctly absent from the jar.
|
||||||
@@ -38,9 +36,12 @@ public final class BukkitArtifactVerifier {
|
|||||||
"art/arcane/iris/util/aether/",
|
"art/arcane/iris/util/aether/",
|
||||||
"art/arcane/iris/util/guice/",
|
"art/arcane/iris/util/guice/",
|
||||||
"art/arcane/iris/util/dom4j/",
|
"art/arcane/iris/util/dom4j/",
|
||||||
"art/arcane/iris/util/jaxen/"
|
"art/arcane/iris/util/jaxen/",
|
||||||
|
"art/arcane/iris/util/gson/",
|
||||||
|
"art/arcane/iris/util/lru/",
|
||||||
|
"art/arcane/iris/util/caffeine/",
|
||||||
|
"art/arcane/iris/util/paralithic/"
|
||||||
);
|
);
|
||||||
private static final String CAFFEINE_CACHE_PACKAGE = "art/arcane/iris/util/caffeine/cache/";
|
|
||||||
private static final String MATTER_SLICE_PACKAGE = "art/arcane/volmlib/util/matter/slices/";
|
private static final String MATTER_SLICE_PACKAGE = "art/arcane/volmlib/util/matter/slices/";
|
||||||
private static final String LANGUAGE_DIRECTORY = "languages/";
|
private static final String LANGUAGE_DIRECTORY = "languages/";
|
||||||
|
|
||||||
@@ -48,10 +49,14 @@ public final class BukkitArtifactVerifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void verify(File artifact, List<String> requiredEntries, int minimumLocales,
|
public static void verify(File artifact, List<String> requiredEntries, int minimumLocales,
|
||||||
int minimumCaffeineFactories, int minimumMatterSlices) {
|
int minimumMatterSlices, long maximumArtifactBytes) {
|
||||||
if (!artifact.isFile()) {
|
if (!artifact.isFile()) {
|
||||||
throw new GradleException("Missing Bukkit Iris artifact: " + artifact.getAbsolutePath());
|
throw new GradleException("Missing Bukkit Iris artifact: " + artifact.getAbsolutePath());
|
||||||
}
|
}
|
||||||
|
if (artifact.length() > maximumArtifactBytes) {
|
||||||
|
throw new GradleException(artifact.getName() + " is " + artifact.length()
|
||||||
|
+ " bytes; Bukkit artifacts must not exceed " + maximumArtifactBytes + " bytes");
|
||||||
|
}
|
||||||
|
|
||||||
try (JarFile jar = new JarFile(artifact)) {
|
try (JarFile jar = new JarFile(artifact)) {
|
||||||
for (String requiredEntry : requiredEntries) {
|
for (String requiredEntry : requiredEntries) {
|
||||||
@@ -65,7 +70,6 @@ public final class BukkitArtifactVerifier {
|
|||||||
|
|
||||||
Set<String> shippedClasses = new LinkedHashSet<>();
|
Set<String> shippedClasses = new LinkedHashSet<>();
|
||||||
int locales = 0;
|
int locales = 0;
|
||||||
int caffeineFactories = 0;
|
|
||||||
int matterSlices = 0;
|
int matterSlices = 0;
|
||||||
Enumeration<JarEntry> entries = jar.entries();
|
Enumeration<JarEntry> entries = jar.entries();
|
||||||
while (entries.hasMoreElements()) {
|
while (entries.hasMoreElements()) {
|
||||||
@@ -84,9 +88,6 @@ public final class BukkitArtifactVerifier {
|
|||||||
|
|
||||||
String internalName = name.substring(0, name.length() - ".class".length());
|
String internalName = name.substring(0, name.length() - ".class".length());
|
||||||
shippedClasses.add(internalName);
|
shippedClasses.add(internalName);
|
||||||
if (isGeneratedCaffeineFactory(internalName)) {
|
|
||||||
caffeineFactories++;
|
|
||||||
}
|
|
||||||
if (internalName.startsWith(MATTER_SLICE_PACKAGE)) {
|
if (internalName.startsWith(MATTER_SLICE_PACKAGE)) {
|
||||||
matterSlices++;
|
matterSlices++;
|
||||||
}
|
}
|
||||||
@@ -96,14 +97,6 @@ public final class BukkitArtifactVerifier {
|
|||||||
throw new GradleException(artifact.getName() + " ships " + locales + " locale files; expected at least "
|
throw new GradleException(artifact.getName() + " ships " + locales + " locale files; expected at least "
|
||||||
+ minimumLocales);
|
+ minimumLocales);
|
||||||
}
|
}
|
||||||
// Caffeine picks its cache and node implementation with MethodHandles.Lookup.findClass on a
|
|
||||||
// name built from the builder's feature flags. Nothing references these statically, so only a
|
|
||||||
// population check can tell that an exclude or minimize() ate them.
|
|
||||||
if (caffeineFactories < minimumCaffeineFactories) {
|
|
||||||
throw new GradleException(artifact.getName() + " ships " + caffeineFactories
|
|
||||||
+ " generated Caffeine cache classes; expected at least " + minimumCaffeineFactories
|
|
||||||
+ ". Caffeine resolves these by name and cannot survive static pruning");
|
|
||||||
}
|
|
||||||
// Matter.read() resolves slice types from the canonical name stored in the payload.
|
// Matter.read() resolves slice types from the canonical name stored in the payload.
|
||||||
if (matterSlices < minimumMatterSlices) {
|
if (matterSlices < minimumMatterSlices) {
|
||||||
throw new GradleException(artifact.getName() + " ships " + matterSlices
|
throw new GradleException(artifact.getName() + " ships " + matterSlices
|
||||||
@@ -148,23 +141,6 @@ public final class BukkitArtifactVerifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isGeneratedCaffeineFactory(String internalName) {
|
|
||||||
if (!internalName.startsWith(CAFFEINE_CACHE_PACKAGE)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
String simpleName = internalName.substring(CAFFEINE_CACHE_PACKAGE.length());
|
|
||||||
if (simpleName.isEmpty() || simpleName.indexOf('/') >= 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
for (int i = 0; i < simpleName.length(); i++) {
|
|
||||||
if (simpleName.charAt(i) < 'A' || simpleName.charAt(i) > 'Z') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] readEntryBytes(JarFile jar, JarEntry entry) throws IOException {
|
private static byte[] readEntryBytes(JarFile jar, JarEntry entry) throws IOException {
|
||||||
try (InputStream input = jar.getInputStream(entry)) {
|
try (InputStream input = jar.getInputStream(entry)) {
|
||||||
return input.readAllBytes();
|
return input.readAllBytes();
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import org.gradle.api.GradleException;
|
||||||
|
|
||||||
|
import java.io.BufferedInputStream;
|
||||||
|
import java.io.BufferedOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.file.AtomicMoveNotSupportedException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipInputStream;
|
||||||
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
|
public final class JarCompactor {
|
||||||
|
private static final int BUFFER_BYTES = 64 * 1024;
|
||||||
|
|
||||||
|
private JarCompactor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void compact(File artifact) {
|
||||||
|
if (artifact == null || !artifact.isFile()) {
|
||||||
|
throw new GradleException("Cannot compact missing jar artifact: " + artifact);
|
||||||
|
}
|
||||||
|
|
||||||
|
Path source = artifact.toPath();
|
||||||
|
Path temporary = null;
|
||||||
|
try {
|
||||||
|
temporary = Files.createTempFile(source.getParent(), artifact.getName(), ".compact");
|
||||||
|
rewrite(source, temporary);
|
||||||
|
replace(temporary, source);
|
||||||
|
} catch (IOException exception) {
|
||||||
|
if (temporary != null) {
|
||||||
|
try {
|
||||||
|
Files.deleteIfExists(temporary);
|
||||||
|
} catch (IOException cleanupFailure) {
|
||||||
|
exception.addSuppressed(cleanupFailure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new GradleException("Unable to compact jar artifact " + artifact.getAbsolutePath(), exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void rewrite(Path source, Path destination) throws IOException {
|
||||||
|
byte[] buffer = new byte[BUFFER_BYTES];
|
||||||
|
try (InputStream rawInput = new BufferedInputStream(Files.newInputStream(source));
|
||||||
|
ZipInputStream input = new ZipInputStream(rawInput);
|
||||||
|
OutputStream rawOutput = new BufferedOutputStream(Files.newOutputStream(destination));
|
||||||
|
ZipOutputStream output = new ZipOutputStream(rawOutput)) {
|
||||||
|
output.setLevel(9);
|
||||||
|
ZipEntry entry;
|
||||||
|
while ((entry = input.getNextEntry()) != null) {
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ZipEntry compacted = copyMetadata(entry);
|
||||||
|
output.putNextEntry(compacted);
|
||||||
|
int read;
|
||||||
|
while ((read = input.read(buffer)) >= 0) {
|
||||||
|
if (read > 0) {
|
||||||
|
output.write(buffer, 0, read);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output.closeEntry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ZipEntry copyMetadata(ZipEntry source) {
|
||||||
|
ZipEntry target = new ZipEntry(source.getName());
|
||||||
|
target.setMethod(ZipEntry.DEFLATED);
|
||||||
|
if (source.getTime() >= 0L) {
|
||||||
|
target.setTime(source.getTime());
|
||||||
|
}
|
||||||
|
if (source.getComment() != null) {
|
||||||
|
target.setComment(source.getComment());
|
||||||
|
}
|
||||||
|
if (source.getExtra() != null) {
|
||||||
|
target.setExtra(source.getExtra());
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void replace(Path temporary, Path source) throws IOException {
|
||||||
|
try {
|
||||||
|
Files.move(
|
||||||
|
temporary,
|
||||||
|
source,
|
||||||
|
StandardCopyOption.ATOMIC_MOVE,
|
||||||
|
StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
} catch (AtomicMoveNotSupportedException exception) {
|
||||||
|
Files.move(temporary, source, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,7 +30,6 @@ public class BukkitArtifactVerifierTest {
|
|||||||
NMS_BINDING + ".class"
|
NMS_BINDING + ".class"
|
||||||
);
|
);
|
||||||
private static final int LOCALES = 2;
|
private static final int LOCALES = 2;
|
||||||
private static final int CAFFEINE_FACTORIES = 2;
|
|
||||||
private static final int MATTER_SLICES = 1;
|
private static final int MATTER_SLICES = 1;
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
@@ -40,7 +39,7 @@ public class BukkitArtifactVerifierTest {
|
|||||||
public void acceptsCompleteArtifact() throws Exception {
|
public void acceptsCompleteArtifact() throws Exception {
|
||||||
File artifact = createArtifact(validEntries());
|
File artifact = createArtifact(validEntries());
|
||||||
|
|
||||||
BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, CAFFEINE_FACTORIES, MATTER_SLICES);
|
BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, MATTER_SLICES, Long.MAX_VALUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -50,8 +49,7 @@ public class BukkitArtifactVerifierTest {
|
|||||||
File artifact = createArtifact(entries);
|
File artifact = createArtifact(entries);
|
||||||
|
|
||||||
GradleException failure = assertThrows(GradleException.class,
|
GradleException failure = assertThrows(GradleException.class,
|
||||||
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, CAFFEINE_FACTORIES,
|
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, MATTER_SLICES, Long.MAX_VALUE));
|
||||||
MATTER_SLICES));
|
|
||||||
assertTrue(failure.getMessage().contains(PLUGIN_DESCRIPTOR));
|
assertTrue(failure.getMessage().contains(PLUGIN_DESCRIPTOR));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,8 +60,7 @@ public class BukkitArtifactVerifierTest {
|
|||||||
File artifact = createArtifact(entries);
|
File artifact = createArtifact(entries);
|
||||||
|
|
||||||
GradleException failure = assertThrows(GradleException.class,
|
GradleException failure = assertThrows(GradleException.class,
|
||||||
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, CAFFEINE_FACTORIES,
|
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, MATTER_SLICES, Long.MAX_VALUE));
|
||||||
MATTER_SLICES));
|
|
||||||
assertTrue(failure.getMessage().contains(SLIMJAR_DEPENDENCIES));
|
assertTrue(failure.getMessage().contains(SLIMJAR_DEPENDENCIES));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,8 +71,7 @@ public class BukkitArtifactVerifierTest {
|
|||||||
File artifact = createArtifact(entries);
|
File artifact = createArtifact(entries);
|
||||||
|
|
||||||
GradleException failure = assertThrows(GradleException.class,
|
GradleException failure = assertThrows(GradleException.class,
|
||||||
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, CAFFEINE_FACTORIES,
|
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, MATTER_SLICES, Long.MAX_VALUE));
|
||||||
MATTER_SLICES));
|
|
||||||
assertTrue(failure.getMessage().contains(SLIMJAR_RESOLUTIONS));
|
assertTrue(failure.getMessage().contains(SLIMJAR_RESOLUTIONS));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,8 +82,7 @@ public class BukkitArtifactVerifierTest {
|
|||||||
File artifact = createArtifact(entries);
|
File artifact = createArtifact(entries);
|
||||||
|
|
||||||
GradleException failure = assertThrows(GradleException.class,
|
GradleException failure = assertThrows(GradleException.class,
|
||||||
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, CAFFEINE_FACTORIES,
|
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, MATTER_SLICES, Long.MAX_VALUE));
|
||||||
MATTER_SLICES));
|
|
||||||
assertTrue(failure.getMessage().contains("art/arcane/volmlib/util/noise/CNG"));
|
assertTrue(failure.getMessage().contains("art/arcane/volmlib/util/noise/CNG"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,21 +91,31 @@ public class BukkitArtifactVerifierTest {
|
|||||||
Map<String, byte[]> entries = validEntries();
|
Map<String, byte[]> entries = validEntries();
|
||||||
entries.put("art/arcane/iris/Consumer.class",
|
entries.put("art/arcane/iris/Consumer.class",
|
||||||
classReferencing("art/arcane/iris/Consumer", "art/arcane/iris/util/kyori/adventure/text/Component"));
|
classReferencing("art/arcane/iris/Consumer", "art/arcane/iris/util/kyori/adventure/text/Component"));
|
||||||
|
entries.put("art/arcane/iris/GsonConsumer.class",
|
||||||
|
classReferencing("art/arcane/iris/GsonConsumer", "art/arcane/iris/util/gson/Gson"));
|
||||||
|
entries.put("art/arcane/iris/LruConsumer.class",
|
||||||
|
classReferencing("art/arcane/iris/LruConsumer", "art/arcane/iris/util/lru/ConcurrentLinkedHashMap"));
|
||||||
|
entries.put("art/arcane/iris/CaffeineConsumer.class",
|
||||||
|
classReferencing("art/arcane/iris/CaffeineConsumer", "art/arcane/iris/util/caffeine/cache/Caffeine"));
|
||||||
|
entries.put("art/arcane/iris/ParalithicConsumer.class",
|
||||||
|
classReferencing("art/arcane/iris/ParalithicConsumer", "art/arcane/iris/util/paralithic/functions/Function"));
|
||||||
File artifact = createArtifact(entries);
|
File artifact = createArtifact(entries);
|
||||||
|
|
||||||
BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, CAFFEINE_FACTORIES, MATTER_SLICES);
|
BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, MATTER_SLICES, Long.MAX_VALUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void rejectsStrippedCaffeineFactories() throws Exception {
|
public void rejectsArtifactAboveConfiguredSize() throws Exception {
|
||||||
Map<String, byte[]> entries = validEntries();
|
File artifact = createArtifact(validEntries());
|
||||||
entries.remove("art/arcane/iris/util/caffeine/cache/SSMS.class");
|
|
||||||
File artifact = createArtifact(entries);
|
|
||||||
|
|
||||||
GradleException failure = assertThrows(GradleException.class,
|
GradleException failure = assertThrows(GradleException.class,
|
||||||
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, CAFFEINE_FACTORIES,
|
() -> BukkitArtifactVerifier.verify(
|
||||||
MATTER_SLICES));
|
artifact,
|
||||||
assertTrue(failure.getMessage().contains("generated Caffeine cache classes"));
|
REQUIRED_ENTRIES,
|
||||||
|
LOCALES,
|
||||||
|
MATTER_SLICES,
|
||||||
|
artifact.length() - 1L));
|
||||||
|
assertTrue(failure.getMessage().contains("must not exceed"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -120,8 +125,7 @@ public class BukkitArtifactVerifierTest {
|
|||||||
File artifact = createArtifact(entries);
|
File artifact = createArtifact(entries);
|
||||||
|
|
||||||
GradleException failure = assertThrows(GradleException.class,
|
GradleException failure = assertThrows(GradleException.class,
|
||||||
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, CAFFEINE_FACTORIES,
|
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, MATTER_SLICES, Long.MAX_VALUE));
|
||||||
MATTER_SLICES));
|
|
||||||
assertTrue(failure.getMessage().contains("locale files"));
|
assertTrue(failure.getMessage().contains("locale files"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,8 +136,7 @@ public class BukkitArtifactVerifierTest {
|
|||||||
File artifact = createArtifact(entries);
|
File artifact = createArtifact(entries);
|
||||||
|
|
||||||
GradleException failure = assertThrows(GradleException.class,
|
GradleException failure = assertThrows(GradleException.class,
|
||||||
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, CAFFEINE_FACTORIES,
|
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, MATTER_SLICES, Long.MAX_VALUE));
|
||||||
MATTER_SLICES));
|
|
||||||
assertTrue(failure.getMessage().contains("Matter slice types"));
|
assertTrue(failure.getMessage().contains("Matter slice types"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,7 +147,7 @@ public class BukkitArtifactVerifierTest {
|
|||||||
classAnnotatedWith("art/arcane/iris/Annotated", "com/google/errorprone/annotations/CanIgnoreReturnValue"));
|
classAnnotatedWith("art/arcane/iris/Annotated", "com/google/errorprone/annotations/CanIgnoreReturnValue"));
|
||||||
File artifact = createArtifact(entries);
|
File artifact = createArtifact(entries);
|
||||||
|
|
||||||
BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, CAFFEINE_FACTORIES, MATTER_SLICES);
|
BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, MATTER_SLICES, Long.MAX_VALUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, byte[]> validEntries() {
|
private Map<String, byte[]> validEntries() {
|
||||||
@@ -159,10 +162,6 @@ public class BukkitArtifactVerifierTest {
|
|||||||
entries.put("art/arcane/volmlib/util/noise/CNG.class", emptyClass("art/arcane/volmlib/util/noise/CNG"));
|
entries.put("art/arcane/volmlib/util/noise/CNG.class", emptyClass("art/arcane/volmlib/util/noise/CNG"));
|
||||||
entries.put("art/arcane/volmlib/util/matter/slices/BlockMatter.class",
|
entries.put("art/arcane/volmlib/util/matter/slices/BlockMatter.class",
|
||||||
emptyClass("art/arcane/volmlib/util/matter/slices/BlockMatter"));
|
emptyClass("art/arcane/volmlib/util/matter/slices/BlockMatter"));
|
||||||
entries.put("art/arcane/iris/util/caffeine/cache/SSMS.class",
|
|
||||||
emptyClass("art/arcane/iris/util/caffeine/cache/SSMS"));
|
|
||||||
entries.put("art/arcane/iris/util/caffeine/cache/SSLMS.class",
|
|
||||||
emptyClass("art/arcane/iris/util/caffeine/cache/SSLMS"));
|
|
||||||
return entries;
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.jar.JarEntry;
|
||||||
|
import java.util.jar.JarFile;
|
||||||
|
import java.util.jar.JarOutputStream;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertArrayEquals;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
|
||||||
|
public class JarCompactorTest {
|
||||||
|
@Rule
|
||||||
|
public final TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void preservesFilesAndOmitsDirectoryEntries() throws Exception {
|
||||||
|
File artifact = temporaryFolder.newFile("artifact.jar");
|
||||||
|
byte[] content = "Iris artifact content".getBytes(StandardCharsets.UTF_8);
|
||||||
|
try (JarOutputStream output = new JarOutputStream(new FileOutputStream(artifact))) {
|
||||||
|
output.putNextEntry(new JarEntry("example/"));
|
||||||
|
output.closeEntry();
|
||||||
|
output.putNextEntry(new JarEntry("example/value.txt"));
|
||||||
|
output.write(content);
|
||||||
|
output.closeEntry();
|
||||||
|
}
|
||||||
|
|
||||||
|
JarCompactor.compact(artifact);
|
||||||
|
|
||||||
|
try (JarFile jar = new JarFile(artifact)) {
|
||||||
|
assertNull(jar.getJarEntry("example/"));
|
||||||
|
assertArrayEquals(content, jar.getInputStream(
|
||||||
|
jar.getJarEntry("example/value.txt")).readAllBytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+148
-6
@@ -86,12 +86,11 @@ dependencies {
|
|||||||
implementation(volmLibCoordinate) {
|
implementation(volmLibCoordinate) {
|
||||||
transitive = false
|
transitive = false
|
||||||
}
|
}
|
||||||
implementation(libs.gson)
|
|
||||||
implementation(libs.lru)
|
|
||||||
implementation(libs.caffeine)
|
|
||||||
implementation(libs.paralithic)
|
|
||||||
|
|
||||||
// Dynamically Loaded
|
// Dynamically Loaded
|
||||||
|
slim(libs.gson)
|
||||||
|
slim(libs.lru)
|
||||||
|
slim(libs.caffeine)
|
||||||
|
slim(libs.paralithic)
|
||||||
slim(libs.paperlib)
|
slim(libs.paperlib)
|
||||||
slim(libs.adventure.api)
|
slim(libs.adventure.api)
|
||||||
slim(libs.adventure.minimessage)
|
slim(libs.adventure.minimessage)
|
||||||
@@ -154,6 +153,8 @@ slimJar {
|
|||||||
]
|
]
|
||||||
|
|
||||||
relocate('com.dfsek.paralithic', "${lib}.paralithic")
|
relocate('com.dfsek.paralithic', "${lib}.paralithic")
|
||||||
|
relocate('com.google.gson', "${lib}.gson")
|
||||||
|
relocate('com.googlecode.concurrentlinkedhashmap', "${lib}.lru")
|
||||||
relocate('io.papermc.lib', "${lib}.paper")
|
relocate('io.papermc.lib', "${lib}.paper")
|
||||||
relocate('net.kyori', "${lib}.kyori")
|
relocate('net.kyori', "${lib}.kyori")
|
||||||
relocate('org.bstats', "${lib}.metrics")
|
relocate('org.bstats', "${lib}.metrics")
|
||||||
@@ -285,9 +286,149 @@ List<String> supersededVolmLibPackages = [
|
|||||||
'art/arcane/volmlib/util/director/visual/**',
|
'art/arcane/volmlib/util/director/visual/**',
|
||||||
'art/arcane/volmlib/util/value/**',
|
'art/arcane/volmlib/util/value/**',
|
||||||
'art/arcane/volmlib/util/api/**',
|
'art/arcane/volmlib/util/api/**',
|
||||||
'art/arcane/volmlib/util/entity/**'
|
'art/arcane/volmlib/util/entity/**',
|
||||||
|
'art/arcane/volmlib/util/config/**',
|
||||||
|
'art/arcane/volmlib/util/reflect/**',
|
||||||
|
'art/arcane/volmlib/util/documentation/**',
|
||||||
|
'art/arcane/iris/core/report/**',
|
||||||
|
'art/arcane/iris/util/common/inventorygui/**',
|
||||||
|
'art/arcane/iris/util/common/board/**',
|
||||||
|
'art/arcane/volmlib/integration/VaultEconomy*.class'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
List<String> unusedVolmLibClasses = [
|
||||||
|
'art/arcane/volmlib/util/nbt/io/ParseException',
|
||||||
|
'art/arcane/volmlib/util/nbt/io/SNBTDeserializer',
|
||||||
|
'art/arcane/volmlib/util/nbt/io/SNBTParser',
|
||||||
|
'art/arcane/volmlib/util/nbt/io/SNBTSerializer',
|
||||||
|
'art/arcane/volmlib/util/nbt/io/SNBTUtil',
|
||||||
|
'art/arcane/volmlib/util/nbt/io/SNBTWriter',
|
||||||
|
'art/arcane/volmlib/util/nbt/io/StringPointer',
|
||||||
|
'art/arcane/volmlib/util/io/StringDeserializer',
|
||||||
|
'art/arcane/volmlib/util/io/StringSerializer',
|
||||||
|
'art/arcane/volmlib/util/json/EnumType',
|
||||||
|
'art/arcane/volmlib/util/json/HTTP',
|
||||||
|
'art/arcane/volmlib/util/json/HTTPTokener',
|
||||||
|
'art/arcane/volmlib/util/json/JSONML',
|
||||||
|
'art/arcane/volmlib/util/json/JSONStringer',
|
||||||
|
'art/arcane/volmlib/util/json/JSONWriter',
|
||||||
|
'art/arcane/volmlib/util/json/XML',
|
||||||
|
'art/arcane/volmlib/util/json/XMLTokener',
|
||||||
|
'art/arcane/volmlib/util/hud/HudBid',
|
||||||
|
'art/arcane/volmlib/util/hud/HudBidder',
|
||||||
|
'art/arcane/volmlib/util/hud/HudLocalLedger',
|
||||||
|
'art/arcane/volmlib/util/hud/HudTitleClaim',
|
||||||
|
'art/arcane/volmlib/util/hud/HudTitleService',
|
||||||
|
'art/arcane/volmlib/util/scheduling/GroupedExecutor',
|
||||||
|
'art/arcane/volmlib/util/scheduling/SchedulerRuntime',
|
||||||
|
'art/arcane/volmlib/util/scheduling/SchedulerUtils',
|
||||||
|
'art/arcane/volmlib/util/scheduling/TaskExecutor',
|
||||||
|
'art/arcane/volmlib/util/parallel/StreamUtilsSupport',
|
||||||
|
'art/arcane/volmlib/util/parallel/SyncExecutorSupport',
|
||||||
|
'art/arcane/volmlib/util/cache/ByteBitCache',
|
||||||
|
'art/arcane/volmlib/util/cache/DataBitCache',
|
||||||
|
'art/arcane/volmlib/util/cache/FloatBitCache',
|
||||||
|
'art/arcane/volmlib/util/cache/IntBitCache',
|
||||||
|
'art/arcane/volmlib/util/cache/ShortBitCache',
|
||||||
|
'art/arcane/volmlib/util/cache/UByteBitCache',
|
||||||
|
'art/arcane/volmlib/util/data/ChunkCache',
|
||||||
|
'art/arcane/volmlib/util/data/ComplexCache',
|
||||||
|
'art/arcane/volmlib/util/data/IrisBiomeStorage',
|
||||||
|
'art/arcane/volmlib/util/data/NibbleArray',
|
||||||
|
'art/arcane/volmlib/util/data/NibbleDataPalette',
|
||||||
|
'art/arcane/volmlib/util/data/Recycler',
|
||||||
|
'art/arcane/volmlib/util/data/VanillaBiomeMap',
|
||||||
|
'art/arcane/volmlib/util/data/Writable',
|
||||||
|
'art/arcane/volmlib/util/data/base/ComplexCacheBase',
|
||||||
|
'art/arcane/volmlib/util/director/DirectorNodeBase',
|
||||||
|
'art/arcane/volmlib/util/director/DirectorParameterBase',
|
||||||
|
'art/arcane/volmlib/util/director/theme/DirectorThemes',
|
||||||
|
'art/arcane/volmlib/util/interpolation/InterpolationMethod',
|
||||||
|
'art/arcane/volmlib/util/interpolation/IrisInterpolation',
|
||||||
|
'art/arcane/volmlib/util/math/INode',
|
||||||
|
'art/arcane/volmlib/util/math/IrisMathHelper',
|
||||||
|
'art/arcane/volmlib/util/math/KochanekBartelsInterpolation',
|
||||||
|
'art/arcane/volmlib/util/math/PathInterpolation',
|
||||||
|
'art/arcane/volmlib/util/math/Spiral',
|
||||||
|
'art/arcane/volmlib/util/board/BoardManager',
|
||||||
|
'art/arcane/volmlib/util/board/BoardUpdateTask',
|
||||||
|
'art/arcane/volmlib/util/bukkit/Placeholders',
|
||||||
|
'art/arcane/volmlib/util/hunk/HunkFactory',
|
||||||
|
'art/arcane/volmlib/util/hunk/storage/ArrayHunk',
|
||||||
|
'art/arcane/volmlib/util/hunk/storage/AtomicDoubleHunk',
|
||||||
|
'art/arcane/volmlib/util/hunk/storage/AtomicIntegerHunk',
|
||||||
|
'art/arcane/volmlib/util/hunk/storage/AtomicLongHunk',
|
||||||
|
'art/arcane/volmlib/util/hunk/storage/SynchronizedArrayHunk',
|
||||||
|
'art/arcane/volmlib/util/hunk/view/BiomeForceBridge',
|
||||||
|
'art/arcane/volmlib/util/hunk/view/BiomeGridForceSupport',
|
||||||
|
'art/arcane/volmlib/util/hunk/view/BiomeGridHunkHolder',
|
||||||
|
'art/arcane/volmlib/util/hunk/view/BiomeGridHunkView',
|
||||||
|
'art/arcane/volmlib/util/hunk/view/ChunkDataHunkHolder',
|
||||||
|
'art/arcane/volmlib/util/hunk/view/RotatedXHunkView',
|
||||||
|
'art/arcane/volmlib/util/hunk/view/RotatedYHunkView',
|
||||||
|
'art/arcane/volmlib/util/hunk/view/RotatedZHunkView',
|
||||||
|
'art/arcane/volmlib/util/exceptions/MissingDimensionException',
|
||||||
|
'art/arcane/volmlib/util/function/Supplier2',
|
||||||
|
'art/arcane/volmlib/util/function/Supplier3',
|
||||||
|
'art/arcane/volmlib/util/hud/HudPriority',
|
||||||
|
'art/arcane/volmlib/util/interpolation/InterpolationMethod3D',
|
||||||
|
'art/arcane/volmlib/util/interpolation/InterpolationType',
|
||||||
|
'art/arcane/volmlib/util/inventorygui/UIRainbowDecorator',
|
||||||
|
'art/arcane/volmlib/util/io/Converter',
|
||||||
|
'art/arcane/volmlib/util/matter/MatterPlacer',
|
||||||
|
'art/arcane/volmlib/util/plugin/SplashScreenSupport',
|
||||||
|
'art/arcane/volmlib/util/scheduling/Contained',
|
||||||
|
'art/arcane/volmlib/util/scheduling/IrisLock',
|
||||||
|
'art/arcane/volmlib/util/scheduling/QueueExecutor',
|
||||||
|
'art/arcane/volmlib/util/scheduling/S',
|
||||||
|
'art/arcane/volmlib/util/scheduling/SBase',
|
||||||
|
'art/arcane/volmlib/util/scheduling/SlidingWindowRateLimiter',
|
||||||
|
'art/arcane/volmlib/util/scheduling/Switch',
|
||||||
|
'art/arcane/volmlib/util/scheduling/ThreadMonitor',
|
||||||
|
'art/arcane/volmlib/util/cache/ByteCache',
|
||||||
|
'art/arcane/volmlib/util/cache/ChunkCache2DSimple',
|
||||||
|
'art/arcane/volmlib/util/cache/IntCache',
|
||||||
|
'art/arcane/volmlib/util/cache/ShortCache',
|
||||||
|
'art/arcane/volmlib/util/cache/UByteCache',
|
||||||
|
'art/arcane/volmlib/util/cache/WorldCache2DSimple',
|
||||||
|
'art/arcane/volmlib/util/data/CuboidException',
|
||||||
|
'art/arcane/volmlib/util/data/DUTF',
|
||||||
|
'art/arcane/volmlib/util/data/Heafty',
|
||||||
|
'art/arcane/volmlib/util/data/InvertedBiomeGrid',
|
||||||
|
'art/arcane/volmlib/util/data/Shrinkwrap',
|
||||||
|
'art/arcane/volmlib/util/data/WeightMap',
|
||||||
|
'art/arcane/volmlib/util/data/WeightedRandom',
|
||||||
|
'art/arcane/volmlib/util/director/compat/BukkitDirectorContext',
|
||||||
|
'art/arcane/volmlib/util/director/handlers/base/DummyHandlerBase',
|
||||||
|
'art/arcane/volmlib/util/director/handlers/base/OptionalWorldHandlerBase',
|
||||||
|
'art/arcane/volmlib/util/parallel/BurstedHunk',
|
||||||
|
'art/arcane/volmlib/util/parallel/GridLockSupport',
|
||||||
|
'art/arcane/volmlib/util/parallel/NoopGridLockSupport',
|
||||||
|
'art/arcane/volmlib/util/bukkit/ChunkPositionSet',
|
||||||
|
'art/arcane/volmlib/util/bukkit/Events',
|
||||||
|
'art/arcane/volmlib/util/json/SingleCollectionTypeFactory',
|
||||||
|
'art/arcane/volmlib/util/collection/StateList',
|
||||||
|
'art/arcane/iris/core/pack/IrisPack',
|
||||||
|
'art/arcane/iris/core/pack/IrisPackRepository',
|
||||||
|
'art/arcane/iris/core/jobs/DownloadJob',
|
||||||
|
'art/arcane/iris/core/jobs/JobCollection',
|
||||||
|
'art/arcane/iris/core/jobs/ParallelQueueJob',
|
||||||
|
'art/arcane/iris/core/jobs/QueueJob',
|
||||||
|
'art/arcane/iris/engine/framework/placer/WorldObjectPlacer',
|
||||||
|
'art/arcane/iris/engine/object/IPostBlockAccess',
|
||||||
|
'art/arcane/iris/engine/object/IrisPotionEffect',
|
||||||
|
'art/arcane/iris/engine/platform/DummyChunkGenerator',
|
||||||
|
'art/arcane/iris/util/common/math/VectorMath',
|
||||||
|
'art/arcane/iris/util/common/parallel/GridLock',
|
||||||
|
'art/arcane/iris/util/common/reflect/W',
|
||||||
|
'art/arcane/iris/util/slimjar/BuildConstants',
|
||||||
|
'art/arcane/iris/util/slimjar/relocation/meta/AttributeMetaMediator',
|
||||||
|
'art/arcane/iris/util/slimjar/relocation/meta/AttributeMetaMediatorFactory'
|
||||||
|
]
|
||||||
|
List<String> unusedVolmLibClassEntries = unusedVolmLibClasses.collectMany { String classPath ->
|
||||||
|
[classPath + '.class', classPath + '$*.class']
|
||||||
|
}
|
||||||
|
|
||||||
// Annotation-only artifacts pulled in transitively by Gson and Caffeine. Their types appear solely
|
// Annotation-only artifacts pulled in transitively by Gson and Caffeine. Their types appear solely
|
||||||
// in annotation attributes, which the JVM skips silently when the type is absent, so nothing loads
|
// in annotation attributes, which the JVM skips silently when the type is absent, so nothing loads
|
||||||
// or links against them at runtime.
|
// or links against them at runtime.
|
||||||
@@ -315,6 +456,7 @@ tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.Shadow
|
|||||||
relocate('io.github.slimjar', "${lib}.slimjar")
|
relocate('io.github.slimjar', "${lib}.slimjar")
|
||||||
exclude('modules/loader-agent.isolated-jar')
|
exclude('modules/loader-agent.isolated-jar')
|
||||||
exclude(supersededVolmLibPackages)
|
exclude(supersededVolmLibPackages)
|
||||||
|
exclude(unusedVolmLibClassEntries)
|
||||||
exclude(annotationOnlyArtifacts)
|
exclude(annotationOnlyArtifacts)
|
||||||
exclude(dependencyBuildMetadata)
|
exclude(dependencyBuildMetadata)
|
||||||
from(embeddedAgentJar.map { it.archiveFile }) {
|
from(embeddedAgentJar.map { it.archiveFile }) {
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ import art.arcane.iris.engine.object.IrisBiomeCustom;
|
|||||||
import art.arcane.iris.engine.object.IrisDimension;
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
import art.arcane.volmlib.util.collection.KList;
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
import art.arcane.volmlib.util.collection.KSet;
|
import art.arcane.volmlib.util.collection.KSet;
|
||||||
import art.arcane.iris.util.common.format.C;
|
|
||||||
import art.arcane.iris.util.common.misc.ServerProperties;
|
import art.arcane.iris.util.common.misc.ServerProperties;
|
||||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||||
import art.arcane.iris.util.common.scheduling.J;
|
import art.arcane.iris.util.common.scheduling.J;
|
||||||
@@ -90,6 +89,8 @@ public class ServerConfigurator {
|
|||||||
private static final Object DATAPACK_INSTALL_LOCK = new Object();
|
private static final Object DATAPACK_INSTALL_LOCK = new Object();
|
||||||
private static final String CODE_WORKSPACE_SUFFIX = ".code-workspace";
|
private static final String CODE_WORKSPACE_SUFFIX = ".code-workspace";
|
||||||
private static final String COMPILER_INPUT_FINGERPRINT_CACHE = "datapack-compiler-input-fingerprint";
|
private static final String COMPILER_INPUT_FINGERPRINT_CACHE = "datapack-compiler-input-fingerprint";
|
||||||
|
static final String POST_COMPILE_RESTART_WARNING = "Iris installed updated datapack registry entries; "
|
||||||
|
+ "restart the server before creating worlds or opening Studios.";
|
||||||
private static final int FINGERPRINT_BUFFER_BYTES = 64 * 1024;
|
private static final int FINGERPRINT_BUFFER_BYTES = 64 * 1024;
|
||||||
private static volatile boolean loadedDatapackRuntimeReady;
|
private static volatile boolean loadedDatapackRuntimeReady;
|
||||||
private static volatile String loadedDatapackCompilerInputFingerprint = "";
|
private static volatile String loadedDatapackCompilerInputFingerprint = "";
|
||||||
@@ -419,13 +420,18 @@ public class ServerConfigurator {
|
|||||||
&& !reusableRuntimeFingerprint(
|
&& !reusableRuntimeFingerprint(
|
||||||
loadedDatapackCompilerInputFingerprint,
|
loadedDatapackCompilerInputFingerprint,
|
||||||
current);
|
current);
|
||||||
|
boolean loadedRegistryRestartRequired = fullInstall
|
||||||
|
&& loadedCompilerInputsChanged
|
||||||
|
&& currentRegistryRequiresRestart();
|
||||||
if (!current.isEmpty() && current.equals(cached)) {
|
if (!current.isEmpty() && current.equals(cached)) {
|
||||||
IrisLogging.debug("Data packs unchanged, skipping install.");
|
IrisLogging.debug("Data packs unchanged, skipping install.");
|
||||||
DatapackInstallResult result = fullInstall && loadedCompilerInputsChanged
|
DatapackInstallResult result = loadedRegistryRestartRequired
|
||||||
? DatapackInstallResult.restartRequiredResult()
|
? DatapackInstallResult.restartRequiredResult()
|
||||||
: resultForUnchangedFingerprint(fullInstall, reapply);
|
: resultForUnchangedFingerprint(fullInstall, reapply);
|
||||||
if (result.restartRequired()) {
|
if (result.restartRequired()) {
|
||||||
requireDatapackRestart();
|
requireDatapackRestart();
|
||||||
|
} else if (result.succeeded()) {
|
||||||
|
loadedDatapackCompilerInputFingerprint = current;
|
||||||
}
|
}
|
||||||
reportTiming(timingConsumer, "datapack_install_if_changed_total", totalStart);
|
reportTiming(timingConsumer, "datapack_install_if_changed_total", totalStart);
|
||||||
return result;
|
return result;
|
||||||
@@ -435,7 +441,7 @@ public class ServerConfigurator {
|
|||||||
resolveDataFixer(),
|
resolveDataFixer(),
|
||||||
fullInstall,
|
fullInstall,
|
||||||
reapply);
|
reapply);
|
||||||
if (fullInstall && loadedCompilerInputsChanged && result.succeeded()) {
|
if (loadedRegistryRestartRequired && result.succeeded()) {
|
||||||
result = DatapackInstallResult.restartRequiredResult();
|
result = DatapackInstallResult.restartRequiredResult();
|
||||||
}
|
}
|
||||||
if (result.restartRequired()) {
|
if (result.restartRequired()) {
|
||||||
@@ -444,6 +450,7 @@ public class ServerConfigurator {
|
|||||||
reportTiming(timingConsumer, "datapack_compile_publish", compileStart);
|
reportTiming(timingConsumer, "datapack_compile_publish", compileStart);
|
||||||
if (result.succeeded() && !result.restartRequired()) {
|
if (result.succeeded() && !result.restartRequired()) {
|
||||||
writeCompilerInputFingerprintCache(cacheFile.toPath(), current);
|
writeCompilerInputFingerprintCache(cacheFile.toPath(), current);
|
||||||
|
loadedDatapackCompilerInputFingerprint = current;
|
||||||
}
|
}
|
||||||
reportTiming(timingConsumer, "datapack_install_if_changed_total", totalStart);
|
reportTiming(timingConsumer, "datapack_install_if_changed_total", totalStart);
|
||||||
return result;
|
return result;
|
||||||
@@ -456,6 +463,22 @@ public class ServerConfigurator {
|
|||||||
IrisWorldStorage.levelRoot().toPath());
|
IrisWorldStorage.levelRoot().toPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean currentRegistryRequiresRestart() {
|
||||||
|
try {
|
||||||
|
Map<String, String> currentRequirements = IrisDatapackCompiler.computeRegistryRequirements(
|
||||||
|
collectCompilerPackRoots(),
|
||||||
|
resolveDataFixer());
|
||||||
|
return runtimeRequiresRegistryRestart(
|
||||||
|
loadedDatapackRegistryRequirements,
|
||||||
|
currentRequirements);
|
||||||
|
} catch (IOException | RuntimeException exception) {
|
||||||
|
IrisLogging.reportError(
|
||||||
|
"Unable to compare loaded Iris datapack registry requirements.",
|
||||||
|
exception);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static List<IrisGeneratorBinding> collectConfiguredLevelStemBindings() throws IOException {
|
private static List<IrisGeneratorBinding> collectConfiguredLevelStemBindings() throws IOException {
|
||||||
File levelRoot = IrisWorldStorage.levelRoot();
|
File levelRoot = IrisWorldStorage.levelRoot();
|
||||||
String levelId = levelRoot.getName();
|
String levelId = levelRoot.getName();
|
||||||
@@ -545,6 +568,19 @@ public class ServerConfigurator {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static boolean runtimeRequiresRegistryRestart(
|
||||||
|
Map<String, String> loadedRequirements,
|
||||||
|
Map<String, String> currentRequirements
|
||||||
|
) {
|
||||||
|
if (currentRequirements == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (currentRequirements.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !loadedRegistrySatisfies(loadedRequirements, currentRequirements);
|
||||||
|
}
|
||||||
|
|
||||||
static boolean reusableRuntimeFingerprint(String loadedFingerprint, String currentFingerprint) {
|
static boolean reusableRuntimeFingerprint(String loadedFingerprint, String currentFingerprint) {
|
||||||
return loadedFingerprint != null
|
return loadedFingerprint != null
|
||||||
&& !loadedFingerprint.isBlank()
|
&& !loadedFingerprint.isBlank()
|
||||||
@@ -961,14 +997,19 @@ public class ServerConfigurator {
|
|||||||
|
|
||||||
private static boolean verifyDataPacksPost() {
|
private static boolean verifyDataPacksPost() {
|
||||||
try (Stream<IrisData> stream = allPacks()) {
|
try (Stream<IrisData> stream = allPacks()) {
|
||||||
boolean bad = stream
|
return verifyDataPacksPost(stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean verifyDataPacksPost(Stream<IrisData> packs) {
|
||||||
|
boolean bad = Objects.requireNonNull(packs, "Iris packs")
|
||||||
.map(data -> {
|
.map(data -> {
|
||||||
IrisLogging.debug("Checking Pack: " + data.getDataFolder().getPath());
|
IrisLogging.debug("Checking Pack: " + data.getDataFolder().getPath());
|
||||||
ResourceLoader<IrisDimension> loader = data.getDimensionLoader();
|
ResourceLoader<IrisDimension> loader = data.getDimensionLoader();
|
||||||
return loader.loadAll(loader.getPossibleKeys())
|
return loader.loadAll(loader.getPossibleKeys())
|
||||||
.stream()
|
.stream()
|
||||||
.filter(Objects::nonNull)
|
.filter(Objects::nonNull)
|
||||||
.map(ServerConfigurator::verifyDataPackInstalled)
|
.map(dimension -> verifyDataPackInstalled(dimension, false))
|
||||||
.toList()
|
.toList()
|
||||||
.contains(false);
|
.contains(false);
|
||||||
})
|
})
|
||||||
@@ -977,15 +1018,8 @@ public class ServerConfigurator {
|
|||||||
if (!bad) {
|
if (!bad) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (INMS.get().supportsDataPacks()) {
|
if (INMS.get().supportsDataPacks()) {
|
||||||
// Three sentences, no rules: a separator carries the record's severity too, so a box drawn
|
IrisLogging.warn(POST_COMPILE_RESTART_WARNING);
|
||||||
// out of equals signs became three more [SEVERE] lines saying nothing.
|
|
||||||
IrisLogging.error(C.ITALIC + "You need to restart your server to properly generate custom biomes.");
|
|
||||||
IrisLogging.error(C.ITALIC + "By continuing, Iris will use backup biomes in place of the custom biomes.");
|
|
||||||
IrisLogging.error(C.UNDERLINE + "Restart the server before generating.");
|
|
||||||
|
|
||||||
for (Player i : Bukkit.getOnlinePlayers()) {
|
for (Player i : Bukkit.getOnlinePlayers()) {
|
||||||
if (i.isOp() || i.hasPermission("iris.all")) {
|
if (i.isOp() || i.hasPermission("iris.all")) {
|
||||||
@@ -1054,6 +1088,10 @@ public class ServerConfigurator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static boolean verifyDataPackInstalled(IrisDimension dimension) {
|
public static boolean verifyDataPackInstalled(IrisDimension dimension) {
|
||||||
|
return verifyDataPackInstalled(dimension, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean verifyDataPackInstalled(IrisDimension dimension, boolean reportRuntimeFailure) {
|
||||||
KSet<String> keys = new KSet<>();
|
KSet<String> keys = new KSet<>();
|
||||||
boolean warn = false;
|
boolean warn = false;
|
||||||
|
|
||||||
@@ -1082,17 +1120,21 @@ public class ServerConfigurator {
|
|||||||
Object o = INMS.get().getCustomBiomeBaseFor(i);
|
Object o = INMS.get().getCustomBiomeBaseFor(i);
|
||||||
|
|
||||||
if (o == null) {
|
if (o == null) {
|
||||||
|
if (reportRuntimeFailure) {
|
||||||
IrisLogging.warn("The Biome " + i + " is not registered on the server.");
|
IrisLogging.warn("The Biome " + i + " is not registered on the server.");
|
||||||
|
}
|
||||||
warn = true;
|
warn = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (INMS.get().missingDimensionTypes(dimension.getDimensionTypeKey())) {
|
if (INMS.get().missingDimensionTypes(dimension.getDimensionTypeKey())) {
|
||||||
|
if (reportRuntimeFailure) {
|
||||||
IrisLogging.warn("The Dimension Type for " + dimension.getLoadFile() + " is not registered on the server.");
|
IrisLogging.warn("The Dimension Type for " + dimension.getLoadFile() + " is not registered on the server.");
|
||||||
|
}
|
||||||
warn = true;
|
warn = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (warn) {
|
if (warn && reportRuntimeFailure) {
|
||||||
IrisLogging.error("The Pack " + key + " is INCAPABLE of generating custom biomes");
|
IrisLogging.error("The Pack " + key + " is INCAPABLE of generating custom biomes");
|
||||||
IrisLogging.error("If not done automatically, restart your server before generating with this pack!");
|
IrisLogging.error("If not done automatically, restart your server before generating with this pack!");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import java.awt.event.WindowAdapter;
|
|||||||
import java.awt.event.WindowEvent;
|
import java.awt.event.WindowEvent;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
@@ -63,7 +64,7 @@ public final class GuiHost {
|
|||||||
default void unregisterHotloadHook(Runnable onHotload) {
|
default void unregisterHotloadHook(Runnable onHotload) {
|
||||||
}
|
}
|
||||||
|
|
||||||
default GuiOverlay overlayFor(Engine engine) {
|
default GuiOverlay overlayFor(Engine engine, UUID openerId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ import java.util.List;
|
|||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
public final class VisionGUI extends JPanel implements MouseWheelListener, KeyListener, MouseMotionListener, MouseInputListener {
|
public final class VisionGUI extends JPanel implements MouseWheelListener, KeyListener, MouseMotionListener, MouseInputListener {
|
||||||
private static final long serialVersionUID = 2094606939770332040L;
|
private static final long serialVersionUID = 2094606939770332040L;
|
||||||
@@ -107,6 +108,7 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi
|
|||||||
private static final double KEYBOARD_ZOOM_FACTOR = 1.189207115002721D;
|
private static final double KEYBOARD_ZOOM_FACTOR = 1.189207115002721D;
|
||||||
|
|
||||||
private final JFrame hostFrame;
|
private final JFrame hostFrame;
|
||||||
|
private final UUID openerId;
|
||||||
private final VisionRenderController controller;
|
private final VisionRenderController controller;
|
||||||
private final Runnable hotloadHook;
|
private final Runnable hotloadHook;
|
||||||
private final Timer resizeTimer;
|
private final Timer resizeTimer;
|
||||||
@@ -147,11 +149,12 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi
|
|||||||
private boolean controlsUpdating;
|
private boolean controlsUpdating;
|
||||||
private boolean closed;
|
private boolean closed;
|
||||||
|
|
||||||
private VisionGUI(JFrame hostFrame, Engine engine) {
|
private VisionGUI(JFrame hostFrame, Engine engine, UUID openerId) {
|
||||||
this.hostFrame = Objects.requireNonNull(hostFrame, "hostFrame");
|
this.hostFrame = Objects.requireNonNull(hostFrame, "hostFrame");
|
||||||
this.engine = Objects.requireNonNull(engine, "engine");
|
this.engine = Objects.requireNonNull(engine, "engine");
|
||||||
|
this.openerId = openerId;
|
||||||
this.renderer = new IrisRenderer(engine);
|
this.renderer = new IrisRenderer(engine);
|
||||||
this.overlay = GuiHost.get().overlayFor(engine);
|
this.overlay = GuiHost.get().overlayFor(engine, openerId);
|
||||||
this.controller = new VisionRenderController(this::repaint);
|
this.controller = new VisionRenderController(this::repaint);
|
||||||
this.hotloadHook = () -> EventQueue.invokeLater(this::refreshContent);
|
this.hotloadHook = () -> EventQueue.invokeLater(this::refreshContent);
|
||||||
this.notifications = new LinkedHashMap<>();
|
this.notifications = new LinkedHashMap<>();
|
||||||
@@ -201,14 +204,14 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi
|
|||||||
notificationTimer.start();
|
notificationTimer.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void launch(Engine engine) {
|
public static void launch(Engine engine, UUID openerId) {
|
||||||
EventQueue.invokeLater(() -> createAndShowGUI(engine));
|
EventQueue.invokeLater(() -> createAndShowGUI(engine, openerId));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void createAndShowGUI(Engine engine) {
|
private static void createAndShowGUI(Engine engine, UUID openerId) {
|
||||||
JFrame frame = new JFrame(IrisLanguage.plain(DesktopUiMessages.VISION_TITLE));
|
JFrame frame = new JFrame(IrisLanguage.plain(DesktopUiMessages.VISION_TITLE));
|
||||||
GuiHost.prepareFrame(frame);
|
GuiHost.prepareFrame(frame);
|
||||||
VisionGUI vision = new VisionGUI(frame, engine);
|
VisionGUI vision = new VisionGUI(frame, engine, openerId);
|
||||||
frame.getContentPane().setBackground(BACKGROUND);
|
frame.getContentPane().setBackground(BACKGROUND);
|
||||||
frame.setLayout(new BorderLayout());
|
frame.setLayout(new BorderLayout());
|
||||||
frame.add(buildToolbar(vision), BorderLayout.NORTH);
|
frame.add(buildToolbar(vision), BorderLayout.NORTH);
|
||||||
@@ -739,7 +742,7 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi
|
|||||||
}
|
}
|
||||||
engine = reacquired;
|
engine = reacquired;
|
||||||
renderer = new IrisRenderer(reacquired);
|
renderer = new IrisRenderer(reacquired);
|
||||||
overlay = GuiHost.get().overlayFor(reacquired);
|
overlay = GuiHost.get().overlayFor(reacquired, openerId);
|
||||||
contentRevision++;
|
contentRevision++;
|
||||||
captureHeightRange();
|
captureHeightRange();
|
||||||
return true;
|
return true;
|
||||||
@@ -758,7 +761,7 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
renderer = new IrisRenderer(engine);
|
renderer = new IrisRenderer(engine);
|
||||||
overlay = GuiHost.get().overlayFor(engine);
|
overlay = GuiHost.get().overlayFor(engine, openerId);
|
||||||
contentRevision++;
|
contentRevision++;
|
||||||
captureHeightRange();
|
captureHeightRange();
|
||||||
requestRender();
|
requestRender();
|
||||||
|
|||||||
@@ -56,6 +56,10 @@ public final class IrisLanguage {
|
|||||||
private static final Pattern LOCALE_NAME = Pattern.compile("[A-Za-z0-9_-]+");
|
private static final Pattern LOCALE_NAME = Pattern.compile("[A-Za-z0-9_-]+");
|
||||||
private static final Pattern LEGACY_COLOR = Pattern.compile("(?i)\\u00a7[0-9A-FK-ORX]");
|
private static final Pattern LEGACY_COLOR = Pattern.compile("(?i)\\u00a7[0-9A-FK-ORX]");
|
||||||
private static final MessageCatalog CATALOG = IrisMessages.catalog();
|
private static final MessageCatalog CATALOG = IrisMessages.catalog();
|
||||||
|
private static final List<String> BUKKIT_MESSAGE_IDS = CATALOG.ids().stream()
|
||||||
|
.filter(id -> !id.startsWith("iris.modded."))
|
||||||
|
.sorted()
|
||||||
|
.toList();
|
||||||
private static final LocalizationManager MANAGER = new LocalizationManager(
|
private static final LocalizationManager MANAGER = new LocalizationManager(
|
||||||
LocalizationCandidate.english(CATALOG, PluralSelector.oneOther())
|
LocalizationCandidate.english(CATALOG, PluralSelector.oneOther())
|
||||||
);
|
);
|
||||||
@@ -369,8 +373,11 @@ public final class IrisLanguage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static LocaleOverlay parseOverlay(String source, String locale, String raw) {
|
static LocaleOverlay parseOverlay(String source, String locale, String raw) {
|
||||||
JsonElement parsed = JsonParser.parseString(raw == null || raw.isBlank() ? "{}" : raw);
|
JsonElement parsed = JsonParser.parseString(raw == null || raw.isBlank() ? "{}" : raw);
|
||||||
|
if (parsed.isJsonArray()) {
|
||||||
|
return parseCompactBukkitOverlay(source, locale, parsed.getAsJsonArray());
|
||||||
|
}
|
||||||
if (!parsed.isJsonObject()) {
|
if (!parsed.isJsonObject()) {
|
||||||
throw new IllegalArgumentException("Locale source is not a JSON object: " + source);
|
throw new IllegalArgumentException("Locale source is not a JSON object: " + source);
|
||||||
}
|
}
|
||||||
@@ -398,6 +405,49 @@ public final class IrisLanguage {
|
|||||||
return builder.build();
|
return builder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static LocaleOverlay parseCompactBukkitOverlay(
|
||||||
|
String source,
|
||||||
|
String locale,
|
||||||
|
JsonArray root
|
||||||
|
) {
|
||||||
|
if (root.size() != 2 || !root.get(0).isJsonPrimitive() || !root.get(1).isJsonArray()) {
|
||||||
|
throw new IllegalArgumentException("Compact locale source is invalid: " + source);
|
||||||
|
}
|
||||||
|
if (!locale.equals(normalizeLocale(root.get(0).getAsString()))) {
|
||||||
|
throw new IllegalArgumentException("Locale source declares a different locale than its file: " + source);
|
||||||
|
}
|
||||||
|
JsonArray messages = root.get(1).getAsJsonArray();
|
||||||
|
if (messages.size() != BUKKIT_MESSAGE_IDS.size()) {
|
||||||
|
throw new IllegalArgumentException("Compact locale message count is invalid: " + source);
|
||||||
|
}
|
||||||
|
LocaleOverlay.Builder builder = LocaleOverlay.builder(source, locale);
|
||||||
|
for (int i = 0; i < messages.size(); i++) {
|
||||||
|
JsonElement value = messages.get(i);
|
||||||
|
if (value == null || value.isJsonNull()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
appendCompactMessage(builder, BUKKIT_MESSAGE_IDS.get(i), value);
|
||||||
|
}
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void appendCompactMessage(
|
||||||
|
LocaleOverlay.Builder builder,
|
||||||
|
String key,
|
||||||
|
JsonElement value
|
||||||
|
) {
|
||||||
|
MessageKey definition = CATALOG.key(key);
|
||||||
|
if (value.isJsonObject() && definition instanceof PluralKey) {
|
||||||
|
builder.plural(key, readPlural(key, value.getAsJsonObject()));
|
||||||
|
} else if (value.isJsonArray()) {
|
||||||
|
builder.lines(key, readLines(key, value.getAsJsonArray()));
|
||||||
|
} else if (value.isJsonPrimitive() && value.getAsJsonPrimitive().isString()) {
|
||||||
|
builder.text(key, value.getAsString());
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Compact locale value has an invalid shape: " + key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void appendMessages(LocaleOverlay.Builder builder, JsonObject object, String prefix) {
|
private static void appendMessages(LocaleOverlay.Builder builder, JsonObject object, String prefix) {
|
||||||
for (Map.Entry<String, JsonElement> entry : object.entrySet()) {
|
for (Map.Entry<String, JsonElement> entry : object.entrySet()) {
|
||||||
String key = prefix.isEmpty() ? entry.getKey() : prefix + "." + entry.getKey();
|
String key = prefix.isEmpty() ? entry.getKey() : prefix + "." + entry.getKey();
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import java.util.List;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
final class PackRiverValidator {
|
final class PackRiverValidator {
|
||||||
private static final Set<String> WATER_MODES = Set.of("SEA_LEVEL", "TERRACED");
|
private static final Set<String> WATER_MODES = Set.of("FIXED", "TERRACED");
|
||||||
private static final Set<String> TERMINAL_MODES = Set.of("SUPPRESS", "DRY_CHANNEL", "SINKHOLE_GROTTO");
|
private static final Set<String> TERMINAL_MODES = Set.of("SUPPRESS", "DRY_CHANNEL", "SINKHOLE_GROTTO");
|
||||||
private static final Set<String> ROUTING_POLICIES = Set.of("ALLOW", "AVOID", "BLOCK");
|
private static final Set<String> ROUTING_POLICIES = Set.of("ALLOW", "AVOID", "BLOCK");
|
||||||
private static final Set<String> CAVE_MODES = Set.of(
|
private static final Set<String> CAVE_MODES = Set.of(
|
||||||
@@ -91,7 +91,7 @@ final class PackRiverValidator {
|
|||||||
validateTerrain(packFolder, path + ".terrain", terrain, errors, warnings);
|
validateTerrain(packFolder, path + ".terrain", terrain, errors, warnings);
|
||||||
}
|
}
|
||||||
if (water != null) {
|
if (water != null) {
|
||||||
validateWater(path + ".water", water, errors);
|
validateWater(path + ".water", water, context.dimension(), errors);
|
||||||
}
|
}
|
||||||
if (biomes != null) {
|
if (biomes != null) {
|
||||||
validateBiomePools(
|
validateBiomePools(
|
||||||
@@ -107,7 +107,15 @@ final class PackRiverValidator {
|
|||||||
boolean sinkholeTerminal = terrain != null
|
boolean sinkholeTerminal = terrain != null
|
||||||
&& "SINKHOLE_GROTTO".equals(stringValue(terrain, "terminalMode", "DRY_CHANNEL"));
|
&& "SINKHOLE_GROTTO".equals(stringValue(terrain, "terminalMode", "DRY_CHANNEL"));
|
||||||
if (caves != null) {
|
if (caves != null) {
|
||||||
validateCaves(packFolder, path + ".caves", caves, sinkholeTerminal, errors, warnings);
|
validateCaves(
|
||||||
|
packFolder,
|
||||||
|
path + ".caves",
|
||||||
|
caves,
|
||||||
|
context.dimension(),
|
||||||
|
sinkholeTerminal,
|
||||||
|
errors,
|
||||||
|
warnings
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (topology != null && terrain != null) {
|
if (topology != null && terrain != null) {
|
||||||
@@ -168,6 +176,7 @@ final class PackRiverValidator {
|
|||||||
validateStyledRange(packFolder, terrain, "bankWidth", path, 0D, 2048D, errors, warnings);
|
validateStyledRange(packFolder, terrain, "bankWidth", path, 0D, 2048D, errors, warnings);
|
||||||
validateStyledRange(packFolder, terrain, "depth", path, 1D, 512D, errors, warnings);
|
validateStyledRange(packFolder, terrain, "depth", path, 1D, 512D, errors, warnings);
|
||||||
validateStyledRange(packFolder, terrain, "tunnelWidthMultiplier", path, 1D, 8D, errors, warnings);
|
validateStyledRange(packFolder, terrain, "tunnelWidthMultiplier", path, 1D, 8D, errors, warnings);
|
||||||
|
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "channelRadiusBonus", 0D, 64D, errors);
|
||||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "maxChannelWidth", 1D, 2048D, errors);
|
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "maxChannelWidth", 1D, 2048D, errors);
|
||||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "maxBankWidth", 0D, 2048D, errors);
|
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "maxBankWidth", 0D, 2048D, errors);
|
||||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "maxDepth", 1D, 512D, errors);
|
PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "maxDepth", 1D, 512D, errors);
|
||||||
@@ -211,18 +220,50 @@ final class PackRiverValidator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void validateWater(String path, JSONObject water, List<String> errors) {
|
private static void validateWater(
|
||||||
|
String path,
|
||||||
|
JSONObject water,
|
||||||
|
JSONObject dimension,
|
||||||
|
List<String> errors
|
||||||
|
) {
|
||||||
PackJsonFieldChecks.validateOptionalEnum(path, water, "mode", WATER_MODES, errors);
|
PackJsonFieldChecks.validateOptionalEnum(path, water, "mode", WATER_MODES, errors);
|
||||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, water, "poolLength", 8, 4096, errors);
|
PackJsonFieldChecks.validateOptionalIntegerRange(path, water, "poolLength", 8, 4096, errors);
|
||||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, water, "maximumPoolRise", 0, 64, errors);
|
PackJsonFieldChecks.validateOptionalIntegerRange(path, water, "maximumPoolRise", 0, 64, errors);
|
||||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, water, "dropHeight", 1, 32, errors);
|
PackJsonFieldChecks.validateOptionalIntegerRange(path, water, "dropHeight", 1, 32, errors);
|
||||||
|
PackJsonFieldChecks.validateOptionalIntegerRange(path, water, "fluidHeight", -2048, 2048, errors);
|
||||||
|
validateFluidPalette(path, water, errors);
|
||||||
|
|
||||||
String mode = stringValue(water, "mode", "SEA_LEVEL");
|
String mode = stringValue(water, "mode", "FIXED");
|
||||||
|
int fluidHeight = integerValue(water, "fluidHeight", 63);
|
||||||
int maximumPoolRise = integerValue(water, "maximumPoolRise", 4);
|
int maximumPoolRise = integerValue(water, "maximumPoolRise", 4);
|
||||||
int dropHeight = integerValue(water, "dropHeight", 1);
|
int dropHeight = integerValue(water, "dropHeight", 1);
|
||||||
if ("TERRACED".equals(mode) && dropHeight > maximumPoolRise) {
|
if ("TERRACED".equals(mode) && dropHeight > maximumPoolRise) {
|
||||||
errors.add(path + ".dropHeight must not exceed maximumPoolRise in TERRACED mode.");
|
errors.add(path + ".dropHeight must not exceed maximumPoolRise in TERRACED mode.");
|
||||||
}
|
}
|
||||||
|
JSONObject dimensionHeight = dimension.optJSONObject("dimensionHeight");
|
||||||
|
int minimumHeight = dimensionHeight == null ? -64 : integerValue(dimensionHeight, "min", -64);
|
||||||
|
int maximumHeight = dimensionHeight == null ? 320 : integerValue(dimensionHeight, "max", 320);
|
||||||
|
if (fluidHeight < minimumHeight || fluidHeight > maximumHeight) {
|
||||||
|
errors.add(path + ".fluidHeight must remain inside dimensionHeight.");
|
||||||
|
}
|
||||||
|
if ("TERRACED".equals(mode) && fluidHeight + maximumPoolRise > maximumHeight) {
|
||||||
|
errors.add(path + ".fluidHeight plus maximumPoolRise must remain inside dimensionHeight.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validateFluidPalette(String path, JSONObject water, List<String> errors) {
|
||||||
|
if (!water.has("fluidPalette")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Object rawPalette = water.opt("fluidPalette");
|
||||||
|
if (!(rawPalette instanceof JSONObject palette)) {
|
||||||
|
errors.add(path + ".fluidPalette must be an object.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Object rawBlocks = palette.opt("palette");
|
||||||
|
if (!(rawBlocks instanceof JSONArray blocks) || blocks.length() < 1) {
|
||||||
|
errors.add(path + ".fluidPalette.palette must contain at least one fluid block.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void validateTopologyComplexity(
|
private static void validateTopologyComplexity(
|
||||||
@@ -345,9 +386,11 @@ final class PackRiverValidator {
|
|||||||
PackJsonFieldChecks.validateOptionalDoubleRange(
|
PackJsonFieldChecks.validateOptionalDoubleRange(
|
||||||
wormPath, worm, "depthMultiplier", 0.125D, 8D, errors);
|
wormPath, worm, "depthMultiplier", 0.125D, 8D, errors);
|
||||||
PackJsonFieldChecks.validateOptionalDoubleRange(
|
PackJsonFieldChecks.validateOptionalDoubleRange(
|
||||||
wormPath, worm, "bodyWavelength", 32D, 16384D, errors);
|
wormPath, worm, "bodyWavelength", 8D, 16384D, errors);
|
||||||
PackJsonFieldChecks.validateOptionalDoubleRange(
|
PackJsonFieldChecks.validateOptionalDoubleRange(
|
||||||
wormPath, worm, "bodyDetailWavelength", 32D, 16384D, errors);
|
wormPath, worm, "bodyDetailWavelength", 8D, 16384D, errors);
|
||||||
|
PackJsonFieldChecks.validateOptionalDoubleRange(
|
||||||
|
wormPath, worm, "bodyDetailInfluence", 0D, 1D, errors);
|
||||||
PackJsonFieldChecks.validateOptionalDoubleRange(
|
PackJsonFieldChecks.validateOptionalDoubleRange(
|
||||||
wormPath, worm, "widthVariation", 0D, 0.875D, errors);
|
wormPath, worm, "widthVariation", 0D, 0.875D, errors);
|
||||||
PackJsonFieldChecks.validateOptionalDoubleRange(
|
PackJsonFieldChecks.validateOptionalDoubleRange(
|
||||||
@@ -452,6 +495,7 @@ final class PackRiverValidator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void validateCaves(File packFolder, String path, JSONObject caves,
|
private static void validateCaves(File packFolder, String path, JSONObject caves,
|
||||||
|
JSONObject dimension,
|
||||||
boolean forceGeneratedGrotto,
|
boolean forceGeneratedGrotto,
|
||||||
List<String> errors, List<String> warnings) {
|
List<String> errors, List<String> warnings) {
|
||||||
PackJsonFieldChecks.validateOptionalEnum(path, caves, "mode", CAVE_MODES, errors);
|
PackJsonFieldChecks.validateOptionalEnum(path, caves, "mode", CAVE_MODES, errors);
|
||||||
@@ -473,6 +517,19 @@ final class PackRiverValidator {
|
|||||||
validateNoiseChance(packFolder, caves, "entry", path, errors);
|
validateNoiseChance(packFolder, caves, "entry", path, errors);
|
||||||
validateStyle(packFolder, caves, "grottoShapeStyle", path, errors);
|
validateStyle(packFolder, caves, "grottoShapeStyle", path, errors);
|
||||||
validateStyle(packFolder, caves, "grottoWarpStyle", path, errors);
|
validateStyle(packFolder, caves, "grottoWarpStyle", path, errors);
|
||||||
|
JSONObject deepPools = caves.has("deepPools")
|
||||||
|
? requireObject(caves, "deepPools", path + ".deepPools", errors)
|
||||||
|
: null;
|
||||||
|
if (deepPools != null) {
|
||||||
|
validateDeepPools(
|
||||||
|
packFolder,
|
||||||
|
path + ".deepPools",
|
||||||
|
deepPools,
|
||||||
|
dimension,
|
||||||
|
errors,
|
||||||
|
warnings
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
String mode = stringValue(caves, "mode", "SEALED");
|
String mode = stringValue(caves, "mode", "SEALED");
|
||||||
if ("SEALED".equals(mode) && !forceGeneratedGrotto) {
|
if ("SEALED".equals(mode) && !forceGeneratedGrotto) {
|
||||||
@@ -505,6 +562,86 @@ final class PackRiverValidator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void validateDeepPools(
|
||||||
|
File packFolder,
|
||||||
|
String path,
|
||||||
|
JSONObject deepPools,
|
||||||
|
JSONObject dimension,
|
||||||
|
List<String> errors,
|
||||||
|
List<String> warnings
|
||||||
|
) {
|
||||||
|
PackJsonFieldChecks.validateOptionalBoolean(path, deepPools, "enabled", errors);
|
||||||
|
PackJsonFieldChecks.validateOptionalIntegerRange(
|
||||||
|
path, deepPools, "minimumSpacing", 16, 4096, errors);
|
||||||
|
PackJsonFieldChecks.validateOptionalIntegerRange(
|
||||||
|
path, deepPools, "maximumPerReach", 0, 16, errors);
|
||||||
|
PackJsonFieldChecks.validateOptionalIntegerRange(
|
||||||
|
path, deepPools, "minimumFluidY", -2048, 2048, errors);
|
||||||
|
PackJsonFieldChecks.validateOptionalIntegerRange(
|
||||||
|
path, deepPools, "maximumFluidY", -2048, 2048, errors);
|
||||||
|
PackJsonFieldChecks.validateOptionalIntegerRange(
|
||||||
|
path, deepPools, "searchRadius", 0, 256, errors);
|
||||||
|
PackJsonFieldChecks.validateOptionalIntegerRange(
|
||||||
|
path, deepPools, "searchAttempts", 1, 64, errors);
|
||||||
|
PackJsonFieldChecks.validateOptionalIntegerRange(
|
||||||
|
path, deepPools, "horizontalRadius", 2, 128, errors);
|
||||||
|
PackJsonFieldChecks.validateOptionalIntegerRange(
|
||||||
|
path, deepPools, "verticalRadius", 2, 64, errors);
|
||||||
|
PackJsonFieldChecks.validateOptionalIntegerRange(
|
||||||
|
path, deepPools, "dryHeadroom", 1, 63, errors);
|
||||||
|
PackJsonFieldChecks.validateOptionalDoubleRange(
|
||||||
|
path, deepPools, "shapeVariation", 0D, 0.75D, errors);
|
||||||
|
PackJsonFieldChecks.validateOptionalDoubleRange(
|
||||||
|
path, deepPools, "warpStrength", 0D, 64D, errors);
|
||||||
|
PackJsonFieldChecks.validateOptionalIntegerRange(
|
||||||
|
path, deepPools, "maximumVolume", 64, 1048576, errors);
|
||||||
|
validateNoiseChance(packFolder, deepPools, "reach", path, errors);
|
||||||
|
validateStyle(packFolder, deepPools, "shapeStyle", path, errors);
|
||||||
|
validateStyle(packFolder, deepPools, "warpStyle", path, errors);
|
||||||
|
validateFluidPalette(path, deepPools, errors);
|
||||||
|
|
||||||
|
int minimumFluidY = integerValue(deepPools, "minimumFluidY", -224);
|
||||||
|
int maximumFluidY = integerValue(deepPools, "maximumFluidY", -104);
|
||||||
|
int searchRadius = integerValue(deepPools, "searchRadius", 16);
|
||||||
|
int horizontalRadius = integerValue(deepPools, "horizontalRadius", 18);
|
||||||
|
int verticalRadius = integerValue(deepPools, "verticalRadius", 8);
|
||||||
|
int dryHeadroom = integerValue(deepPools, "dryHeadroom", 4);
|
||||||
|
int maximumVolume = integerValue(deepPools, "maximumVolume", 32768);
|
||||||
|
if (minimumFluidY > maximumFluidY) {
|
||||||
|
errors.add(path + ".minimumFluidY must not exceed maximumFluidY.");
|
||||||
|
}
|
||||||
|
if (dryHeadroom >= verticalRadius) {
|
||||||
|
errors.add(path + ".dryHeadroom must be smaller than verticalRadius.");
|
||||||
|
}
|
||||||
|
if (searchRadius + horizontalRadius > 128) {
|
||||||
|
errors.add(path + ".searchRadius plus horizontalRadius must not exceed 128 blocks.");
|
||||||
|
}
|
||||||
|
long minimumVolume = grottoVolume(horizontalRadius, verticalRadius);
|
||||||
|
if (minimumVolume > maximumVolume) {
|
||||||
|
errors.add(path + ".maximumVolume must be at least " + minimumVolume
|
||||||
|
+ " to contain the base deep-pool chamber.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!booleanValue(deepPools, "enabled", false)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONObject dimensionHeight = dimension.optJSONObject("dimensionHeight");
|
||||||
|
int minimumHeight = dimensionHeight == null ? -64 : integerValue(dimensionHeight, "min", -64);
|
||||||
|
int maximumHeight = dimensionHeight == null ? 320 : integerValue(dimensionHeight, "max", 320);
|
||||||
|
int lowestBoundaryY = minimumFluidY - (verticalRadius * 2 - dryHeadroom) - 1;
|
||||||
|
int highestBoundaryY = maximumFluidY + dryHeadroom + 1;
|
||||||
|
if (lowestBoundaryY <= minimumHeight || highestBoundaryY >= maximumHeight) {
|
||||||
|
errors.add(path + " fluid range and chamber envelope must remain inside dimensionHeight.");
|
||||||
|
}
|
||||||
|
|
||||||
|
int maximumPerReach = integerValue(deepPools, "maximumPerReach", 1);
|
||||||
|
double reachChance = noiseChanceValue(deepPools, "reach", 1D / 3D);
|
||||||
|
if (maximumPerReach == 0 || reachChance == 0D) {
|
||||||
|
warnings.add(path + " is enabled but its reach gate cannot accept any pools.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void validateGrotto(String path, JSONObject caves, List<String> errors) {
|
private static void validateGrotto(String path, JSONObject caves, List<String> errors) {
|
||||||
int throatRadius = integerValue(caves, "throatRadius", 2);
|
int throatRadius = integerValue(caves, "throatRadius", 2);
|
||||||
int dryHeadroom = integerValue(caves, "dryHeadroom", 4);
|
int dryHeadroom = integerValue(caves, "dryHeadroom", 4);
|
||||||
|
|||||||
+28
-12
@@ -65,7 +65,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
|||||||
private static final AtomicInteger BOOST_HOLDERS = new AtomicInteger();
|
private static final AtomicInteger BOOST_HOLDERS = new AtomicInteger();
|
||||||
private static final int ADAPTIVE_SLOW_REQUEST_STEP = 3;
|
private static final int ADAPTIVE_SLOW_REQUEST_STEP = 3;
|
||||||
private static final int ADAPTIVE_RECOVERY_INTERVAL = 8;
|
private static final int ADAPTIVE_RECOVERY_INTERVAL = 8;
|
||||||
private static final long CLOSE_DRAIN_TIMEOUT_SECONDS = 60L;
|
private static final long CLOSE_DRAIN_WARNING_SECONDS = 60L;
|
||||||
private static final long FLUSH_TIMEOUT_SECONDS = 120L;
|
private static final long FLUSH_TIMEOUT_SECONDS = 120L;
|
||||||
private final World world;
|
private final World world;
|
||||||
private final IrisRuntimeSchedulerMode runtimeSchedulerMode;
|
private final IrisRuntimeSchedulerMode runtimeSchedulerMode;
|
||||||
@@ -773,17 +773,13 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
|||||||
// A stop request interrupts the pregen worker; shield the drain and flush so chunks still hit disk.
|
// A stop request interrupts the pregen worker; shield the drain and flush so chunks still hit disk.
|
||||||
boolean interrupted = Thread.interrupted();
|
boolean interrupted = Thread.interrupted();
|
||||||
try {
|
try {
|
||||||
boolean drained = false;
|
interrupted |= awaitDrain(
|
||||||
try {
|
semaphore,
|
||||||
drained = semaphore.tryAcquire(threads, CLOSE_DRAIN_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
threads,
|
||||||
} catch (InterruptedException e) {
|
CLOSE_DRAIN_WARNING_SECONDS,
|
||||||
interrupted = true;
|
TimeUnit.SECONDS,
|
||||||
}
|
() -> IrisLogging.warn("Async pregen is still draining outstanding chunks. " + metricsSnapshot())
|
||||||
|
);
|
||||||
if (!drained) {
|
|
||||||
IrisLogging.warn("Async pregen close did not drain in " + CLOSE_DRAIN_TIMEOUT_SECONDS
|
|
||||||
+ "s, continuing degraded. " + metricsSnapshot());
|
|
||||||
}
|
|
||||||
|
|
||||||
flushAllRemainingChunks();
|
flushAllRemainingChunks();
|
||||||
executor.shutdown();
|
executor.shutdown();
|
||||||
@@ -797,6 +793,26 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static boolean awaitDrain(
|
||||||
|
Semaphore semaphore,
|
||||||
|
int permits,
|
||||||
|
long warningInterval,
|
||||||
|
TimeUnit timeUnit,
|
||||||
|
Runnable onWait
|
||||||
|
) {
|
||||||
|
boolean interrupted = false;
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
if (semaphore.tryAcquire(permits, warningInterval, timeUnit)) {
|
||||||
|
return interrupted;
|
||||||
|
}
|
||||||
|
onWait.run();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
interrupted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isCancelled() {
|
private boolean isCancelled() {
|
||||||
return closing.get() || Thread.currentThread().isInterrupted();
|
return closing.get() || Thread.currentThread().isInterrupted();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,18 +85,34 @@ public class IrisProject {
|
|||||||
StudioOpenCoordinator.StudioOpenKind openKind,
|
StudioOpenCoordinator.StudioOpenKind openKind,
|
||||||
Consumer<World> onDone
|
Consumer<World> onDone
|
||||||
) throws IrisException {
|
) throws IrisException {
|
||||||
if (isOpen()) {
|
return open(sender, seed, openKind, onDone, System.nanoTime());
|
||||||
return close().thenCompose(ignored -> openInternal(sender, seed, openKind, onDone));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return openInternal(sender, seed, openKind, onDone);
|
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,
|
||||||
|
requestedAtNanos));
|
||||||
|
}
|
||||||
|
|
||||||
|
return openInternal(sender, seed, openKind, onDone, requestedAtNanos);
|
||||||
}
|
}
|
||||||
|
|
||||||
private CompletableFuture<StudioOpenCoordinator.StudioOpenResult> openInternal(
|
private CompletableFuture<StudioOpenCoordinator.StudioOpenResult> openInternal(
|
||||||
VolmitSender sender,
|
VolmitSender sender,
|
||||||
long seed,
|
long seed,
|
||||||
StudioOpenCoordinator.StudioOpenKind openKind,
|
StudioOpenCoordinator.StudioOpenKind openKind,
|
||||||
Consumer<World> onDone
|
Consumer<World> onDone,
|
||||||
|
long requestedAtNanos
|
||||||
) {
|
) {
|
||||||
AtomicReference<String> stage = new AtomicReference<>("Queued");
|
AtomicReference<String> stage = new AtomicReference<>("Queued");
|
||||||
AtomicReference<Double> progress = new AtomicReference<>(0.01D);
|
AtomicReference<Double> progress = new AtomicReference<>(0.01D);
|
||||||
@@ -114,7 +130,8 @@ public class IrisProject {
|
|||||||
}
|
}
|
||||||
progress.set(Math.max(0D, Math.min(0.99D, update.progress())));
|
progress.set(Math.max(0D, Math.min(0.99D, update.progress())));
|
||||||
},
|
},
|
||||||
onDone
|
onDone,
|
||||||
|
requestedAtNanos
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
StudioOpenProgressReporter.startStudioOpenReporter(sender, stage, progress, complete, failed);
|
StudioOpenProgressReporter.startStudioOpenReporter(sender, stage, progress, complete, failed);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import art.arcane.iris.core.project.IrisProject;
|
|||||||
import art.arcane.iris.core.project.IrisCodeWorkspace;
|
import art.arcane.iris.core.project.IrisCodeWorkspace;
|
||||||
import art.arcane.iris.core.tools.IrisCreator;
|
import art.arcane.iris.core.tools.IrisCreator;
|
||||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||||
|
import art.arcane.iris.engine.IrisEngine;
|
||||||
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
|
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
|
||||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||||
@@ -98,16 +99,11 @@ public final class StudioOpenCoordinator {
|
|||||||
|
|
||||||
public CompletableFuture<Boolean> teleportPlayerToProject(
|
public CompletableFuture<Boolean> teleportPlayerToProject(
|
||||||
IrisProject project,
|
IrisProject project,
|
||||||
Player player,
|
Player player
|
||||||
AtomicBoolean admission,
|
|
||||||
long deadlineNanos
|
|
||||||
) {
|
) {
|
||||||
if (project == null || player == null) {
|
if (project == null || player == null) {
|
||||||
return CompletableFuture.completedFuture(false);
|
return CompletableFuture.completedFuture(false);
|
||||||
}
|
}
|
||||||
AtomicBoolean activeAdmission = Objects.requireNonNull(
|
|
||||||
admission,
|
|
||||||
"Studio teleport admission");
|
|
||||||
PlatformChunkGenerator provider = project.getActiveProvider();
|
PlatformChunkGenerator provider = project.getActiveProvider();
|
||||||
if (provider == null) {
|
if (provider == null) {
|
||||||
return CompletableFuture.failedFuture(new IllegalStateException(
|
return CompletableFuture.failedFuture(new IllegalStateException(
|
||||||
@@ -123,10 +119,6 @@ public final class StudioOpenCoordinator {
|
|||||||
return CompletableFuture.failedFuture(new IllegalStateException(
|
return CompletableFuture.failedFuture(new IllegalStateException(
|
||||||
"Studio entry point could not be resolved."));
|
"Studio entry point could not be resolved."));
|
||||||
}
|
}
|
||||||
if (System.nanoTime() >= deadlineNanos
|
|
||||||
|| !activeAdmission.compareAndSet(true, false)) {
|
|
||||||
return studioTeleportDeadlineFailure("native teleport delegation");
|
|
||||||
}
|
|
||||||
CompletableFuture<Boolean> teleport = project.getActiveOpenKind() == StudioOpenKind.STANDARD
|
CompletableFuture<Boolean> teleport = project.getActiveOpenKind() == StudioOpenKind.STANDARD
|
||||||
? WorldRuntimeControlService.get().teleportInMode(player, entry, GameMode.SPECTATOR)
|
? WorldRuntimeControlService.get().teleportInMode(player, entry, GameMode.SPECTATOR)
|
||||||
: WorldRuntimeControlService.get().teleport(player, entry);
|
: WorldRuntimeControlService.get().teleport(player, entry);
|
||||||
@@ -134,25 +126,12 @@ public final class StudioOpenCoordinator {
|
|||||||
return CompletableFuture.failedFuture(new IllegalStateException(
|
return CompletableFuture.failedFuture(new IllegalStateException(
|
||||||
"Studio native teleport returned no completion future."));
|
"Studio native teleport returned no completion future."));
|
||||||
}
|
}
|
||||||
long remainingNanos = deadlineNanos - System.nanoTime();
|
|
||||||
if (remainingNanos <= 0L) {
|
|
||||||
teleport.completeExceptionally(new TimeoutException(
|
|
||||||
"Studio teleport deadline expired before native teleport completion."));
|
|
||||||
return teleport;
|
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) {
|
private void executeOpen(StudioOpenRequest request, CompletableFuture<StudioOpenResult> future) {
|
||||||
World world = null;
|
World world = null;
|
||||||
PlatformChunkGenerator provider = null;
|
PlatformChunkGenerator provider = null;
|
||||||
CompletableFuture<Boolean> nativeTeleportFuture = null;
|
|
||||||
try {
|
try {
|
||||||
long openStart = System.nanoTime();
|
long openStart = System.nanoTime();
|
||||||
long t = openStart;
|
long t = openStart;
|
||||||
@@ -181,6 +160,10 @@ public final class StudioOpenCoordinator {
|
|||||||
if (provider == null) {
|
if (provider == null) {
|
||||||
throw new IllegalStateException("Studio runtime provider is unavailable for world \"" + request.worldName() + "\".");
|
throw new IllegalStateException("Studio runtime provider is unavailable for world \"" + request.worldName() + "\".");
|
||||||
}
|
}
|
||||||
|
World entryWorld = world;
|
||||||
|
PlatformChunkGenerator entryProvider = provider;
|
||||||
|
CompletableFuture<Void> entryBootstrap = J.afut(
|
||||||
|
() -> endStudioEntryBootstrap(entryWorld, entryProvider));
|
||||||
|
|
||||||
updateStage(request, "apply_world_rules", 0.72D);
|
updateStage(request, "apply_world_rules", 0.72D);
|
||||||
final World rulesWorld = world;
|
final World rulesWorld = world;
|
||||||
@@ -204,9 +187,15 @@ public final class StudioOpenCoordinator {
|
|||||||
t = logStudioPhase(request, "resolve_entry_anchor", t, openStart);
|
t = logStudioPhase(request, "resolve_entry_anchor", t, openStart);
|
||||||
|
|
||||||
updateStage(request, "prepare_structure_rings", 0.79D);
|
updateStage(request, "prepare_structure_rings", 0.79D);
|
||||||
endStudioEntryBootstrap(world, provider);
|
entryBootstrap.get(STUDIO_STRUCTURE_ACTIVATION_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||||
t = logStudioPhase(request, "prepare_structure_rings", t, openStart);
|
t = logStudioPhase(request, "prepare_structure_rings", t, openStart);
|
||||||
|
|
||||||
|
updateStage(request, "prepare_generation_caches", 0.88D);
|
||||||
|
if (entryProvider.getEngine() instanceof IrisEngine irisEngine) {
|
||||||
|
irisEngine.awaitGenerationCacheWarm();
|
||||||
|
}
|
||||||
|
t = logStudioPhase(request, "prepare_generation_caches", t, openStart);
|
||||||
|
|
||||||
Location entryLocation = entryAnchor;
|
Location entryLocation = entryAnchor;
|
||||||
|
|
||||||
if (request.openKind().teleportThroughStandardEntry()
|
if (request.openKind().teleportThroughStandardEntry()
|
||||||
@@ -219,7 +208,7 @@ public final class StudioOpenCoordinator {
|
|||||||
}
|
}
|
||||||
Boolean teleported;
|
Boolean teleported;
|
||||||
try {
|
try {
|
||||||
nativeTeleportFuture = WorldRuntimeControlService.get().teleportInMode(
|
CompletableFuture<Boolean> nativeTeleportFuture = WorldRuntimeControlService.get().teleportInMode(
|
||||||
player,
|
player,
|
||||||
entryLocation,
|
entryLocation,
|
||||||
GameMode.SPECTATOR);
|
GameMode.SPECTATOR);
|
||||||
@@ -227,15 +216,20 @@ public final class StudioOpenCoordinator {
|
|||||||
throw new IllegalStateException(
|
throw new IllegalStateException(
|
||||||
"Studio native teleport returned no completion future.");
|
"Studio native teleport returned no completion future.");
|
||||||
}
|
}
|
||||||
teleported = nativeTeleportFuture.get(60L, TimeUnit.SECONDS);
|
teleported = nativeTeleportFuture.get();
|
||||||
} catch (TimeoutException e) {
|
} catch (ExecutionException e) {
|
||||||
nativeTeleportFuture.completeExceptionally(e);
|
Throwable failure = unwrapFailure(e);
|
||||||
throw new IllegalStateException("Studio teleport timed out — destination region may still be generating.");
|
throw new IllegalStateException("Studio teleport failed.", failure);
|
||||||
}
|
}
|
||||||
if (!Boolean.TRUE.equals(teleported)) {
|
if (!Boolean.TRUE.equals(teleported)) {
|
||||||
throw new IllegalStateException("Studio teleport did not complete successfully.");
|
throw new IllegalStateException("Studio teleport did not complete successfully.");
|
||||||
}
|
}
|
||||||
t = logStudioPhase(request, "teleport_standard_entry", t, openStart);
|
t = logStudioPhase(request, "teleport_standard_entry", t, openStart);
|
||||||
|
IrisLogging.info("Studio player %s arrived in %dms: dimension=%s world=%s",
|
||||||
|
player.getName(),
|
||||||
|
elapsedMillis(request.requestedAtNanos()),
|
||||||
|
request.dimensionKey(),
|
||||||
|
world.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
updateStage(request, "finalize_open", 1.00D);
|
updateStage(request, "finalize_open", 1.00D);
|
||||||
@@ -861,10 +855,14 @@ public final class StudioOpenCoordinator {
|
|||||||
StudioOpenKind openKind,
|
StudioOpenKind openKind,
|
||||||
boolean retainOnFailure,
|
boolean retainOnFailure,
|
||||||
Consumer<StudioOpenProgress> progressConsumer,
|
Consumer<StudioOpenProgress> progressConsumer,
|
||||||
Consumer<World> onDone
|
Consumer<World> onDone,
|
||||||
|
long requestedAtNanos
|
||||||
) {
|
) {
|
||||||
public StudioOpenRequest {
|
public StudioOpenRequest {
|
||||||
openKind = Objects.requireNonNull(openKind, "Studio open kind");
|
openKind = Objects.requireNonNull(openKind, "Studio open kind");
|
||||||
|
if (requestedAtNanos <= 0L) {
|
||||||
|
throw new IllegalArgumentException("Studio request time must be positive.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static StudioOpenRequest studioProject(
|
public static StudioOpenRequest studioProject(
|
||||||
@@ -874,6 +872,25 @@ public final class StudioOpenCoordinator {
|
|||||||
StudioOpenKind openKind,
|
StudioOpenKind openKind,
|
||||||
Consumer<StudioOpenProgress> progressConsumer,
|
Consumer<StudioOpenProgress> progressConsumer,
|
||||||
Consumer<World> onDone
|
Consumer<World> onDone
|
||||||
|
) {
|
||||||
|
return studioProject(
|
||||||
|
project,
|
||||||
|
sender,
|
||||||
|
seed,
|
||||||
|
openKind,
|
||||||
|
progressConsumer,
|
||||||
|
onDone,
|
||||||
|
System.nanoTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StudioOpenRequest studioProject(
|
||||||
|
IrisProject project,
|
||||||
|
VolmitSender sender,
|
||||||
|
long seed,
|
||||||
|
StudioOpenKind openKind,
|
||||||
|
Consumer<StudioOpenProgress> progressConsumer,
|
||||||
|
Consumer<World> onDone,
|
||||||
|
long requestedAtNanos
|
||||||
) {
|
) {
|
||||||
String playerName = sender != null && sender.isPlayer() && sender.player() != null ? sender.player().getName() : null;
|
String playerName = sender != null && sender.isPlayer() && sender.player() != null ? sender.player().getName() : null;
|
||||||
return new StudioOpenRequest(
|
return new StudioOpenRequest(
|
||||||
@@ -886,7 +903,8 @@ public final class StudioOpenCoordinator {
|
|||||||
openKind,
|
openKind,
|
||||||
false,
|
false,
|
||||||
progressConsumer,
|
progressConsumer,
|
||||||
onDone
|
onDone,
|
||||||
|
requestedAtNanos
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import art.arcane.iris.core.service.BoardSVC;
|
|||||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||||
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
|
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
|
||||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||||
|
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||||
import art.arcane.iris.util.common.scheduling.J;
|
import art.arcane.iris.util.common.scheduling.J;
|
||||||
import io.papermc.lib.PaperLib;
|
import io.papermc.lib.PaperLib;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
@@ -41,10 +42,13 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
public final class WorldRuntimeControlService {
|
public final class WorldRuntimeControlService {
|
||||||
|
private static final int STUDIO_ENTRY_VIEW_DISTANCE = 2;
|
||||||
private static final int MAX_SAFE_ENTRY_HORIZONTAL_RADIUS = 15;
|
private static final int MAX_SAFE_ENTRY_HORIZONTAL_RADIUS = 15;
|
||||||
private static final int MAX_SAFE_ENTRY_VERTICAL_SEARCH = 64;
|
private static final int SAFE_ENTRY_UPWARD_ALLOWANCE = 32;
|
||||||
private static final double BLOCK_CENTER = 0.5D;
|
private static final double BLOCK_CENTER = 0.5D;
|
||||||
private static final double COLLISION_EPSILON = 0.000001D;
|
private static final double COLLISION_EPSILON = 0.000001D;
|
||||||
|
private static final Method PLAYER_GET_VIEW_DISTANCE_METHOD = findPlayerMethod("getViewDistance");
|
||||||
|
private static final Method PLAYER_SET_VIEW_DISTANCE_METHOD = findPlayerMethod("setViewDistance", int.class);
|
||||||
private static final Set<Material> UNSAFE_ENTRY_MATERIALS = Set.of(
|
private static final Set<Material> UNSAFE_ENTRY_MATERIALS = Set.of(
|
||||||
Material.CACTUS,
|
Material.CACTUS,
|
||||||
Material.CAMPFIRE,
|
Material.CAMPFIRE,
|
||||||
@@ -302,7 +306,7 @@ public final class WorldRuntimeControlService {
|
|||||||
CompletableFuture<Location> future = new CompletableFuture<>();
|
CompletableFuture<Location> future = new CompletableFuture<>();
|
||||||
boolean scheduled = J.runRegion(world, chunkX, chunkZ, () -> {
|
boolean scheduled = J.runRegion(world, chunkX, chunkZ, () -> {
|
||||||
try {
|
try {
|
||||||
future.complete(findTopSafeLocation(world, source));
|
future.complete(findTopSafeLocationWithTicket(world, source, chunkX, chunkZ));
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
future.completeExceptionally(t);
|
future.completeExceptionally(t);
|
||||||
}
|
}
|
||||||
@@ -314,6 +318,22 @@ public final class WorldRuntimeControlService {
|
|||||||
return future;
|
return future;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Location findTopSafeLocationWithTicket(
|
||||||
|
World world,
|
||||||
|
Location source,
|
||||||
|
int chunkX,
|
||||||
|
int chunkZ
|
||||||
|
) {
|
||||||
|
boolean ticketAdded = world.addPluginChunkTicket(chunkX, chunkZ, BukkitPlatform.plugin());
|
||||||
|
try {
|
||||||
|
return findTopSafeLocation(world, source);
|
||||||
|
} finally {
|
||||||
|
if (ticketAdded) {
|
||||||
|
world.removePluginChunkTicket(chunkX, chunkZ, BukkitPlatform.plugin());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public CompletableFuture<Boolean> teleport(Player player, Location location) {
|
public CompletableFuture<Boolean> teleport(Player player, Location location) {
|
||||||
return scheduleTeleport(player, location, null);
|
return scheduleTeleport(player, location, null);
|
||||||
}
|
}
|
||||||
@@ -350,6 +370,7 @@ public final class WorldRuntimeControlService {
|
|||||||
|
|
||||||
CompletableFuture<Boolean> future = new CompletableFuture<>();
|
CompletableFuture<Boolean> future = new CompletableFuture<>();
|
||||||
GameModeRestore modeRestore = new GameModeRestore(player);
|
GameModeRestore modeRestore = new GameModeRestore(player);
|
||||||
|
PlayerViewDistanceRestore viewDistanceRestore = new PlayerViewDistanceRestore(player);
|
||||||
AtomicReference<CompletableFuture<Boolean>> activeTeleport = new AtomicReference<>();
|
AtomicReference<CompletableFuture<Boolean>> activeTeleport = new AtomicReference<>();
|
||||||
future.whenComplete((success, failure) -> {
|
future.whenComplete((success, failure) -> {
|
||||||
if (Boolean.TRUE.equals(success)) {
|
if (Boolean.TRUE.equals(success)) {
|
||||||
@@ -359,6 +380,7 @@ public final class WorldRuntimeControlService {
|
|||||||
if (teleport != null && !teleport.isDone()) {
|
if (teleport != null && !teleport.isDone()) {
|
||||||
teleport.cancel(false);
|
teleport.cancel(false);
|
||||||
}
|
}
|
||||||
|
viewDistanceRestore.restore();
|
||||||
modeRestore.restore();
|
modeRestore.restore();
|
||||||
});
|
});
|
||||||
boolean scheduled = J.runEntity(player, () -> {
|
boolean scheduled = J.runEntity(player, () -> {
|
||||||
@@ -368,7 +390,9 @@ public final class WorldRuntimeControlService {
|
|||||||
}
|
}
|
||||||
if (gameMode != null) {
|
if (gameMode != null) {
|
||||||
modeRestore.apply(gameMode);
|
modeRestore.apply(gameMode);
|
||||||
|
viewDistanceRestore.apply();
|
||||||
if (future.isDone()) {
|
if (future.isDone()) {
|
||||||
|
viewDistanceRestore.restore();
|
||||||
modeRestore.restore();
|
modeRestore.restore();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -393,6 +417,7 @@ public final class WorldRuntimeControlService {
|
|||||||
|
|
||||||
if (Boolean.TRUE.equals(success)) {
|
if (Boolean.TRUE.equals(success)) {
|
||||||
if (future.complete(true)) {
|
if (future.complete(true)) {
|
||||||
|
viewDistanceRestore.restore();
|
||||||
J.runEntity(player, () -> IrisServices.get(BoardSVC.class).updatePlayer(player));
|
J.runEntity(player, () -> IrisServices.get(BoardSVC.class).updatePlayer(player));
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -479,6 +504,7 @@ public final class WorldRuntimeControlService {
|
|||||||
z,
|
z,
|
||||||
minimumFloorY,
|
minimumFloorY,
|
||||||
maximumFloorY,
|
maximumFloorY,
|
||||||
|
source.getBlockY() - 1,
|
||||||
yaw,
|
yaw,
|
||||||
pitch
|
pitch
|
||||||
);
|
);
|
||||||
@@ -498,13 +524,14 @@ public final class WorldRuntimeControlService {
|
|||||||
int z,
|
int z,
|
||||||
int minimumFloorY,
|
int minimumFloorY,
|
||||||
int maximumFloorY,
|
int maximumFloorY,
|
||||||
|
int preferredFloorY,
|
||||||
float yaw,
|
float yaw,
|
||||||
float pitch
|
float pitch
|
||||||
) {
|
) {
|
||||||
int highestY = world.getHighestBlockYAt(x, z, HeightMap.MOTION_BLOCKING_NO_LEAVES);
|
int highestY = world.getHighestBlockYAt(x, z, HeightMap.MOTION_BLOCKING_NO_LEAVES);
|
||||||
int startingFloorY = Math.max(minimumFloorY, Math.min(maximumFloorY, highestY));
|
int preferredMaximumY = Math.min(maximumFloorY, preferredFloorY + SAFE_ENTRY_UPWARD_ALLOWANCE);
|
||||||
int lowestFloorY = Math.max(minimumFloorY, startingFloorY - MAX_SAFE_ENTRY_VERTICAL_SEARCH + 1);
|
int startingFloorY = Math.max(minimumFloorY, Math.min(preferredMaximumY, highestY));
|
||||||
for (int floorY = startingFloorY; floorY >= lowestFloorY; floorY--) {
|
for (int floorY = startingFloorY; floorY >= minimumFloorY; floorY--) {
|
||||||
Block floor = world.getBlockAt(x, floorY, z);
|
Block floor = world.getBlockAt(x, floorY, z);
|
||||||
if (!isSafeFloor(floor)) {
|
if (!isSafeFloor(floor)) {
|
||||||
continue;
|
continue;
|
||||||
@@ -777,6 +804,14 @@ public final class WorldRuntimeControlService {
|
|||||||
return method.invoke(instance);
|
return method.invoke(instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Method findPlayerMethod(String methodName, Class<?>... parameterTypes) {
|
||||||
|
try {
|
||||||
|
return Player.class.getMethod(methodName, parameterTypes);
|
||||||
|
} catch (NoSuchMethodException ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
interface TeleportExecutor {
|
interface TeleportExecutor {
|
||||||
CompletableFuture<Boolean> teleport(Player player, Location location);
|
CompletableFuture<Boolean> teleport(Player player, Location location);
|
||||||
@@ -822,4 +857,56 @@ public final class WorldRuntimeControlService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final class PlayerViewDistanceRestore {
|
||||||
|
private final Player player;
|
||||||
|
private final AtomicBoolean changed;
|
||||||
|
private final AtomicBoolean restored;
|
||||||
|
private int previousViewDistance;
|
||||||
|
|
||||||
|
private PlayerViewDistanceRestore(Player player) {
|
||||||
|
this.player = player;
|
||||||
|
changed = new AtomicBoolean(false);
|
||||||
|
restored = new AtomicBoolean(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void apply() {
|
||||||
|
if (PLAYER_GET_VIEW_DISTANCE_METHOD == null || PLAYER_SET_VIEW_DISTANCE_METHOD == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Object currentValue = PLAYER_GET_VIEW_DISTANCE_METHOD.invoke(player);
|
||||||
|
if (!(currentValue instanceof Number)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int currentViewDistance = ((Number) currentValue).intValue();
|
||||||
|
if (currentViewDistance <= STUDIO_ENTRY_VIEW_DISTANCE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
PLAYER_SET_VIEW_DISTANCE_METHOD.invoke(player, STUDIO_ENTRY_VIEW_DISTANCE);
|
||||||
|
previousViewDistance = currentViewDistance;
|
||||||
|
changed.set(true);
|
||||||
|
} catch (ReflectiveOperationException failure) {
|
||||||
|
IrisLogging.reportError("Failed to apply a temporary player view distance for a studio teleport.", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void restore() {
|
||||||
|
if (!changed.get() || !restored.compareAndSet(false, true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Runnable restoration = () -> {
|
||||||
|
try {
|
||||||
|
PLAYER_SET_VIEW_DISTANCE_METHOD.invoke(player, previousViewDistance);
|
||||||
|
} catch (ReflectiveOperationException failure) {
|
||||||
|
IrisLogging.reportError("Failed to restore a player's view distance after a studio teleport.", failure);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (!J.runEntity(player, restoration)) {
|
||||||
|
IrisLogging.reportError(
|
||||||
|
"Failed to restore a player's view distance after a studio teleport.",
|
||||||
|
new IllegalStateException("The player entity scheduler rejected the view-distance restoration."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,7 +83,6 @@ import java.util.UUID;
|
|||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
@@ -95,7 +94,6 @@ import art.arcane.volmlib.util.localization.MessageArgument;
|
|||||||
public class StudioSVC implements IrisService {
|
public class StudioSVC implements IrisService {
|
||||||
public static final String WORKSPACE_NAME = "packs";
|
public static final String WORKSPACE_NAME = "packs";
|
||||||
private static final long DOWNLOAD_SHUTDOWN_POLL_SECONDS = 15L;
|
private static final long DOWNLOAD_SHUTDOWN_POLL_SECONDS = 15L;
|
||||||
private static final long STUDIO_PLAYER_TELEPORT_TIMEOUT_SECONDS = 10L;
|
|
||||||
private static final Pattern PROJECT_NAME = Pattern.compile("[a-z0-9_-]+");
|
private static final Pattern PROJECT_NAME = Pattern.compile("[a-z0-9_-]+");
|
||||||
private static final AtomicCache<Integer> counter = new AtomicCache<>();
|
private static final AtomicCache<Integer> counter = new AtomicCache<>();
|
||||||
private final StudioTransitionQueue studioTransitions = new StudioTransitionQueue();
|
private final StudioTransitionQueue studioTransitions = new StudioTransitionQueue();
|
||||||
@@ -478,24 +476,14 @@ public class StudioSVC implements IrisService {
|
|||||||
|
|
||||||
public CompletableFuture<Boolean> teleportToActiveProject(Player player) {
|
public CompletableFuture<Boolean> teleportToActiveProject(Player player) {
|
||||||
Player target = Objects.requireNonNull(player, "Studio teleport player");
|
Player target = Objects.requireNonNull(player, "Studio teleport player");
|
||||||
AtomicBoolean admission = new AtomicBoolean(true);
|
return studioTransitions.submit(() -> {
|
||||||
long deadlineNanos = System.nanoTime()
|
|
||||||
+ TimeUnit.SECONDS.toNanos(STUDIO_PLAYER_TELEPORT_TIMEOUT_SECONDS);
|
|
||||||
CompletableFuture<Boolean> transition = studioTransitions.submit(() -> {
|
|
||||||
IrisProject project = activeProject;
|
IrisProject project = activeProject;
|
||||||
if (project == null || !project.isOpen()) {
|
if (project == null || !project.isOpen()) {
|
||||||
return CompletableFuture.failedFuture(new IllegalStateException(
|
return CompletableFuture.failedFuture(new IllegalStateException(
|
||||||
"No active Studio project is available for teleport."));
|
"No active Studio project is available for teleport."));
|
||||||
}
|
}
|
||||||
return StudioOpenCoordinator.get().teleportPlayerToProject(
|
return StudioOpenCoordinator.get().teleportPlayerToProject(project, target);
|
||||||
project,
|
|
||||||
target,
|
|
||||||
admission,
|
|
||||||
deadlineNanos);
|
|
||||||
});
|
});
|
||||||
transition.orTimeout(STUDIO_PLAYER_TELEPORT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
|
||||||
transition.whenComplete((ignored, failure) -> admission.set(false));
|
|
||||||
return transition;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void open(VolmitSender sender, String dimm) {
|
public void open(VolmitSender sender, String dimm) {
|
||||||
@@ -565,13 +553,20 @@ public class StudioSVC implements IrisService {
|
|||||||
StudioOpenCoordinator.StudioOpenKind openKind,
|
StudioOpenCoordinator.StudioOpenKind openKind,
|
||||||
Consumer<World> onDone
|
Consumer<World> onDone
|
||||||
) throws IrisException {
|
) throws IrisException {
|
||||||
|
long requestedAtNanos = System.nanoTime();
|
||||||
if (reportPackAdmissionFailure(sender, dimm) != null) {
|
if (reportPackAdmissionFailure(sender, dimm) != null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
StudioOpenCoordinator.StudioOpenKind requiredOpenKind = Objects.requireNonNull(
|
StudioOpenCoordinator.StudioOpenKind requiredOpenKind = Objects.requireNonNull(
|
||||||
openKind,
|
openKind,
|
||||||
"Studio open kind");
|
"Studio open kind");
|
||||||
studioTransitions.submit(() -> replaceActiveProject(sender, seed, dimm, requiredOpenKind, onDone))
|
studioTransitions.submit(() -> replaceActiveProject(
|
||||||
|
sender,
|
||||||
|
seed,
|
||||||
|
dimm,
|
||||||
|
requiredOpenKind,
|
||||||
|
onDone,
|
||||||
|
requestedAtNanos))
|
||||||
.whenComplete((ignored, throwable) -> {
|
.whenComplete((ignored, throwable) -> {
|
||||||
if (throwable == null) {
|
if (throwable == null) {
|
||||||
return;
|
return;
|
||||||
@@ -591,6 +586,7 @@ public class StudioSVC implements IrisService {
|
|||||||
Runnable beforeOpen,
|
Runnable beforeOpen,
|
||||||
Consumer<World> onDone
|
Consumer<World> onDone
|
||||||
) {
|
) {
|
||||||
|
long requestedAtNanos = System.nanoTime();
|
||||||
BrokenPackException failure = reportPackAdmissionFailure(sender, dimension);
|
BrokenPackException failure = reportPackAdmissionFailure(sender, dimension);
|
||||||
if (failure != null) {
|
if (failure != null) {
|
||||||
return CompletableFuture.failedFuture(failure);
|
return CompletableFuture.failedFuture(failure);
|
||||||
@@ -601,7 +597,8 @@ public class StudioSVC implements IrisService {
|
|||||||
dimension,
|
dimension,
|
||||||
Objects.requireNonNull(openKind, "Studio open kind"),
|
Objects.requireNonNull(openKind, "Studio open kind"),
|
||||||
Objects.requireNonNull(beforeOpen, "Studio before-open callback"),
|
Objects.requireNonNull(beforeOpen, "Studio before-open callback"),
|
||||||
Objects.requireNonNull(onDone, "Studio open completion callback")));
|
Objects.requireNonNull(onDone, "Studio open completion callback"),
|
||||||
|
requestedAtNanos));
|
||||||
}
|
}
|
||||||
|
|
||||||
private CompletableFuture<StudioOpenCoordinator.StudioOpenResult> replaceActiveProjectTracked(
|
private CompletableFuture<StudioOpenCoordinator.StudioOpenResult> replaceActiveProjectTracked(
|
||||||
@@ -610,7 +607,8 @@ public class StudioSVC implements IrisService {
|
|||||||
String dimension,
|
String dimension,
|
||||||
StudioOpenCoordinator.StudioOpenKind openKind,
|
StudioOpenCoordinator.StudioOpenKind openKind,
|
||||||
Runnable beforeOpen,
|
Runnable beforeOpen,
|
||||||
Consumer<World> onDone
|
Consumer<World> onDone,
|
||||||
|
long requestedAtNanos
|
||||||
) {
|
) {
|
||||||
return closeActiveProject().thenCompose(closeResult -> {
|
return closeActiveProject().thenCompose(closeResult -> {
|
||||||
if (closeResult == null) {
|
if (closeResult == null) {
|
||||||
@@ -621,7 +619,13 @@ public class StudioSVC implements IrisService {
|
|||||||
return CompletableFuture.failedFuture(closeResult.failureCause());
|
return CompletableFuture.failedFuture(closeResult.failureCause());
|
||||||
}
|
}
|
||||||
beforeOpen.run();
|
beforeOpen.run();
|
||||||
return beginStudioOpenTracked(sender, seed, dimension, openKind, onDone);
|
return beginStudioOpenTracked(
|
||||||
|
sender,
|
||||||
|
seed,
|
||||||
|
dimension,
|
||||||
|
openKind,
|
||||||
|
onDone,
|
||||||
|
requestedAtNanos);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -630,13 +634,14 @@ public class StudioSVC implements IrisService {
|
|||||||
long seed,
|
long seed,
|
||||||
String dimension,
|
String dimension,
|
||||||
StudioOpenCoordinator.StudioOpenKind openKind,
|
StudioOpenCoordinator.StudioOpenKind openKind,
|
||||||
Consumer<World> onDone
|
Consumer<World> onDone,
|
||||||
|
long requestedAtNanos
|
||||||
) {
|
) {
|
||||||
IrisProject project = new IrisProject(new File(getWorkspaceFolder(), dimension));
|
IrisProject project = new IrisProject(new File(getWorkspaceFolder(), dimension));
|
||||||
activeProject = project;
|
activeProject = project;
|
||||||
CompletableFuture<StudioOpenCoordinator.StudioOpenResult> opening;
|
CompletableFuture<StudioOpenCoordinator.StudioOpenResult> opening;
|
||||||
try {
|
try {
|
||||||
opening = project.open(sender, seed, openKind, onDone);
|
opening = project.open(sender, seed, openKind, onDone, requestedAtNanos);
|
||||||
} catch (IrisException exception) {
|
} catch (IrisException exception) {
|
||||||
if (activeProject == project) {
|
if (activeProject == project) {
|
||||||
activeProject = null;
|
activeProject = null;
|
||||||
@@ -663,7 +668,8 @@ public class StudioSVC implements IrisService {
|
|||||||
long seed,
|
long seed,
|
||||||
String dimension,
|
String dimension,
|
||||||
StudioOpenCoordinator.StudioOpenKind openKind,
|
StudioOpenCoordinator.StudioOpenKind openKind,
|
||||||
Consumer<World> onDone
|
Consumer<World> onDone,
|
||||||
|
long requestedAtNanos
|
||||||
) {
|
) {
|
||||||
return closeActiveProjectForReplacement(sender).handle((closeResult, closeThrowable) -> {
|
return closeActiveProjectForReplacement(sender).handle((closeResult, closeThrowable) -> {
|
||||||
if (closeThrowable != null) {
|
if (closeThrowable != null) {
|
||||||
@@ -691,7 +697,13 @@ public class StudioSVC implements IrisService {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}).thenCompose(closed -> closed
|
}).thenCompose(closed -> closed
|
||||||
? beginStudioOpen(sender, seed, dimension, openKind, onDone)
|
? beginStudioOpen(
|
||||||
|
sender,
|
||||||
|
seed,
|
||||||
|
dimension,
|
||||||
|
openKind,
|
||||||
|
onDone,
|
||||||
|
requestedAtNanos)
|
||||||
: CompletableFuture.completedFuture(null));
|
: CompletableFuture.completedFuture(null));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -700,7 +712,8 @@ public class StudioSVC implements IrisService {
|
|||||||
long seed,
|
long seed,
|
||||||
String dimension,
|
String dimension,
|
||||||
StudioOpenCoordinator.StudioOpenKind openKind,
|
StudioOpenCoordinator.StudioOpenKind openKind,
|
||||||
Consumer<World> onDone
|
Consumer<World> onDone,
|
||||||
|
long requestedAtNanos
|
||||||
) {
|
) {
|
||||||
IrisProject project = new IrisProject(new File(getWorkspaceFolder(), dimension));
|
IrisProject project = new IrisProject(new File(getWorkspaceFolder(), dimension));
|
||||||
activeProject = project;
|
activeProject = project;
|
||||||
@@ -710,7 +723,8 @@ public class StudioSVC implements IrisService {
|
|||||||
sender,
|
sender,
|
||||||
seed,
|
seed,
|
||||||
openKind,
|
openKind,
|
||||||
onDone);
|
onDone,
|
||||||
|
requestedAtNanos);
|
||||||
} catch (IrisException e) {
|
} catch (IrisException e) {
|
||||||
if (activeProject == project) {
|
if (activeProject == project) {
|
||||||
activeProject = null;
|
activeProject = null;
|
||||||
|
|||||||
@@ -306,6 +306,9 @@ public class IrisCreator {
|
|||||||
world = J.sfut(() -> INMS.get().createWorldAsync(wc, request))
|
world = J.sfut(() -> INMS.get().createWorldAsync(wc, request))
|
||||||
.thenCompose(Function.identity())
|
.thenCompose(Function.identity())
|
||||||
.get(WORLD_CREATE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
.get(WORLD_CREATE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||||
|
if (!studio && !benchmark) {
|
||||||
|
awaitInitialSpawnPreparation(access, name);
|
||||||
|
}
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
done.set(true);
|
done.set(true);
|
||||||
cancelRepeatingTask(createProgressTask);
|
cancelRepeatingTask(createProgressTask);
|
||||||
@@ -603,6 +606,21 @@ public class IrisCreator {
|
|||||||
return taskId;
|
return taskId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void awaitInitialSpawnPreparation(
|
||||||
|
PlatformChunkGenerator generator,
|
||||||
|
String worldName
|
||||||
|
) throws InterruptedException, ExecutionException, TimeoutException {
|
||||||
|
CompletableFuture<Void> initialSpawnReady = Objects.requireNonNull(
|
||||||
|
generator.getInitialSpawnReady(),
|
||||||
|
"Initial spawn preparation future");
|
||||||
|
try {
|
||||||
|
initialSpawnReady.get(WORLD_CREATE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||||
|
} catch (TimeoutException failure) {
|
||||||
|
throw new TimeoutException("Initial spawn preparation timed out for world \""
|
||||||
|
+ worldName + "\".");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private AtomicInteger startPregenProgressReporter(
|
private AtomicInteger startPregenProgressReporter(
|
||||||
AtomicDouble progress,
|
AtomicDouble progress,
|
||||||
AtomicBoolean done,
|
AtomicBoolean done,
|
||||||
|
|||||||
@@ -29,10 +29,14 @@ import art.arcane.iris.engine.object.IrisDecorationPart;
|
|||||||
import art.arcane.iris.engine.object.IrisDecorator;
|
import art.arcane.iris.engine.object.IrisDecorator;
|
||||||
import art.arcane.iris.engine.object.IrisGenerator;
|
import art.arcane.iris.engine.object.IrisGenerator;
|
||||||
import art.arcane.iris.engine.object.IrisInterpolator;
|
import art.arcane.iris.engine.object.IrisInterpolator;
|
||||||
|
import art.arcane.iris.engine.object.IrisMaterialPalette;
|
||||||
import art.arcane.iris.engine.object.IrisRegion;
|
import art.arcane.iris.engine.object.IrisRegion;
|
||||||
|
import art.arcane.iris.engine.object.IrisRiverCaves;
|
||||||
|
import art.arcane.iris.engine.object.IrisRiverDeepPools;
|
||||||
|
import art.arcane.iris.engine.object.IrisRiverNetwork;
|
||||||
import art.arcane.iris.engine.object.IrisRiverOverride;
|
import art.arcane.iris.engine.object.IrisRiverOverride;
|
||||||
import art.arcane.iris.engine.object.IrisRiverRoutingPolicy;
|
import art.arcane.iris.engine.object.IrisRiverRoutingPolicy;
|
||||||
import art.arcane.iris.engine.object.IrisRiverWaterMode;
|
import art.arcane.iris.engine.object.IrisRiverWater;
|
||||||
import art.arcane.iris.engine.object.IrisShapedGeneratorStyle;
|
import art.arcane.iris.engine.object.IrisShapedGeneratorStyle;
|
||||||
import art.arcane.iris.engine.river.runtime.IrisRiverRuntime;
|
import art.arcane.iris.engine.river.runtime.IrisRiverRuntime;
|
||||||
import art.arcane.iris.engine.river.runtime.IrisRiverRuntimeContext;
|
import art.arcane.iris.engine.river.runtime.IrisRiverRuntimeContext;
|
||||||
@@ -40,6 +44,7 @@ import art.arcane.iris.engine.river.runtime.IrisRiverSurfaceSample;
|
|||||||
import art.arcane.iris.engine.river.RiverRouteState;
|
import art.arcane.iris.engine.river.RiverRouteState;
|
||||||
import art.arcane.iris.engine.river.RiverSample;
|
import art.arcane.iris.engine.river.RiverSample;
|
||||||
import art.arcane.iris.engine.river.RiverSection;
|
import art.arcane.iris.engine.river.RiverSection;
|
||||||
|
import art.arcane.iris.engine.river.cave.RiverCaveFluidKind;
|
||||||
import art.arcane.iris.spi.IrisPlatforms;
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
import art.arcane.iris.spi.PlatformBiome;
|
import art.arcane.iris.spi.PlatformBiome;
|
||||||
@@ -66,6 +71,7 @@ import java.util.HashMap;
|
|||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.IdentityHashMap;
|
import java.util.IdentityHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
@@ -132,6 +138,8 @@ public class IrisComplex implements DataProvider {
|
|||||||
private ProceduralStream<IrisDecorator> shoreSurfaceDecoration;
|
private ProceduralStream<IrisDecorator> shoreSurfaceDecoration;
|
||||||
private ProceduralStream<PlatformBlockState> rockStream;
|
private ProceduralStream<PlatformBlockState> rockStream;
|
||||||
private ProceduralStream<PlatformBlockState> fluidStream;
|
private ProceduralStream<PlatformBlockState> fluidStream;
|
||||||
|
private ProceduralStream<PlatformBlockState> riverFluidStream;
|
||||||
|
private ProceduralStream<PlatformBlockState> riverDeepPoolFluidStream;
|
||||||
private IrisBiome focusBiome;
|
private IrisBiome focusBiome;
|
||||||
private IrisRegion focusRegion;
|
private IrisRegion focusRegion;
|
||||||
private Map<IrisInterpolator, IdentityHashMap<IrisBiome, GeneratorBounds>> generatorBounds;
|
private Map<IrisInterpolator, IdentityHashMap<IrisBiome, GeneratorBounds>> generatorBounds;
|
||||||
@@ -213,6 +221,29 @@ public class IrisComplex implements DataProvider {
|
|||||||
.select(engine.getDimension().getRockPalette().getBlockData(data));
|
.select(engine.getDimension().getRockPalette().getBlockData(data));
|
||||||
fluidStream = engine.getDimension().getFluidPalette().getLayerGenerator(rng.nextParallelRNG(78), data).stream()
|
fluidStream = engine.getDimension().getFluidPalette().getLayerGenerator(rng.nextParallelRNG(78), data).stream()
|
||||||
.select(engine.getDimension().getFluidPalette().getBlockData(data));
|
.select(engine.getDimension().getFluidPalette().getBlockData(data));
|
||||||
|
IrisRiverNetwork configuredRivers = engine.getDimension().getRivers();
|
||||||
|
IrisRiverWater configuredRiverWater = configuredRivers == null ? null : configuredRivers.getWater();
|
||||||
|
riverFluidStream = configuredRivers != null && configuredRivers.isEnabled()
|
||||||
|
? configuredFluidStream(
|
||||||
|
Objects.requireNonNull(configuredRiverWater).getFluidPalette(),
|
||||||
|
rng.nextParallelRNG(79),
|
||||||
|
"River water"
|
||||||
|
)
|
||||||
|
: fluidStream;
|
||||||
|
IrisRiverCaves configuredRiverCaves = configuredRivers == null ? null : configuredRivers.getCaves();
|
||||||
|
IrisRiverDeepPools configuredDeepPools = configuredRiverCaves == null
|
||||||
|
? null
|
||||||
|
: configuredRiverCaves.getDeepPools();
|
||||||
|
riverDeepPoolFluidStream = configuredRivers != null
|
||||||
|
&& configuredRivers.isEnabled()
|
||||||
|
&& configuredDeepPools != null
|
||||||
|
&& configuredDeepPools.isEnabled()
|
||||||
|
? configuredFluidStream(
|
||||||
|
configuredDeepPools.getFluidPalette(),
|
||||||
|
rng.nextParallelRNG(80),
|
||||||
|
"River deep-pool"
|
||||||
|
)
|
||||||
|
: riverFluidStream;
|
||||||
regionStyleStream = engine.getDimension().getRegionStyle().create(rng.nextParallelRNG(883), getData()).stream()
|
regionStyleStream = engine.getDimension().getRegionStyle().create(rng.nextParallelRNG(883), getData()).stream()
|
||||||
.zoom(engine.getDimension().getRegionZoom());
|
.zoom(engine.getDimension().getRegionZoom());
|
||||||
regionIdentityStream = regionStyleStream.fit(Integer.MIN_VALUE, Integer.MAX_VALUE);
|
regionIdentityStream = regionStyleStream.fit(Integer.MIN_VALUE, Integer.MAX_VALUE);
|
||||||
@@ -303,16 +334,16 @@ public class IrisComplex implements DataProvider {
|
|||||||
.cache2D("naturalTrueBiomeStream", engine, cacheSize);
|
.cache2D("naturalTrueBiomeStream", engine, cacheSize);
|
||||||
if (engine.getDimension().getRivers() != null && engine.getDimension().getRivers().isEnabled()) {
|
if (engine.getDimension().getRivers() != null && engine.getDimension().getRivers().isEnabled()) {
|
||||||
ProceduralStream<Boolean> naturalOceanStream = createNaturalOceanStream(
|
ProceduralStream<Boolean> naturalOceanStream = createNaturalOceanStream(
|
||||||
naturalHeightStream,
|
|
||||||
bridgeStream,
|
bridgeStream,
|
||||||
focusBiome,
|
focusBiome
|
||||||
fluidHeight,
|
|
||||||
engine.getDimension().getRivers().getWater().getMode()
|
|
||||||
).cache2D("naturalOceanStream", engine, cacheSize);
|
).cache2D("naturalOceanStream", engine, cacheSize);
|
||||||
|
int riverFluidHeight = engine.getDimension().getRivers().getWater().getFluidHeight()
|
||||||
|
- engine.getDimension().getMinHeight();
|
||||||
riverRuntime = new IrisRiverRuntime(new IrisRiverRuntimeContext(
|
riverRuntime = new IrisRiverRuntime(new IrisRiverRuntimeContext(
|
||||||
engine.getSeedManager().getBodies(),
|
engine.getSeedManager().getBodies(),
|
||||||
engine.getDimension().getRivers(),
|
engine.getDimension().getRivers(),
|
||||||
data,
|
data,
|
||||||
|
riverFluidHeight,
|
||||||
(int) Math.round(fluidHeight),
|
(int) Math.round(fluidHeight),
|
||||||
IrisEngineMantle.isRiverHydrologyEnabled(engine.getDimension()),
|
IrisEngineMantle.isRiverHydrologyEnabled(engine.getDimension()),
|
||||||
IrisEngineMantle.isRiverCaveHydrologyEnabled(engine.getDimension()),
|
IrisEngineMantle.isRiverCaveHydrologyEnabled(engine.getDimension()),
|
||||||
@@ -458,23 +489,49 @@ public class IrisComplex implements DataProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static ProceduralStream<Boolean> createNaturalOceanStream(
|
static ProceduralStream<Boolean> createNaturalOceanStream(
|
||||||
ProceduralStream<Double> naturalHeightStream,
|
|
||||||
ProceduralStream<InferredType> bridgeStream,
|
ProceduralStream<InferredType> bridgeStream,
|
||||||
IrisBiome focusBiome,
|
IrisBiome focusBiome
|
||||||
double fluidHeight,
|
|
||||||
IrisRiverWaterMode waterMode
|
|
||||||
) {
|
) {
|
||||||
if (focusBiome != null) {
|
if (focusBiome != null) {
|
||||||
boolean ocean = focusBiome.getInferredType() == InferredType.SEA;
|
boolean ocean = focusBiome.getInferredType() == InferredType.SEA;
|
||||||
return ProceduralStream.of((x, z) -> ocean, Interpolated.BOOLEAN);
|
return ProceduralStream.of((x, z) -> ocean, Interpolated.BOOLEAN);
|
||||||
}
|
}
|
||||||
if (waterMode == IrisRiverWaterMode.SEA_LEVEL) {
|
|
||||||
return bridgeStream.convert(type -> type == InferredType.SEA);
|
return bridgeStream.convert(type -> type == InferredType.SEA);
|
||||||
}
|
}
|
||||||
return ProceduralStream.of(
|
|
||||||
(x, z) -> naturalHeightStream.getDouble(x, z) < fluidHeight - 1D,
|
private ProceduralStream<PlatformBlockState> configuredFluidStream(
|
||||||
Interpolated.BOOLEAN
|
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");
|
||||||
|
}
|
||||||
|
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) {
|
public ProceduralStream<IrisBiome> getBiomeStream(InferredType type) {
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ import java.util.Locale;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.CompletionException;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
@@ -93,6 +94,7 @@ public class IrisEngine implements Engine {
|
|||||||
private final ChronoLatch perSecondLatch;
|
private final ChronoLatch perSecondLatch;
|
||||||
private final ChronoLatch perSecondBudLatch;
|
private final ChronoLatch perSecondBudLatch;
|
||||||
private final EngineMetrics metrics;
|
private final EngineMetrics metrics;
|
||||||
|
private final CompletableFuture<Void> generationCacheWarm;
|
||||||
private final boolean studio;
|
private final boolean studio;
|
||||||
private final AtomicRollingSequence wallClock;
|
private final AtomicRollingSequence wallClock;
|
||||||
@Getter(AccessLevel.NONE)
|
@Getter(AccessLevel.NONE)
|
||||||
@@ -186,6 +188,7 @@ public class IrisEngine implements Engine {
|
|||||||
bud = new AtomicInteger(0);
|
bud = new AtomicInteger(0);
|
||||||
buds = new AtomicInteger(0);
|
buds = new AtomicInteger(0);
|
||||||
metrics = new EngineMetrics(32);
|
metrics = new EngineMetrics(32);
|
||||||
|
generationCacheWarm = new CompletableFuture<>();
|
||||||
cleanLatch = new ChronoLatch(10000);
|
cleanLatch = new ChronoLatch(10000);
|
||||||
generatedLast = new AtomicInteger(0);
|
generatedLast = new AtomicInteger(0);
|
||||||
perSecond = new AtomicDouble(0);
|
perSecond = new AtomicDouble(0);
|
||||||
@@ -232,16 +235,17 @@ public class IrisEngine implements Engine {
|
|||||||
runtimeBuilder.publishRuntime(initialRuntime, null);
|
runtimeBuilder.publishRuntime(initialRuntime, null);
|
||||||
IrisLogging.debug("[IrisEngine timing] setupEngine total=" + (M.ms() - _t0) + "ms");
|
IrisLogging.debug("[IrisEngine timing] setupEngine total=" + (M.ms() - _t0) + "ms");
|
||||||
logStudioInitializationPhase("build_runtime", phaseStartedAt, false);
|
logStudioInitializationPhase("build_runtime", phaseStartedAt, false);
|
||||||
_t0 = M.ms();
|
|
||||||
phaseStartedAt = System.nanoTime();
|
phaseStartedAt = System.nanoTime();
|
||||||
if (requiredMode.warmGenerationCaches()) {
|
if (requiredMode.warmGenerationCaches()) {
|
||||||
GenerationCacheWarmer.warm(this);
|
if (requiredMode.studio()) {
|
||||||
|
startGenerationCacheWarm(phaseStartedAt);
|
||||||
|
} else {
|
||||||
|
warmGenerationCaches(phaseStartedAt);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
generationCacheWarm.complete(null);
|
||||||
|
logStudioInitializationPhase("generation_cache_warm", phaseStartedAt, true);
|
||||||
}
|
}
|
||||||
IrisLogging.debug("[IrisEngine timing] cache warm total=" + (M.ms() - _t0) + "ms");
|
|
||||||
logStudioInitializationPhase(
|
|
||||||
"generation_cache_warm",
|
|
||||||
phaseStartedAt,
|
|
||||||
!requiredMode.warmGenerationCaches());
|
|
||||||
EngineTickRegistry.registerTicking(this);
|
EngineTickRegistry.registerTicking(this);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
shutdownSequence.cleanupFailedConstruction(e);
|
shutdownSequence.cleanupFailedConstruction(e);
|
||||||
@@ -250,6 +254,21 @@ public class IrisEngine implements Engine {
|
|||||||
IrisLogging.debug("Engine Initialized " + getCacheID());
|
IrisLogging.debug("Engine Initialized " + getCacheID());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void awaitGenerationCacheWarm() {
|
||||||
|
if (generationCacheWarm.isDone() && !generationCacheWarm.isCompletedExceptionally()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
generationCacheWarm.join();
|
||||||
|
} catch (CompletionException failure) {
|
||||||
|
throw new IllegalStateException("Iris generation caches did not become ready.", failure.getCause());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isGenerationCacheWarmPending() {
|
||||||
|
return !generationCacheWarm.isDone();
|
||||||
|
}
|
||||||
|
|
||||||
private void logStudioInitializationPhase(String phase, long startedAtNanos, boolean skipped) {
|
private void logStudioInitializationPhase(String phase, long startedAtNanos, boolean skipped) {
|
||||||
if (!studio) {
|
if (!studio) {
|
||||||
return;
|
return;
|
||||||
@@ -262,6 +281,32 @@ public class IrisEngine implements Engine {
|
|||||||
Boolean.toString(skipped));
|
Boolean.toString(skipped));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void startGenerationCacheWarm(long phaseStartedAtNanos) {
|
||||||
|
if (!backgroundTasks.scheduleTrackedTask(() -> warmGenerationCaches(phaseStartedAtNanos))) {
|
||||||
|
throw new IllegalStateException("Iris background task admission closed before generation cache warming.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void warmGenerationCaches(long phaseStartedAtNanos) {
|
||||||
|
long startedAtMillis = M.ms();
|
||||||
|
try {
|
||||||
|
GenerationCacheWarmer.warm(this);
|
||||||
|
generationCacheWarm.complete(null);
|
||||||
|
} catch (Throwable failure) {
|
||||||
|
generationCacheWarm.completeExceptionally(failure);
|
||||||
|
if (failure instanceof Error error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
if (failure instanceof RuntimeException runtimeException) {
|
||||||
|
throw runtimeException;
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("Generation cache warming failed.", failure);
|
||||||
|
} finally {
|
||||||
|
IrisLogging.debug("[IrisEngine timing] cache warm total=" + (M.ms() - startedAtMillis) + "ms");
|
||||||
|
logStudioInitializationPhase("generation_cache_warm", phaseStartedAtNanos, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void verifySeed() {
|
private void verifySeed() {
|
||||||
if (getEngineData().getSeed() != null && getEngineData().getSeed() != target.getWorld().getRawWorldSeed()) {
|
if (getEngineData().getSeed() != null && getEngineData().getSeed() != target.getWorld().getRawWorldSeed()) {
|
||||||
target.getWorld().setRawWorldSeed(getEngineData().getSeed());
|
target.getWorld().setRawWorldSeed(getEngineData().getSeed());
|
||||||
@@ -323,6 +368,7 @@ public class IrisEngine implements Engine {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void generateMatter(int x, int z, boolean multicore, ChunkContext context) {
|
public void generateMatter(int x, int z, boolean multicore, ChunkContext context) {
|
||||||
|
awaitGenerationCacheWarm();
|
||||||
try (GenerationSessionLease lease = acquireGenerationLease("matter_generate");
|
try (GenerationSessionLease lease = acquireGenerationLease("matter_generate");
|
||||||
IrisContext.Scope ignored = IrisContext.open(this, lease.sessionId(), context)) {
|
IrisContext.Scope ignored = IrisContext.open(this, lease.sessionId(), context)) {
|
||||||
IrisComplex activeComplex = getComplex();
|
IrisComplex activeComplex = getComplex();
|
||||||
@@ -574,6 +620,7 @@ public class IrisEngine implements Engine {
|
|||||||
@BlockCoordinates
|
@BlockCoordinates
|
||||||
@Override
|
@Override
|
||||||
public void generate(int x, int z, Hunk<PlatformBlockState> vblocks, Hunk<PlatformBiome> vbiomes, boolean multicore) throws WrongEngineBroException {
|
public void generate(int x, int z, Hunk<PlatformBlockState> vblocks, Hunk<PlatformBiome> vbiomes, boolean multicore) throws WrongEngineBroException {
|
||||||
|
awaitGenerationCacheWarm();
|
||||||
try (GenerationSessionLease lease = acquireGenerationLease("chunk_generate");
|
try (GenerationSessionLease lease = acquireGenerationLease("chunk_generate");
|
||||||
IrisContext.Scope generationScope = IrisContext.open(this, lease.sessionId(), null)) {
|
IrisContext.Scope generationScope = IrisContext.open(this, lease.sessionId(), null)) {
|
||||||
getEngineData().getStatistics().generatedChunk();
|
getEngineData().getStatistics().generatedChunk();
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ import java.util.concurrent.RejectedExecutionException;
|
|||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
|
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
@Data
|
@Data
|
||||||
@@ -78,7 +77,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
final WorldBlockDropRouter blockDropRouter = new WorldBlockDropRouter(this);
|
final WorldBlockDropRouter blockDropRouter = new WorldBlockDropRouter(this);
|
||||||
@Getter(AccessLevel.NONE)
|
@Getter(AccessLevel.NONE)
|
||||||
@Setter(AccessLevel.NONE)
|
@Setter(AccessLevel.NONE)
|
||||||
final WorldTeleportWarmup teleportWarmup = new WorldTeleportWarmup(this);
|
final WorldTeleportWarmup teleportWarmup = new WorldTeleportWarmup();
|
||||||
private boolean looperStopped;
|
private boolean looperStopped;
|
||||||
private volatile boolean cleanupServiceStopped;
|
private volatile boolean cleanupServiceStopped;
|
||||||
volatile int entityCount = 0;
|
volatile int entityCount = 0;
|
||||||
@@ -193,10 +192,6 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
AtomicBoolean ignoreTeleport() {
|
|
||||||
return ignoreTP;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTick() {
|
public void onTick() {
|
||||||
|
|
||||||
|
|||||||
@@ -18,87 +18,53 @@
|
|||||||
|
|
||||||
package art.arcane.iris.engine;
|
package art.arcane.iris.engine;
|
||||||
|
|
||||||
import art.arcane.iris.core.localization.IrisLanguage;
|
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||||
import art.arcane.iris.core.localization.RuntimeUiMessages;
|
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
import art.arcane.iris.util.common.parallel.MultiBurst;
|
|
||||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
|
||||||
import art.arcane.iris.util.common.scheduling.J;
|
|
||||||
import art.arcane.iris.util.common.scheduling.jobs.QueueJob;
|
|
||||||
import art.arcane.volmlib.util.collection.KList;
|
|
||||||
import io.papermc.lib.PaperLib;
|
|
||||||
import org.bukkit.Chunk;
|
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.event.player.PlayerTeleportEvent;
|
import org.bukkit.event.player.PlayerTeleportEvent;
|
||||||
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ExecutionException;
|
|
||||||
import java.util.concurrent.Future;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cancels a teleport, loads the destination chunks off the main thread and then replays the teleport
|
|
||||||
* on the thread that owns the player. The replay sets the manager's ignore flag so the reissued
|
|
||||||
* teleport is not intercepted again.
|
|
||||||
*/
|
|
||||||
final class WorldTeleportWarmup {
|
final class WorldTeleportWarmup {
|
||||||
private final IrisWorldManager manager;
|
|
||||||
|
|
||||||
WorldTeleportWarmup(IrisWorldManager manager) {
|
|
||||||
this.manager = manager;
|
|
||||||
}
|
|
||||||
|
|
||||||
void teleportAsync(PlayerTeleportEvent e) {
|
void teleportAsync(PlayerTeleportEvent e) {
|
||||||
|
Location destination = e.getTo();
|
||||||
|
if (destination == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Player player = e.getPlayer();
|
||||||
|
PlayerTeleportEvent.TeleportCause cause = e.getCause();
|
||||||
e.setCancelled(true);
|
e.setCancelled(true);
|
||||||
warmupAreaAsync(e.getPlayer(), e.getTo(), () -> J.runEntity(e.getPlayer(), manager.managedTask(
|
CompletableFuture<Boolean> teleport;
|
||||||
"bukkit_world_manager_teleport",
|
|
||||||
() -> {
|
|
||||||
manager.ignoreTeleport().set(true);
|
|
||||||
e.getPlayer().teleport(e.getTo(), e.getCause());
|
|
||||||
manager.ignoreTeleport().set(false);
|
|
||||||
})));
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
try {
|
||||||
chunkFuture.get();
|
teleport = BukkitPlatform.teleportAsync(
|
||||||
} catch (InterruptedException ex) {
|
player,
|
||||||
Thread.currentThread().interrupt();
|
destination.clone(),
|
||||||
IrisLogging.debug("Chunk warmup interrupted while loading async teleport chunk.");
|
cause);
|
||||||
} catch (ExecutionException ex) {
|
} catch (Throwable failure) {
|
||||||
IrisLogging.reportError(ex);
|
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."));
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private void reportFailure(Player player, Location destination, Throwable failure) {
|
||||||
public String getName() {
|
IrisLogging.error("Async teleport into Iris world failed for " + player.getName()
|
||||||
return IrisLanguage.text(RuntimeUiMessages.JOB_LOADING_CHUNKS);
|
+ " at " + destination.getBlockX() + ", " + destination.getBlockY() + ", "
|
||||||
}
|
+ destination.getBlockZ() + ".");
|
||||||
}.queue(futures).execute(new VolmitSender(player), true, r);
|
IrisLogging.reportError(failure);
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ public class IrisDecorantActuator extends EngineAssignedActuator<PlatformBlockSt
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (height < surfaceFluidHeight && PREDICATE_SOLID.test(output.get(i, height, j))
|
if (height < surfaceFluidHeight && PREDICATE_SOLID.test(output.get(i, height, j))
|
||||||
&& height + 1 < output.getHeight() && B.isWater(output.get(i, height + 1, j))) {
|
&& height + 1 < output.getHeight() && B.isFluid(output.get(i, height + 1, j))) {
|
||||||
getSeaSurfaceDecorator().decorate(i, j,
|
getSeaSurfaceDecorator().decorate(i, j,
|
||||||
realX, Math.round(i + 1), Math.round(x + i - 1),
|
realX, Math.round(i + 1), Math.round(x + i - 1),
|
||||||
realZ, Math.round(z + j + 1), Math.round(z + j - 1),
|
realZ, Math.round(z + j + 1), Math.round(z + j - 1),
|
||||||
|
|||||||
@@ -92,7 +92,6 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
|
|||||||
boolean hideOres = dimension.isHideOresForHiddenOre();
|
boolean hideOres = dimension.isHideOresForHiddenOre();
|
||||||
ChunkedDataCache<IrisBiome> biomeCache = context.getBiome();
|
ChunkedDataCache<IrisBiome> biomeCache = context.getBiome();
|
||||||
ChunkedDataCache<IrisRegion> regionCache = context.getRegion();
|
ChunkedDataCache<IrisRegion> regionCache = context.getRegion();
|
||||||
ChunkedDataCache<PlatformBlockState> fluidCache = context.getFluid();
|
|
||||||
ChunkedDataCache<PlatformBlockState> rockCache = context.getRock();
|
ChunkedDataCache<PlatformBlockState> rockCache = context.getRock();
|
||||||
int realX = xf + x;
|
int realX = xf + x;
|
||||||
UpperDimensionContext upperContext = getEngine().getUpperContext();
|
UpperDimensionContext upperContext = getEngine().getUpperContext();
|
||||||
@@ -118,7 +117,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
|
|||||||
}
|
}
|
||||||
|
|
||||||
int topY = Math.min(hf, chunkHeight - 1);
|
int topY = Math.min(hf, chunkHeight - 1);
|
||||||
PlatformBlockState fluid = fluidCache.get(xf, zf);
|
PlatformBlockState fluid = complex.resolveSurfaceFluid(realX, realZ);
|
||||||
PlatformBlockState rock = rockCache.get(xf, zf);
|
PlatformBlockState rock = rockCache.get(xf, zf);
|
||||||
PlatformBlockState mappedSurfaceBlock = complex.getImageMapRuntime().sampleSurfaceBlock(realX, realZ);
|
PlatformBlockState mappedSurfaceBlock = complex.getImageMapRuntime().sampleSurfaceBlock(realX, realZ);
|
||||||
KList<IrisOreGenerator> biomeSurfaceOres = hideOres ? null : biome.getSurfaceOreGenerators();
|
KList<IrisOreGenerator> biomeSurfaceOres = hideOres ? null : biome.getSurfaceOreGenerators();
|
||||||
|
|||||||
+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.format.C;
|
||||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||||
import art.arcane.iris.util.common.scheduling.J;
|
import art.arcane.iris.util.common.scheduling.J;
|
||||||
|
import org.bukkit.Location;
|
||||||
import org.bukkit.Sound;
|
import org.bukkit.Sound;
|
||||||
|
import org.bukkit.World;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.event.EventHandler;
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.EventPriority;
|
||||||
import org.bukkit.event.Listener;
|
import org.bukkit.event.Listener;
|
||||||
import org.bukkit.event.block.BlockBreakEvent;
|
import org.bukkit.event.block.BlockBreakEvent;
|
||||||
import org.bukkit.event.block.BlockPlaceEvent;
|
import org.bukkit.event.block.BlockPlaceEvent;
|
||||||
|
import org.bukkit.event.player.PlayerTeleportEvent;
|
||||||
import org.bukkit.event.world.ChunkLoadEvent;
|
import org.bukkit.event.world.ChunkLoadEvent;
|
||||||
import org.bukkit.event.world.ChunkUnloadEvent;
|
import org.bukkit.event.world.ChunkUnloadEvent;
|
||||||
import org.bukkit.event.world.WorldSaveEvent;
|
import org.bukkit.event.world.WorldSaveEvent;
|
||||||
@@ -45,7 +49,6 @@ public abstract class EngineAssignedWorldManager extends EngineAssignedComponent
|
|||||||
private boolean listenerRegistered;
|
private boolean listenerRegistered;
|
||||||
private boolean closeRequested;
|
private boolean closeRequested;
|
||||||
private int taskId;
|
private int taskId;
|
||||||
protected AtomicBoolean ignoreTP = new AtomicBoolean(false);
|
|
||||||
|
|
||||||
public EngineAssignedWorldManager() {
|
public EngineAssignedWorldManager() {
|
||||||
super(null, null);
|
super(null, null);
|
||||||
@@ -126,6 +129,29 @@ public abstract class EngineAssignedWorldManager extends EngineAssignedComponent
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void on(PlayerTeleportEvent e) {
|
||||||
|
if (!BukkitPlatform.isPaperServer()
|
||||||
|
|| !getEngine().isStudio()
|
||||||
|
|| e.getCause() != PlayerTeleportEvent.TeleportCause.COMMAND) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Location destination = e.getTo();
|
||||||
|
World targetWorld = BukkitWorldBinding.world(getTarget().getWorld());
|
||||||
|
if (destination == null || targetWorld == null || !targetWorld.equals(destination.getWorld())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int chunkX = destination.getBlockX() >> 4;
|
||||||
|
int chunkZ = destination.getBlockZ() >> 4;
|
||||||
|
if (targetWorld.isChunkLoaded(chunkX, chunkZ)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
runManagerTask("bukkit_world_manager_teleport_event", () -> teleportAsync(e));
|
||||||
|
}
|
||||||
|
|
||||||
@EventHandler
|
@EventHandler
|
||||||
public void on(ChunkLoadEvent e) {
|
public void on(ChunkLoadEvent e) {
|
||||||
runManagerTask("bukkit_world_manager_chunk_load", () -> {
|
runManagerTask("bukkit_world_manager_chunk_load", () -> {
|
||||||
|
|||||||
@@ -38,6 +38,19 @@ public interface MantleComponent extends Comparable<MantleComponent> {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
default int getInputRadius(
|
||||||
|
int targetChunkX,
|
||||||
|
int targetChunkZ,
|
||||||
|
int invocationChunkRadius,
|
||||||
|
ChunkContext context
|
||||||
|
) {
|
||||||
|
return getInputRadius();
|
||||||
|
}
|
||||||
|
|
||||||
|
default boolean isInputGenerationLazy() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
default IrisData getData() {
|
default IrisData getData() {
|
||||||
return getEngineMantle().getData();
|
return getEngineMantle().getData();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,9 +66,21 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
|
|||||||
private final AtomicReferenceArray<MantleChunk<Matter>> window;
|
private final AtomicReferenceArray<MantleChunk<Matter>> window;
|
||||||
|
|
||||||
public MantleWriter(EngineMantle engineMantle, Mantle<Matter> mantle, int x, int z, int radius, boolean multicore) {
|
public MantleWriter(EngineMantle engineMantle, Mantle<Matter> mantle, int x, int z, int radius, boolean multicore) {
|
||||||
|
this(engineMantle, mantle, x, z, radius, radius * 2, multicore);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MantleWriter(
|
||||||
|
EngineMantle engineMantle,
|
||||||
|
Mantle<Matter> mantle,
|
||||||
|
int x,
|
||||||
|
int z,
|
||||||
|
int prefetchRadius,
|
||||||
|
int accessRadius,
|
||||||
|
boolean multicore
|
||||||
|
) {
|
||||||
this.engineMantle = engineMantle;
|
this.engineMantle = engineMantle;
|
||||||
this.mantle = mantle;
|
this.mantle = mantle;
|
||||||
this.radius = radius * 2;
|
this.radius = accessRadius;
|
||||||
this.x = x;
|
this.x = x;
|
||||||
this.z = z;
|
this.z = z;
|
||||||
// Every coordinate acquireChunk accepts lives in this window, so a flat array replaces the
|
// Every coordinate acquireChunk accepts lives in this window, so a flat array replaces the
|
||||||
@@ -78,7 +90,10 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
|
|||||||
|
|
||||||
final boolean foliaMaintenance = J.isFolia()
|
final boolean foliaMaintenance = J.isFolia()
|
||||||
&& WorldMaintenance.isWorldMaintenanceActive(engineMantle.getEngine().getWorld().identity());
|
&& WorldMaintenance.isWorldMaintenanceActive(engineMantle.getEngine().getWorld().identity());
|
||||||
final int parallelism = foliaMaintenance ? 1 : (multicore ? Runtime.getRuntime().availableProcessors() / 2 : 4);
|
final int parallelism = resolvePrefetchParallelism(
|
||||||
|
foliaMaintenance,
|
||||||
|
multicore,
|
||||||
|
Runtime.getRuntime().availableProcessors());
|
||||||
if (foliaMaintenance && IrisSettings.get().getGeneral().isDebug()) {
|
if (foliaMaintenance && IrisSettings.get().getGeneral().isDebug()) {
|
||||||
IrisLogging.info("MantleWriter using sequential chunk prefetch for maintenance regen at " + x + "," + z + ".");
|
IrisLogging.info("MantleWriter using sequential chunk prefetch for maintenance regen at " + x + "," + z + ".");
|
||||||
}
|
}
|
||||||
@@ -86,10 +101,10 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
|
|||||||
// prefetch must release the permits already pinned into the window here.
|
// prefetch must release the permits already pinned into the window here.
|
||||||
try {
|
try {
|
||||||
mantle.getChunks(
|
mantle.getChunks(
|
||||||
x - radius,
|
x - prefetchRadius,
|
||||||
x + radius,
|
x + prefetchRadius,
|
||||||
z - radius,
|
z - prefetchRadius,
|
||||||
z + radius,
|
z + prefetchRadius,
|
||||||
parallelism,
|
parallelism,
|
||||||
this::storePrefetchedChunk
|
this::storePrefetchedChunk
|
||||||
);
|
);
|
||||||
@@ -99,6 +114,16 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int resolvePrefetchParallelism(boolean foliaMaintenance, boolean multicore, int availableProcessors) {
|
||||||
|
if (foliaMaintenance) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (!multicore) {
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
return Math.max(1, availableProcessors / 2);
|
||||||
|
}
|
||||||
|
|
||||||
private static Set<IrisPosition> getBallooned(Set<IrisPosition> vset, double radius) {
|
private static Set<IrisPosition> getBallooned(Set<IrisPosition> vset, double radius) {
|
||||||
Set<IrisPosition> returnset = new HashSet<>();
|
Set<IrisPosition> returnset = new HashSet<>();
|
||||||
int ceilrad = (int) Math.ceil(radius);
|
int ceilrad = (int) Math.ceil(radius);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import art.arcane.iris.engine.framework.Engine;
|
|||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
import art.arcane.iris.util.common.parallel.MultiBurst;
|
import art.arcane.iris.util.common.parallel.MultiBurst;
|
||||||
import art.arcane.iris.util.project.context.ChunkContext;
|
import art.arcane.iris.util.project.context.ChunkContext;
|
||||||
|
import art.arcane.iris.util.project.context.IrisContext;
|
||||||
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
|
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
|
||||||
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
||||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||||
@@ -43,18 +44,31 @@ public interface MatterGenerator {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int writeRadius = getRadius();
|
MatterGenerationPlan generationPlan = resolveGenerationPlan(x, z, context);
|
||||||
|
MatterPassPlan[] passPlans = generationPlan.passPlans();
|
||||||
|
if (passPlans.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int prefetchRadius = passPlans[0].passChunkRadius();
|
||||||
LongOpenHashSet partialChunks = new LongOpenHashSet();
|
LongOpenHashSet partialChunks = new LongOpenHashSet();
|
||||||
|
|
||||||
try (MantleWriter writer = new MantleWriter(getEngine().getMantle(), getMantle(), x, z, writeRadius, multicore)) {
|
try (MantleWriter writer = new MantleWriter(
|
||||||
|
getEngine().getMantle(),
|
||||||
|
getMantle(),
|
||||||
|
x,
|
||||||
|
z,
|
||||||
|
prefetchRadius,
|
||||||
|
generationPlan.writerAccessRadius(),
|
||||||
|
multicore)) {
|
||||||
// Every task launched below writes through this writer. They must all be finished before
|
// Every task launched below writes through this writer. They must all be finished before
|
||||||
// close() releases the cached chunks, even when a pass throws, or detached pool threads
|
// close() releases the cached chunks, even when a pass throws, or detached pool threads
|
||||||
// write into released chunks.
|
// write into released chunks.
|
||||||
List<MatterComponentTask> outstandingTasks = null;
|
List<MatterComponentTask> outstandingTasks = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (MantlePass pass : getComponents()) {
|
for (MatterPassPlan passPlan : passPlans) {
|
||||||
int passRadius = pass.passChunkRadius();
|
MantlePass pass = passPlan.pass();
|
||||||
|
int passRadius = passPlan.passChunkRadius();
|
||||||
List<MantleComponent> passComponents = pass.components();
|
List<MantleComponent> passComponents = pass.components();
|
||||||
MantleComponent[] enabledComponents = new MantleComponent[passComponents.size()];
|
MantleComponent[] enabledComponents = new MantleComponent[passComponents.size()];
|
||||||
int[] componentPassRadii = new int[passComponents.size()];
|
int[] componentPassRadii = new int[passComponents.size()];
|
||||||
@@ -63,7 +77,7 @@ public interface MatterGenerator {
|
|||||||
if (component.isEnabled()) {
|
if (component.isEnabled()) {
|
||||||
// A component must cover its own reach plus every later pass' reach, or a
|
// A component must cover its own reach plus every later pass' reach, or a
|
||||||
// later pass reads this component's data from chunks it never wrote.
|
// later pass reads this component's data from chunks it never wrote.
|
||||||
int componentReach = component.getRadius() + pass.downstreamBlockRadius();
|
int componentReach = component.getRadius() + passPlan.downstreamBlockRadius();
|
||||||
componentPassRadii[enabledComponentCount] = componentReach > 0 ? Math.ceilDiv(componentReach, 16) : 0;
|
componentPassRadii[enabledComponentCount] = componentReach > 0 ? Math.ceilDiv(componentReach, 16) : 0;
|
||||||
enabledComponents[enabledComponentCount++] = component;
|
enabledComponents[enabledComponentCount++] = component;
|
||||||
}
|
}
|
||||||
@@ -112,7 +126,8 @@ public interface MatterGenerator {
|
|||||||
MantleChunk<Matter> chunk = writer.acquireChunk(passX, passZ);
|
MantleChunk<Matter> chunk = writer.acquireChunk(passX, passZ);
|
||||||
if (chunk == null) {
|
if (chunk == null) {
|
||||||
throw new IllegalStateException("Mantle pass chunk " + passX + "," + passZ
|
throw new IllegalStateException("Mantle pass chunk " + passX + "," + passZ
|
||||||
+ " is outside the writer prepared at " + x + "," + z + " with radius " + writeRadius);
|
+ " is outside the writer prepared at " + x + "," + z
|
||||||
|
+ " with radius " + generationPlan.writerAccessRadius());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (chunk.isFlagged(MantleFlag.PLANNED)) {
|
if (chunk.isFlagged(MantleFlag.PLANNED)) {
|
||||||
@@ -174,8 +189,9 @@ public interface MatterGenerator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = -getRealRadius(); i <= getRealRadius(); i++) {
|
int realRadius = passPlans[passPlans.length - 1].passChunkRadius();
|
||||||
for (int j = -getRealRadius(); j <= getRealRadius(); j++) {
|
for (int i = -realRadius; i <= realRadius; i++) {
|
||||||
|
for (int j = -realRadius; j <= realRadius; j++) {
|
||||||
int realX = x + i;
|
int realX = x + i;
|
||||||
int realZ = z + j;
|
int realZ = z + j;
|
||||||
long realKey = chunkKey(realX, realZ);
|
long realKey = chunkKey(realX, realZ);
|
||||||
@@ -195,6 +211,45 @@ public interface MatterGenerator {
|
|||||||
return (((long) x) << 32) ^ (z & 0xffffffffL);
|
return (((long) x) << 32) ^ (z & 0xffffffffL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private MatterGenerationPlan resolveGenerationPlan(int x, int z, ChunkContext context) {
|
||||||
|
List<MantlePass> passes = getComponents();
|
||||||
|
MatterPassPlan[] plans = new MatterPassPlan[passes.size()];
|
||||||
|
int accessDownstreamBlockRadius = 0;
|
||||||
|
int generationDownstreamBlockRadius = 0;
|
||||||
|
for (int passIndex = passes.size() - 1; passIndex >= 0; passIndex--) {
|
||||||
|
MantlePass pass = passes.get(passIndex);
|
||||||
|
int passBlockRadius = 0;
|
||||||
|
int passAccessInputRadius = 0;
|
||||||
|
int passGenerationInputRadius = 0;
|
||||||
|
for (MantleComponent component : pass.components()) {
|
||||||
|
if (!component.isEnabled()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int componentRadius = component.getRadius();
|
||||||
|
passBlockRadius = Math.max(passBlockRadius, componentRadius);
|
||||||
|
int componentReach = componentRadius + generationDownstreamBlockRadius;
|
||||||
|
int invocationChunkRadius = componentReach > 0
|
||||||
|
? Math.ceilDiv(componentReach, 16)
|
||||||
|
: 0;
|
||||||
|
int componentInputRadius = component.getInputRadius(x, z, invocationChunkRadius, context);
|
||||||
|
passAccessInputRadius = Math.max(passAccessInputRadius, componentInputRadius);
|
||||||
|
if (!component.isInputGenerationLazy()) {
|
||||||
|
passGenerationInputRadius = Math.max(passGenerationInputRadius, componentInputRadius);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int accessInvocationRadius = accessDownstreamBlockRadius + passBlockRadius;
|
||||||
|
int generationInvocationRadius = generationDownstreamBlockRadius + passBlockRadius;
|
||||||
|
int passChunkRadius = generationInvocationRadius > 0 ? Math.ceilDiv(generationInvocationRadius, 16) : 0;
|
||||||
|
plans[passIndex] = new MatterPassPlan(pass, passChunkRadius, generationDownstreamBlockRadius);
|
||||||
|
accessDownstreamBlockRadius = accessInvocationRadius + passAccessInputRadius;
|
||||||
|
generationDownstreamBlockRadius = generationInvocationRadius + passGenerationInputRadius;
|
||||||
|
}
|
||||||
|
int writerChunkRadius = accessDownstreamBlockRadius > 0
|
||||||
|
? Math.ceilDiv(accessDownstreamBlockRadius, 16)
|
||||||
|
: 0;
|
||||||
|
return new MatterGenerationPlan(plans, writerChunkRadius * 2);
|
||||||
|
}
|
||||||
|
|
||||||
private MatterComponentTask runComponentAsync(
|
private MatterComponentTask runComponentAsync(
|
||||||
MantleChunk<Matter> chunk,
|
MantleChunk<Matter> chunk,
|
||||||
MantleComponent component,
|
MantleComponent component,
|
||||||
@@ -217,11 +272,21 @@ public interface MatterGenerator {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (DISPATCHER.ownsCurrentThread()) {
|
if (DISPATCHER.ownsCurrentThread()) {
|
||||||
completeComponentTask(future, key, chunk, component, writer, chunkX, chunkZ, context);
|
completeComponentTask(future, key, chunk, component, writer, chunkX, chunkZ, context, IrisContext.get());
|
||||||
return new MatterComponentTask(key, future, null);
|
return new MatterComponentTask(key, future, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<?> submission = DISPATCHER.submit(() -> completeComponentTask(future, key, chunk, component, writer, chunkX, chunkZ, context));
|
IrisContext callerContext = IrisContext.get();
|
||||||
|
Future<?> submission = DISPATCHER.submit(() -> completeComponentTask(
|
||||||
|
future,
|
||||||
|
key,
|
||||||
|
chunk,
|
||||||
|
component,
|
||||||
|
writer,
|
||||||
|
chunkX,
|
||||||
|
chunkZ,
|
||||||
|
context,
|
||||||
|
callerContext));
|
||||||
return new MatterComponentTask(key, future, submission);
|
return new MatterComponentTask(key, future, submission);
|
||||||
} catch (Throwable throwable) {
|
} catch (Throwable throwable) {
|
||||||
IN_FLIGHT_COMPONENTS.remove(key, future);
|
IN_FLIGHT_COMPONENTS.remove(key, future);
|
||||||
@@ -329,10 +394,11 @@ public interface MatterGenerator {
|
|||||||
MantleWriter writer,
|
MantleWriter writer,
|
||||||
int chunkX,
|
int chunkX,
|
||||||
int chunkZ,
|
int chunkZ,
|
||||||
ChunkContext context
|
ChunkContext context,
|
||||||
|
IrisContext callerContext
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
runComponentInline(chunk, component, writer, chunkX, chunkZ, context);
|
runComponentWithContext(chunk, component, writer, chunkX, chunkZ, context, callerContext);
|
||||||
future.complete(null);
|
future.complete(null);
|
||||||
} catch (Throwable throwable) {
|
} catch (Throwable throwable) {
|
||||||
future.completeExceptionally(throwable);
|
future.completeExceptionally(throwable);
|
||||||
@@ -342,6 +408,28 @@ public interface MatterGenerator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void runComponentWithContext(
|
||||||
|
MantleChunk<Matter> chunk,
|
||||||
|
MantleComponent component,
|
||||||
|
MantleWriter writer,
|
||||||
|
int chunkX,
|
||||||
|
int chunkZ,
|
||||||
|
ChunkContext context,
|
||||||
|
IrisContext callerContext
|
||||||
|
) {
|
||||||
|
if (callerContext == null) {
|
||||||
|
runComponentInline(chunk, component, writer, chunkX, chunkZ, context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try (IrisContext.Scope ignored = IrisContext.open(
|
||||||
|
callerContext.getEngine(),
|
||||||
|
callerContext.getGenerationSessionId(),
|
||||||
|
callerContext.getChunkContext())) {
|
||||||
|
runComponentInline(chunk, component, writer, chunkX, chunkZ, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void runComponentInline(
|
private void runComponentInline(
|
||||||
MantleChunk<Matter> chunk,
|
MantleChunk<Matter> chunk,
|
||||||
MantleComponent component,
|
MantleComponent component,
|
||||||
@@ -360,6 +448,12 @@ public interface MatterGenerator {
|
|||||||
record MatterComponentTask(MatterTaskKey key, CompletableFuture<Void> future, Future<?> submission) {
|
record MatterComponentTask(MatterTaskKey key, CompletableFuture<Void> future, Future<?> submission) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
record MatterPassPlan(MantlePass pass, int passChunkRadius, int downstreamBlockRadius) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record MatterGenerationPlan(MatterPassPlan[] passPlans, int writerAccessRadius) {
|
||||||
|
}
|
||||||
|
|
||||||
final class MatterTaskKey {
|
final class MatterTaskKey {
|
||||||
private final Mantle<Matter> mantle;
|
private final Mantle<Matter> mantle;
|
||||||
private final int chunkX;
|
private final int chunkX;
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ final class CaveCarveScratch {
|
|||||||
final int[] fluidMaxY = new int[256];
|
final int[] fluidMaxY = new int[256];
|
||||||
final int[] surfaceBreakFloorY = new int[256];
|
final int[] surfaceBreakFloorY = new int[256];
|
||||||
final boolean[] surfaceBreakColumn = new boolean[256];
|
final boolean[] surfaceBreakColumn = new boolean[256];
|
||||||
|
final boolean[] surfaceCeilingColumn = new boolean[256];
|
||||||
final double[] columnThreshold = new double[256];
|
final double[] columnThreshold = new double[256];
|
||||||
final double[] passThreshold = new double[256];
|
final double[] passThreshold = new double[256];
|
||||||
final double[] fullWeights = new double[256];
|
final double[] fullWeights = new double[256];
|
||||||
@@ -62,6 +63,7 @@ final class CaveCarveScratch {
|
|||||||
int activeModulesY = Integer.MIN_VALUE;
|
int activeModulesY = Integer.MIN_VALUE;
|
||||||
int activeModuleCount;
|
int activeModuleCount;
|
||||||
double[] verticalEdgeFade = new double[0];
|
double[] verticalEdgeFade = new double[0];
|
||||||
|
double[] surfaceClosureThreshold = new double[0];
|
||||||
MatterCavern[] matterByY = new MatterCavern[0];
|
MatterCavern[] matterByY = new MatterCavern[0];
|
||||||
Matter[] sectionMatter = new Matter[0];
|
Matter[] sectionMatter = new Matter[0];
|
||||||
MatterSlice<?>[] sectionSlices = new MatterSlice<?>[0];
|
MatterSlice<?>[] sectionSlices = new MatterSlice<?>[0];
|
||||||
|
|||||||
+5
-2
@@ -20,13 +20,15 @@ final class ConfiguredRiverGrottoShape implements RiverCaveGrottoShape {
|
|||||||
private final CNG warpY;
|
private final CNG warpY;
|
||||||
private final CNG warpZ;
|
private final CNG warpZ;
|
||||||
private final double warpStrength;
|
private final double warpStrength;
|
||||||
|
private final double boundaryVariation;
|
||||||
|
|
||||||
ConfiguredRiverGrottoShape(
|
ConfiguredRiverGrottoShape(
|
||||||
long seed,
|
long seed,
|
||||||
IrisData data,
|
IrisData data,
|
||||||
IrisGeneratorStyle shapeStyle,
|
IrisGeneratorStyle shapeStyle,
|
||||||
IrisGeneratorStyle warpStyle,
|
IrisGeneratorStyle warpStyle,
|
||||||
double warpStrength
|
double warpStrength,
|
||||||
|
double boundaryVariation
|
||||||
) {
|
) {
|
||||||
IrisGeneratorStyle resolvedShape = shapeStyle == null
|
IrisGeneratorStyle resolvedShape = shapeStyle == null
|
||||||
? new IrisGeneratorStyle(NoiseStyle.FLAT)
|
? new IrisGeneratorStyle(NoiseStyle.FLAT)
|
||||||
@@ -39,6 +41,7 @@ final class ConfiguredRiverGrottoShape implements RiverCaveGrottoShape {
|
|||||||
warpY = resolvedWarp.createNoCache(new RNG(seed ^ WARP_Y_SALT), data);
|
warpY = resolvedWarp.createNoCache(new RNG(seed ^ WARP_Y_SALT), data);
|
||||||
warpZ = resolvedWarp.createNoCache(new RNG(seed ^ WARP_Z_SALT), data);
|
warpZ = resolvedWarp.createNoCache(new RNG(seed ^ WARP_Z_SALT), data);
|
||||||
this.warpStrength = Math.max(0D, warpStrength);
|
this.warpStrength = Math.max(0D, warpStrength);
|
||||||
|
this.boundaryVariation = Math.max(0D, Math.min(0.75D, boundaryVariation));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -60,7 +63,7 @@ final class ConfiguredRiverGrottoShape implements RiverCaveGrottoShape {
|
|||||||
double normalized = (warpedX * warpedX / (horizontalRadius * horizontalRadius))
|
double normalized = (warpedX * warpedX / (horizontalRadius * horizontalRadius))
|
||||||
+ (warpedY * warpedY / (verticalRadius * verticalRadius))
|
+ (warpedY * warpedY / (verticalRadius * verticalRadius))
|
||||||
+ (warpedZ * warpedZ / (horizontalRadius * horizontalRadius));
|
+ (warpedZ * warpedZ / (horizontalRadius * horizontalRadius));
|
||||||
double boundary = shape.fitDouble(-0.2D, 0.2D, worldX, worldY, worldZ);
|
double boundary = shape.fitDouble(-boundaryVariation, boundaryVariation, worldX, worldY, worldZ);
|
||||||
return normalized <= 1D + boundary;
|
return normalized <= 1D + boundary;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ public class IrisCaveCarver3D {
|
|||||||
private static final int ADAPTIVE_DEEP_SAMPLE_STEP = 8;
|
private static final int ADAPTIVE_DEEP_SAMPLE_STEP = 8;
|
||||||
private static final double ADAPTIVE_LOCAL_RANGE_SCALE = 0.125D;
|
private static final double ADAPTIVE_LOCAL_RANGE_SCALE = 0.125D;
|
||||||
private static final double ADAPTIVE_DEEP_MARGIN_BOOST = 0.015D;
|
private static final double ADAPTIVE_DEEP_MARGIN_BOOST = 0.015D;
|
||||||
|
private static final int SURFACE_CEILING_FADE_DEPTH = 12;
|
||||||
|
private static final double SURFACE_CEILING_SOLID_EPSILON = 0.000001D;
|
||||||
|
|
||||||
private final Engine engine;
|
private final Engine engine;
|
||||||
private final IrisData data;
|
private final IrisData data;
|
||||||
@@ -250,9 +252,11 @@ public class IrisCaveCarver3D {
|
|||||||
int[] fluidMaxY = scratch.fluidMaxY;
|
int[] fluidMaxY = scratch.fluidMaxY;
|
||||||
int[] surfaceBreakFloorY = scratch.surfaceBreakFloorY;
|
int[] surfaceBreakFloorY = scratch.surfaceBreakFloorY;
|
||||||
boolean[] surfaceBreakColumn = scratch.surfaceBreakColumn;
|
boolean[] surfaceBreakColumn = scratch.surfaceBreakColumn;
|
||||||
|
boolean[] surfaceCeilingColumn = scratch.surfaceCeilingColumn;
|
||||||
double[] columnThreshold = scratch.columnThreshold;
|
double[] columnThreshold = scratch.columnThreshold;
|
||||||
double[] clampedWeights = scratch.clampedColumnWeights;
|
double[] clampedWeights = scratch.clampedColumnWeights;
|
||||||
double[] verticalEdgeFade = prepareVerticalEdgeFadeTable(scratch, minY, maxY);
|
double[] verticalEdgeFade = prepareVerticalEdgeFadeTable(scratch, minY, maxY);
|
||||||
|
prepareSurfaceClosureThresholdTable(scratch, minY, maxY);
|
||||||
MatterCavern[] matterByY = prepareMatterByYTable(scratch, minY, maxY);
|
MatterCavern[] matterByY = prepareMatterByYTable(scratch, minY, maxY);
|
||||||
prepareSectionCaches(scratch, minY, maxY);
|
prepareSectionCaches(scratch, minY, maxY);
|
||||||
|
|
||||||
@@ -284,7 +288,8 @@ public class IrisCaveCarver3D {
|
|||||||
} else {
|
} else {
|
||||||
columnSurfaceY = engine.getHeight(x, z);
|
columnSurfaceY = engine.getHeight(x, z);
|
||||||
}
|
}
|
||||||
int clearanceTopY = Math.min(maxY, Math.max(minY, columnSurfaceY - surfaceClearance));
|
int unclampedClearanceTopY = columnSurfaceY - surfaceClearance;
|
||||||
|
int clearanceTopY = Math.min(maxY, Math.max(minY, unclampedClearanceTopY));
|
||||||
boolean breakColumn = allowSurfaceBreak
|
boolean breakColumn = allowSurfaceBreak
|
||||||
&& surfaceBreakDensity.noiseFastSigned2D(x, z) >= surfaceBreakNoiseThreshold;
|
&& surfaceBreakDensity.noiseFastSigned2D(x, z) >= surfaceBreakNoiseThreshold;
|
||||||
int columnTopY = breakColumn
|
int columnTopY = breakColumn
|
||||||
@@ -297,6 +302,7 @@ public class IrisCaveCarver3D {
|
|||||||
: Integer.MIN_VALUE;
|
: Integer.MIN_VALUE;
|
||||||
surfaceBreakFloorY[index] = Math.max(minY, columnSurfaceY - surfaceBreakDepth);
|
surfaceBreakFloorY[index] = Math.max(minY, columnSurfaceY - surfaceBreakDepth);
|
||||||
surfaceBreakColumn[index] = breakColumn;
|
surfaceBreakColumn[index] = breakColumn;
|
||||||
|
surfaceCeilingColumn[index] = !breakColumn && unclampedClearanceTopY <= maxY;
|
||||||
columnThreshold[index] = (thresholdDensity == null
|
columnThreshold[index] = (thresholdDensity == null
|
||||||
? constantThreshold
|
? constantThreshold
|
||||||
: thresholdDensity.fitDouble(thresholdMin, thresholdMax, x, z)) - thresholdBias;
|
: thresholdDensity.fitDouble(thresholdMin, thresholdMax, x, z)) - thresholdBias;
|
||||||
@@ -487,6 +493,7 @@ public class IrisCaveCarver3D {
|
|||||||
localThreshold += surfaceBreakThresholdBoost;
|
localThreshold += surfaceBreakThresholdBoost;
|
||||||
}
|
}
|
||||||
localThreshold -= verticalEdgeFade[y - minY];
|
localThreshold -= verticalEdgeFade[y - minY];
|
||||||
|
localThreshold = applySurfaceCeilingFade(scratch, localThreshold, columnIndex, y, minY);
|
||||||
planeThresholdLimit[planeCount] = localThreshold * normalizationFactor;
|
planeThresholdLimit[planeCount] = localThreshold * normalizationFactor;
|
||||||
planeCount++;
|
planeCount++;
|
||||||
}
|
}
|
||||||
@@ -514,7 +521,7 @@ public class IrisCaveCarver3D {
|
|||||||
}
|
}
|
||||||
|
|
||||||
double localThreshold = planeThresholdLimit[planeIndex] * inverseNormalization;
|
double localThreshold = planeThresholdLimit[planeIndex] * inverseNormalization;
|
||||||
MatterCavern matter = resolveMatter(verticalMatter, x0 + localX, y, z0 + localZ,
|
MatterCavern matter = resolveMatter(scratch, verticalMatter, x0 + localX, y, z0 + localZ,
|
||||||
columnIndex, fluidMaxY, localThreshold);
|
columnIndex, fluidMaxY, localThreshold);
|
||||||
writeCavern(cavernSlice, localX, y, localZ, matter, fluidSupportPlan);
|
writeCavern(cavernSlice, localX, y, localZ, matter, fluidSupportPlan);
|
||||||
carved++;
|
carved++;
|
||||||
@@ -531,7 +538,7 @@ public class IrisCaveCarver3D {
|
|||||||
int localX = PowerOfTwoCoordinates.unpackLocal16X(columnIndex);
|
int localX = PowerOfTwoCoordinates.unpackLocal16X(columnIndex);
|
||||||
int localZ = columnIndex & 15;
|
int localZ = columnIndex & 15;
|
||||||
double localThreshold = planeThresholdLimit[planeIndex] * inverseNormalization;
|
double localThreshold = planeThresholdLimit[planeIndex] * inverseNormalization;
|
||||||
MatterCavern matter = resolveMatter(verticalMatter, x0 + localX, y, z0 + localZ,
|
MatterCavern matter = resolveMatter(scratch, verticalMatter, x0 + localX, y, z0 + localZ,
|
||||||
columnIndex, fluidMaxY, localThreshold);
|
columnIndex, fluidMaxY, localThreshold);
|
||||||
writeCavern(cavernSlice, localX, y, localZ, matter, fluidSupportPlan);
|
writeCavern(cavernSlice, localX, y, localZ, matter, fluidSupportPlan);
|
||||||
carved++;
|
carved++;
|
||||||
@@ -624,6 +631,7 @@ public class IrisCaveCarver3D {
|
|||||||
localThreshold += surfaceBreakThresholdBoost;
|
localThreshold += surfaceBreakThresholdBoost;
|
||||||
}
|
}
|
||||||
localThreshold -= verticalEdgeFade[y - minY];
|
localThreshold -= verticalEdgeFade[y - minY];
|
||||||
|
localThreshold = applySurfaceCeilingFade(scratch, localThreshold, columnIndex, y, minY);
|
||||||
planeThresholdLimit[planeCount] = localThreshold * normalizationFactor;
|
planeThresholdLimit[planeCount] = localThreshold * normalizationFactor;
|
||||||
planeCount++;
|
planeCount++;
|
||||||
}
|
}
|
||||||
@@ -662,7 +670,7 @@ public class IrisCaveCarver3D {
|
|||||||
}
|
}
|
||||||
|
|
||||||
double localThreshold = planeThresholdLimit[planeIndex] * inverseNormalization;
|
double localThreshold = planeThresholdLimit[planeIndex] * inverseNormalization;
|
||||||
MatterCavern matter = resolveMatter(verticalMatter, x0 + localX, y, z0 + localZ,
|
MatterCavern matter = resolveMatter(scratch, verticalMatter, x0 + localX, y, z0 + localZ,
|
||||||
columnIndex, fluidMaxY, localThreshold);
|
columnIndex, fluidMaxY, localThreshold);
|
||||||
writeCavern(cavernSlice, localX, y, localZ, matter, fluidSupportPlan);
|
writeCavern(cavernSlice, localX, y, localZ, matter, fluidSupportPlan);
|
||||||
carved++;
|
carved++;
|
||||||
@@ -679,7 +687,7 @@ public class IrisCaveCarver3D {
|
|||||||
int localX = PowerOfTwoCoordinates.unpackLocal16X(columnIndex);
|
int localX = PowerOfTwoCoordinates.unpackLocal16X(columnIndex);
|
||||||
int localZ = columnIndex & 15;
|
int localZ = columnIndex & 15;
|
||||||
double localThreshold = planeThresholdLimit[planeIndex] * inverseNormalization;
|
double localThreshold = planeThresholdLimit[planeIndex] * inverseNormalization;
|
||||||
MatterCavern matter = resolveMatter(verticalMatter, x0 + localX, y, z0 + localZ,
|
MatterCavern matter = resolveMatter(scratch, verticalMatter, x0 + localX, y, z0 + localZ,
|
||||||
columnIndex, fluidMaxY, localThreshold);
|
columnIndex, fluidMaxY, localThreshold);
|
||||||
writeCavern(cavernSlice, localX, y, localZ, matter, fluidSupportPlan);
|
writeCavern(cavernSlice, localX, y, localZ, matter, fluidSupportPlan);
|
||||||
carved++;
|
carved++;
|
||||||
@@ -822,6 +830,7 @@ public class IrisCaveCarver3D {
|
|||||||
localThreshold += surfaceBreakThresholdBoost;
|
localThreshold += surfaceBreakThresholdBoost;
|
||||||
}
|
}
|
||||||
localThreshold -= verticalEdgeFade[fadeIndex];
|
localThreshold -= verticalEdgeFade[fadeIndex];
|
||||||
|
localThreshold = applySurfaceCeilingFade(scratch, localThreshold, index, yy, minY);
|
||||||
if (density > localThreshold) {
|
if (density > localThreshold) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -830,7 +839,7 @@ public class IrisCaveCarver3D {
|
|||||||
int localZ = tileLocalZ[columnIndex];
|
int localZ = tileLocalZ[columnIndex];
|
||||||
int worldX = x0 + localX;
|
int worldX = x0 + localX;
|
||||||
int worldZ = z0 + localZ;
|
int worldZ = z0 + localZ;
|
||||||
MatterCavern matter = resolveMatter(verticalMatter, worldX, yy, worldZ,
|
MatterCavern matter = resolveMatter(scratch, verticalMatter, worldX, yy, worldZ,
|
||||||
index, fluidMaxY, localThreshold);
|
index, fluidMaxY, localThreshold);
|
||||||
if (skipExistingCarved) {
|
if (skipExistingCarved) {
|
||||||
if (cavernSlice.get(localX, localY, localZ) == null) {
|
if (cavernSlice.get(localX, localY, localZ) == null) {
|
||||||
@@ -897,23 +906,23 @@ public class IrisCaveCarver3D {
|
|||||||
double threshold = columnThreshold[index] + thresholdBoost - ((1D - columnWeight) * thresholdPenalty);
|
double threshold = columnThreshold[index] + thresholdBoost - ((1D - columnWeight) * thresholdPenalty);
|
||||||
|
|
||||||
for (int y = minY; y <= columnTopY; y += sampleStep) {
|
for (int y = minY; y <= columnTopY; y += sampleStep) {
|
||||||
double localThreshold = threshold;
|
double density = sampleDensityOptimized(scratch, x, y, z);
|
||||||
if (breakColumn && y >= breakFloorY) {
|
|
||||||
localThreshold += surfaceBreakThresholdBoost;
|
|
||||||
}
|
|
||||||
|
|
||||||
localThreshold -= verticalEdgeFade[y - minY];
|
|
||||||
if (sampleDensityOptimized(scratch, x, y, z) > localThreshold) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
int carveMaxY = Math.min(columnTopY, y + sampleStep - 1);
|
int carveMaxY = Math.min(columnTopY, y + sampleStep - 1);
|
||||||
for (int yy = y; yy <= carveMaxY; yy++) {
|
for (int yy = y; yy <= carveMaxY; yy++) {
|
||||||
if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaries, index, yy)) {
|
if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaries, index, yy)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
double localThreshold = threshold;
|
||||||
|
if (breakColumn && yy >= breakFloorY) {
|
||||||
|
localThreshold += surfaceBreakThresholdBoost;
|
||||||
|
}
|
||||||
|
localThreshold -= verticalEdgeFade[yy - minY];
|
||||||
|
localThreshold = applySurfaceCeilingFade(scratch, localThreshold, index, yy, minY);
|
||||||
|
if (density > localThreshold) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
MatterCavern verticalMatter = matterByY[yy - minY];
|
MatterCavern verticalMatter = matterByY[yy - minY];
|
||||||
MatterCavern matter = resolveMatter(verticalMatter, x, yy, z,
|
MatterCavern matter = resolveMatter(scratch, verticalMatter, x, yy, z,
|
||||||
index, fluidMaxY, localThreshold);
|
index, fluidMaxY, localThreshold);
|
||||||
MatterSlice<MatterCavern> cavernSlice = resolveCavernSlice(scratch, chunk, PowerOfTwoCoordinates.floorDivPow2(yy, 4));
|
MatterSlice<MatterCavern> cavernSlice = resolveCavernSlice(scratch, chunk, PowerOfTwoCoordinates.floorDivPow2(yy, 4));
|
||||||
int localY = yy & 15;
|
int localY = yy & 15;
|
||||||
@@ -2191,57 +2200,61 @@ public class IrisCaveCarver3D {
|
|||||||
return matterByY;
|
return matterByY;
|
||||||
}
|
}
|
||||||
|
|
||||||
private MatterCavern resolveMatter(MatterCavern verticalMatter, int x, int y, int z,
|
private MatterCavern resolveMatter(CaveCarveScratch scratch, MatterCavern verticalMatter, int x, int y, int z,
|
||||||
int columnIndex, int[] fluidMaxY, double localThreshold) {
|
int columnIndex, int[] fluidMaxY, double localThreshold) {
|
||||||
if (verticalMatter != carveLava
|
if (verticalMatter != carveLava
|
||||||
&& y <= fluidMaxY[columnIndex]
|
&& y <= fluidMaxY[columnIndex]
|
||||||
&& isAquiferCandidate(x, y, z, localThreshold)) {
|
&& isAquiferCandidate(scratch, x, y, z, localThreshold)) {
|
||||||
return carveFluid;
|
return carveFluid;
|
||||||
}
|
}
|
||||||
return verticalMatter;
|
return verticalMatter;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isAquiferCandidate(int x, int y, int z, double localThreshold) {
|
private boolean isAquiferCandidate(CaveCarveScratch scratch, int x, int y, int z, double localThreshold) {
|
||||||
double depthFactor = Math.max(0D, Math.min(1.5D, (fluidHeight - y) / 48D));
|
double depthFactor = Math.max(0D, Math.min(1.5D, (fluidHeight - y) / 48D));
|
||||||
double cutoff = 0.35D + (depthFactor * 0.2D);
|
double cutoff = 0.35D + (depthFactor * 0.2D);
|
||||||
if (detailDensity.noiseFastSigned3D(x, y * 0.5D, z) <= cutoff) {
|
if (detailDensity.noiseFastSigned3D(x, y * 0.5D, z) <= cutoff) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return !fluidRequiresFloor || hasAquiferCupSupport(x, y, z, localThreshold);
|
return !fluidRequiresFloor || hasAquiferCupSupport(scratch, x, y, z, localThreshold);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean hasAquiferCupSupport(int x, int y, int z, double threshold) {
|
private boolean isAquiferCandidate(int x, int y, int z, double localThreshold) {
|
||||||
|
return isAquiferCandidate(scratchCache.get(), x, y, z, localThreshold);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasAquiferCupSupport(CaveCarveScratch scratch, int x, int y, int z, double threshold) {
|
||||||
int floorY = Math.max(0, y - 1);
|
int floorY = Math.max(0, y - 1);
|
||||||
int deepFloorY = Math.max(0, y - 2);
|
int deepFloorY = Math.max(0, y - 2);
|
||||||
int aboveY = Math.min(aquiferCeilingY, y + 1);
|
int aboveY = Math.min(aquiferCeilingY, y + 1);
|
||||||
if (!isDensitySolid(x, floorY, z, threshold)) {
|
if (!isDensitySolid(scratch, x, floorY, z, threshold)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!isDensitySolid(x, deepFloorY, z, threshold - 0.05D)) {
|
if (!isDensitySolid(scratch, x, deepFloorY, z, threshold - 0.05D)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
int support = 0;
|
int support = 0;
|
||||||
if (isDensitySolid(x + 1, y, z, threshold)) {
|
if (isDensitySolid(scratch, x + 1, y, z, threshold)) {
|
||||||
support++;
|
support++;
|
||||||
}
|
}
|
||||||
if (isDensitySolid(x - 1, y, z, threshold)) {
|
if (isDensitySolid(scratch, x - 1, y, z, threshold)) {
|
||||||
support++;
|
support++;
|
||||||
}
|
}
|
||||||
if (isDensitySolid(x, y, z + 1, threshold)) {
|
if (isDensitySolid(scratch, x, y, z + 1, threshold)) {
|
||||||
support++;
|
support++;
|
||||||
}
|
}
|
||||||
if (isDensitySolid(x, y, z - 1, threshold)) {
|
if (isDensitySolid(scratch, x, y, z - 1, threshold)) {
|
||||||
support++;
|
support++;
|
||||||
}
|
}
|
||||||
if (isDensitySolid(x, aboveY, z, threshold)) {
|
if (isDensitySolid(scratch, x, aboveY, z, threshold)) {
|
||||||
support++;
|
support++;
|
||||||
}
|
}
|
||||||
return support >= 4;
|
return support >= 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isDensitySolid(int x, int y, int z, double threshold) {
|
private boolean isDensitySolid(CaveCarveScratch scratch, int x, int y, int z, double threshold) {
|
||||||
return sampleDensityOptimized(x, y, z) > threshold;
|
return sampleDensityOptimized(scratch, x, y, z) > threshold;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void writeCavern(MatterSlice<MatterCavern> cavernSlice, int localX, int y, int localZ,
|
private void writeCavern(MatterSlice<MatterCavern> cavernSlice, int localX, int y, int localZ,
|
||||||
@@ -2288,6 +2301,52 @@ public class IrisCaveCarver3D {
|
|||||||
return (value * 2D) - 1D;
|
return (value * 2D) - 1D;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private double applySurfaceCeilingFade(
|
||||||
|
CaveCarveScratch scratch,
|
||||||
|
double threshold,
|
||||||
|
int columnIndex,
|
||||||
|
int y,
|
||||||
|
int minY
|
||||||
|
) {
|
||||||
|
if (!scratch.surfaceCeilingColumn[columnIndex]) {
|
||||||
|
return threshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ceilingDistance = scratch.columnMaxY[columnIndex] - y;
|
||||||
|
if (ceilingDistance < 0 || ceilingDistance >= SURFACE_CEILING_FADE_DEPTH) {
|
||||||
|
return threshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
double closureThreshold = scratch.surfaceClosureThreshold[y - minY];
|
||||||
|
if (threshold <= closureThreshold) {
|
||||||
|
return threshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
double progress = ceilingDistance / (double) SURFACE_CEILING_FADE_DEPTH;
|
||||||
|
double smooth = progress * progress * (3D - (2D * progress));
|
||||||
|
return closureThreshold + ((threshold - closureThreshold) * smooth);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void prepareSurfaceClosureThresholdTable(CaveCarveScratch scratch, int minY, int maxY) {
|
||||||
|
int size = Math.max(0, maxY - minY + 1);
|
||||||
|
if (scratch.surfaceClosureThreshold.length < size) {
|
||||||
|
scratch.surfaceClosureThreshold = new double[size];
|
||||||
|
}
|
||||||
|
|
||||||
|
double baseMinimum = -Math.abs(baseWeight) - Math.abs(detailWeight);
|
||||||
|
for (int y = minY; y <= maxY; y++) {
|
||||||
|
double minimumDensity = baseMinimum;
|
||||||
|
for (CaveFieldModuleState module : modules) {
|
||||||
|
if (y < module.minY || y > module.maxY) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
minimumDensity += Math.min(module.minContribution, module.maxContribution);
|
||||||
|
}
|
||||||
|
scratch.surfaceClosureThreshold[y - minY] =
|
||||||
|
(minimumDensity * inverseNormalization) - SURFACE_CEILING_SOLID_EPSILON;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private double[] prepareVerticalEdgeFadeTable(CaveCarveScratch scratch, int minY, int maxY) {
|
private double[] prepareVerticalEdgeFadeTable(CaveCarveScratch scratch, int minY, int maxY) {
|
||||||
int size = Math.max(0, maxY - minY + 1);
|
int size = Math.max(0, maxY - minY + 1);
|
||||||
if (scratch.verticalEdgeFade.length < size) {
|
if (scratch.verticalEdgeFade.length < size) {
|
||||||
|
|||||||
+3
-3
@@ -474,7 +474,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
|||||||
private void prefillProfileFieldSamples(int startX, int startZ, IrisComplex complex, BlendScratch blendScratch) {
|
private void prefillProfileFieldSamples(int startX, int startZ, IrisComplex complex, BlendScratch blendScratch) {
|
||||||
fillFieldHeights(complex.getHeightStream(), startX, startZ, blendScratch.fieldSurfaceHeights);
|
fillFieldHeights(complex.getHeightStream(), startX, startZ, blendScratch.fieldSurfaceHeights);
|
||||||
fillFieldHeights(complex.getRiverWaterSurfaceStream(), startX, startZ, blendScratch.fieldFluidHeights);
|
fillFieldHeights(complex.getRiverWaterSurfaceStream(), startX, startZ, blendScratch.fieldFluidHeights);
|
||||||
fillFieldFluidPresence(complex.getFluidStream(), startX, startZ, blendScratch.fieldSurfaceHeights,
|
fillFieldFluidPresence(complex, startX, startZ, blendScratch.fieldSurfaceHeights,
|
||||||
blendScratch.fieldFluidHeights, blendScratch.fieldHasFluid);
|
blendScratch.fieldFluidHeights, blendScratch.fieldHasFluid);
|
||||||
fillFieldObjects(complex.getRegionStream(), startX, startZ, blendScratch.fieldRegions);
|
fillFieldObjects(complex.getRegionStream(), startX, startZ, blendScratch.fieldRegions);
|
||||||
fillFieldObjects(complex.getTrueBiomeStream(), startX, startZ, blendScratch.fieldSurfaceBiomes);
|
fillFieldObjects(complex.getTrueBiomeStream(), startX, startZ, blendScratch.fieldSurfaceBiomes);
|
||||||
@@ -500,7 +500,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void fillFieldFluidPresence(
|
private void fillFieldFluidPresence(
|
||||||
ProceduralStream<PlatformBlockState> stream,
|
IrisComplex complex,
|
||||||
int startX,
|
int startX,
|
||||||
int startZ,
|
int startZ,
|
||||||
double[] surfaceHeights,
|
double[] surfaceHeights,
|
||||||
@@ -511,7 +511,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
|||||||
int worldX = startX + fieldX;
|
int worldX = startX + fieldX;
|
||||||
for (int fieldZ = 0; fieldZ < FIELD_SIZE; fieldZ++) {
|
for (int fieldZ = 0; fieldZ < FIELD_SIZE; fieldZ++) {
|
||||||
int fieldIndex = (fieldX * FIELD_SIZE) + fieldZ;
|
int fieldIndex = (fieldX * FIELD_SIZE) + fieldZ;
|
||||||
target[fieldIndex] = B.isFluid(stream.get(worldX, startZ + fieldZ))
|
target[fieldIndex] = B.isFluid(complex.resolveSurfaceFluid(worldX, startZ + fieldZ))
|
||||||
&& Math.round(surfaceHeights[fieldIndex]) < Math.round(fluidHeights[fieldIndex]);
|
&& Math.round(surfaceHeights[fieldIndex]) < Math.round(fluidHeights[fieldIndex]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+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.CavePosition;
|
||||||
import art.arcane.iris.engine.river.cave.CaveVoxel;
|
import art.arcane.iris.engine.river.cave.CaveVoxel;
|
||||||
import art.arcane.iris.engine.river.cave.CaveVoxelView;
|
import art.arcane.iris.engine.river.cave.CaveVoxelView;
|
||||||
import art.arcane.iris.engine.river.cave.RiverCaveAction;
|
import art.arcane.iris.engine.river.cave.RiverCaveFluidKind;
|
||||||
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
|
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
|
||||||
import art.arcane.iris.engine.data.cache.Cache;
|
import art.arcane.iris.engine.data.cache.Cache;
|
||||||
import art.arcane.iris.engine.object.IrisProceduralBlocks;
|
import art.arcane.iris.engine.object.IrisProceduralBlocks;
|
||||||
@@ -15,8 +15,10 @@ import art.arcane.volmlib.util.mantle.runtime.TectonicPlate;
|
|||||||
import art.arcane.volmlib.util.matter.Matter;
|
import art.arcane.volmlib.util.matter.Matter;
|
||||||
import art.arcane.volmlib.util.matter.MatterCavern;
|
import art.arcane.volmlib.util.matter.MatterCavern;
|
||||||
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
|
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
|
||||||
|
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
|
|
||||||
final class MantleRiverCaveVoxelView implements MantleRiverHydrologyComponent.TunnelVoxelView {
|
final class MantleRiverCaveVoxelView implements MantleRiverHydrologyComponent.TunnelVoxelView {
|
||||||
private static final int CLOSED_COLUMN = Integer.MAX_VALUE;
|
private static final int CLOSED_COLUMN = Integer.MAX_VALUE;
|
||||||
@@ -26,6 +28,9 @@ final class MantleRiverCaveVoxelView implements MantleRiverHydrologyComponent.Tu
|
|||||||
private final int worldHeight;
|
private final int worldHeight;
|
||||||
private final Function2<Integer, Integer, Integer> surfaceHeight;
|
private final Function2<Integer, Integer, Integer> surfaceHeight;
|
||||||
private final Function2<Integer, Integer, PlatformBlockState> compatibleFluid;
|
private final Function2<Integer, Integer, PlatformBlockState> compatibleFluid;
|
||||||
|
private final RiverCaveFluidKind planningFluidKind;
|
||||||
|
private final BiConsumer<Integer, Integer> chunkLoader;
|
||||||
|
private final LongOpenHashSet loadedChunks;
|
||||||
private final Long2IntOpenHashMap openFloorCache;
|
private final Long2IntOpenHashMap openFloorCache;
|
||||||
private final Long2IntOpenHashMap surfaceHeightCache;
|
private final Long2IntOpenHashMap surfaceHeightCache;
|
||||||
|
|
||||||
@@ -33,12 +38,17 @@ final class MantleRiverCaveVoxelView implements MantleRiverHydrologyComponent.Tu
|
|||||||
Mantle<Matter> mantle,
|
Mantle<Matter> mantle,
|
||||||
int worldHeight,
|
int worldHeight,
|
||||||
Function2<Integer, Integer, Integer> surfaceHeight,
|
Function2<Integer, Integer, Integer> surfaceHeight,
|
||||||
Function2<Integer, Integer, PlatformBlockState> compatibleFluid
|
Function2<Integer, Integer, PlatformBlockState> compatibleFluid,
|
||||||
|
RiverCaveFluidKind planningFluidKind,
|
||||||
|
BiConsumer<Integer, Integer> chunkLoader
|
||||||
) {
|
) {
|
||||||
this.mantle = Objects.requireNonNull(mantle);
|
this.mantle = Objects.requireNonNull(mantle);
|
||||||
this.worldHeight = worldHeight;
|
this.worldHeight = worldHeight;
|
||||||
this.surfaceHeight = Objects.requireNonNull(surfaceHeight);
|
this.surfaceHeight = Objects.requireNonNull(surfaceHeight);
|
||||||
this.compatibleFluid = Objects.requireNonNull(compatibleFluid);
|
this.compatibleFluid = Objects.requireNonNull(compatibleFluid);
|
||||||
|
this.planningFluidKind = Objects.requireNonNull(planningFluidKind);
|
||||||
|
this.chunkLoader = Objects.requireNonNull(chunkLoader);
|
||||||
|
loadedChunks = new LongOpenHashSet();
|
||||||
openFloorCache = new Long2IntOpenHashMap();
|
openFloorCache = new Long2IntOpenHashMap();
|
||||||
openFloorCache.defaultReturnValue(CACHE_MISS);
|
openFloorCache.defaultReturnValue(CACHE_MISS);
|
||||||
surfaceHeightCache = new Long2IntOpenHashMap();
|
surfaceHeightCache = new Long2IntOpenHashMap();
|
||||||
@@ -52,10 +62,18 @@ final class MantleRiverCaveVoxelView implements MantleRiverHydrologyComponent.Tu
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CaveVoxel voxelAt(CavePosition position) {
|
public CaveVoxel voxelAt(CavePosition position) {
|
||||||
|
RiverCaveHydrology hydrology = dataIfPresent(position, RiverCaveHydrology.class);
|
||||||
|
if (hydrology != null && hydrology.fluidKind() != planningFluidKind) {
|
||||||
|
return CaveVoxel.INCOMPATIBLE_FLUID;
|
||||||
|
}
|
||||||
MatterCavern cavern = dataIfPresent(position, MatterCavern.class);
|
MatterCavern cavern = dataIfPresent(position, MatterCavern.class);
|
||||||
if (cavern != null) {
|
if (cavern != null) {
|
||||||
if (cavern.isLava()) {
|
if (cavern.isLava()) {
|
||||||
return CaveVoxel.LAVA;
|
PlatformBlockState expected = compatibleFluid.apply(position.x(), position.z());
|
||||||
|
return expected != null
|
||||||
|
&& IrisProceduralBlocks.materialKey(expected).endsWith(":lava")
|
||||||
|
? CaveVoxel.COMPATIBLE_FLUID
|
||||||
|
: CaveVoxel.LAVA;
|
||||||
}
|
}
|
||||||
if (cavern.getLiquid() == 1) {
|
if (cavern.getLiquid() == 1) {
|
||||||
return CaveVoxel.COMPATIBLE_FLUID;
|
return CaveVoxel.COMPATIBLE_FLUID;
|
||||||
@@ -71,13 +89,13 @@ final class MantleRiverCaveVoxelView implements MantleRiverHydrologyComponent.Tu
|
|||||||
if (!block.isFluid()) {
|
if (!block.isFluid()) {
|
||||||
return CaveVoxel.SOLID;
|
return CaveVoxel.SOLID;
|
||||||
}
|
}
|
||||||
if (IrisProceduralBlocks.materialKey(block).endsWith(":lava")) {
|
|
||||||
return CaveVoxel.LAVA;
|
|
||||||
}
|
|
||||||
PlatformBlockState expected = compatibleFluid.apply(position.x(), position.z());
|
PlatformBlockState expected = compatibleFluid.apply(position.x(), position.z());
|
||||||
return expected != null
|
if (expected != null
|
||||||
&& IrisProceduralBlocks.materialKey(expected).equals(IrisProceduralBlocks.materialKey(block))
|
&& IrisProceduralBlocks.materialKey(expected).equals(IrisProceduralBlocks.materialKey(block))) {
|
||||||
? CaveVoxel.COMPATIBLE_FLUID
|
return CaveVoxel.COMPATIBLE_FLUID;
|
||||||
|
}
|
||||||
|
return IrisProceduralBlocks.materialKey(block).endsWith(":lava")
|
||||||
|
? CaveVoxel.LAVA
|
||||||
: CaveVoxel.INCOMPATIBLE_FLUID;
|
: CaveVoxel.INCOMPATIBLE_FLUID;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,9 +117,8 @@ final class MantleRiverCaveVoxelView implements MantleRiverHydrologyComponent.Tu
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RiverCaveAction riverActionAt(CavePosition position) {
|
public RiverCaveHydrology riverHydrologyAt(CavePosition position) {
|
||||||
RiverCaveHydrology hydrology = dataIfPresent(position, RiverCaveHydrology.class);
|
return dataIfPresent(position, RiverCaveHydrology.class);
|
||||||
return hydrology == null ? null : hydrology.action();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private int resolveOpenFloor(int x, int z) {
|
private int resolveOpenFloor(int x, int z) {
|
||||||
@@ -131,6 +148,10 @@ final class MantleRiverCaveVoxelView implements MantleRiverHydrologyComponent.Tu
|
|||||||
private <T> T dataIfPresent(CavePosition position, Class<T> type) {
|
private <T> T dataIfPresent(CavePosition position, Class<T> type) {
|
||||||
int chunkX = position.x() >> 4;
|
int chunkX = position.x() >> 4;
|
||||||
int chunkZ = position.z() >> 4;
|
int chunkZ = position.z() >> 4;
|
||||||
|
long chunkKey = Mantle.key(chunkX, chunkZ);
|
||||||
|
if (loadedChunks.add(chunkKey)) {
|
||||||
|
chunkLoader.accept(chunkX, chunkZ);
|
||||||
|
}
|
||||||
TectonicPlate<Matter> plate = mantle.getLoadedRegions().get(Mantle.key(chunkX >> 5, chunkZ >> 5));
|
TectonicPlate<Matter> plate = mantle.getLoadedRegions().get(Mantle.key(chunkX >> 5, chunkZ >> 5));
|
||||||
if (plate == null || plate.isClosed()) {
|
if (plate == null || plate.isClosed()) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
+474
-34
@@ -1,17 +1,21 @@
|
|||||||
package art.arcane.iris.engine.mantle.components;
|
package art.arcane.iris.engine.mantle.components;
|
||||||
|
|
||||||
import art.arcane.iris.core.loader.IrisData;
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
|
import art.arcane.iris.engine.data.cache.Cache;
|
||||||
import art.arcane.iris.engine.mantle.ComponentFlag;
|
import art.arcane.iris.engine.mantle.ComponentFlag;
|
||||||
import art.arcane.iris.engine.mantle.EngineMantle;
|
import art.arcane.iris.engine.mantle.EngineMantle;
|
||||||
import art.arcane.iris.engine.mantle.IrisMantleComponent;
|
import art.arcane.iris.engine.mantle.IrisMantleComponent;
|
||||||
|
import art.arcane.iris.engine.mantle.MantleComponent;
|
||||||
import art.arcane.iris.engine.mantle.MantleWriter;
|
import art.arcane.iris.engine.mantle.MantleWriter;
|
||||||
import art.arcane.iris.engine.object.IrisRiverCaveFallback;
|
import art.arcane.iris.engine.object.IrisRiverCaveFallback;
|
||||||
import art.arcane.iris.engine.object.IrisRiverCaveMode;
|
import art.arcane.iris.engine.object.IrisRiverCaveMode;
|
||||||
import art.arcane.iris.engine.object.IrisRiverCaves;
|
import art.arcane.iris.engine.object.IrisRiverCaves;
|
||||||
|
import art.arcane.iris.engine.object.IrisRiverDeepPools;
|
||||||
import art.arcane.iris.engine.object.IrisRiverExistingFluidPolicy;
|
import art.arcane.iris.engine.object.IrisRiverExistingFluidPolicy;
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
import art.arcane.iris.engine.object.IrisRiverNetwork;
|
import art.arcane.iris.engine.object.IrisRiverNetwork;
|
||||||
import art.arcane.iris.engine.river.RiverAnchor;
|
import art.arcane.iris.engine.river.RiverAnchor;
|
||||||
|
import art.arcane.iris.engine.river.RiverNetwork;
|
||||||
import art.arcane.iris.engine.river.RiverRouteState;
|
import art.arcane.iris.engine.river.RiverRouteState;
|
||||||
import art.arcane.iris.engine.river.RiverSample;
|
import art.arcane.iris.engine.river.RiverSample;
|
||||||
import art.arcane.iris.engine.river.RiverSection;
|
import art.arcane.iris.engine.river.RiverSection;
|
||||||
@@ -22,6 +26,7 @@ import art.arcane.iris.engine.river.cave.CaveVoxelPrecondition;
|
|||||||
import art.arcane.iris.engine.river.cave.CaveVoxelView;
|
import art.arcane.iris.engine.river.cave.CaveVoxelView;
|
||||||
import art.arcane.iris.engine.river.cave.RiverCaveAction;
|
import art.arcane.iris.engine.river.cave.RiverCaveAction;
|
||||||
import art.arcane.iris.engine.river.cave.RiverCaveContainmentPlanner;
|
import art.arcane.iris.engine.river.cave.RiverCaveContainmentPlanner;
|
||||||
|
import art.arcane.iris.engine.river.cave.RiverCaveFluidKind;
|
||||||
import art.arcane.iris.engine.river.cave.RiverCaveFluidPolicy;
|
import art.arcane.iris.engine.river.cave.RiverCaveFluidPolicy;
|
||||||
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
|
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
|
||||||
import art.arcane.iris.engine.river.cave.RiverCaveMode;
|
import art.arcane.iris.engine.river.cave.RiverCaveMode;
|
||||||
@@ -35,8 +40,13 @@ import art.arcane.iris.engine.river.runtime.IrisRiverTunnelSample;
|
|||||||
import art.arcane.iris.util.project.context.ChunkContext;
|
import art.arcane.iris.util.project.context.ChunkContext;
|
||||||
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
||||||
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
|
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
|
||||||
|
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||||
|
import art.arcane.volmlib.util.math.BlockPosition;
|
||||||
|
import art.arcane.volmlib.util.matter.Matter;
|
||||||
|
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
@@ -47,6 +57,8 @@ import java.util.Set;
|
|||||||
@ComponentFlag(ReservedFlag.RIVER_HYDROLOGY)
|
@ComponentFlag(ReservedFlag.RIVER_HYDROLOGY)
|
||||||
public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
|
public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
|
||||||
private static final long CANDIDATE_SALT = 0x6A09E667F3BCC909L;
|
private static final long CANDIDATE_SALT = 0x6A09E667F3BCC909L;
|
||||||
|
private static final long DEEP_POOL_CANDIDATE_SALT = 0xBB67AE8584CAA73BL;
|
||||||
|
private static final long DEEP_POOL_POSITION_SALT = 0x3C6EF372FE94F82BL;
|
||||||
static final int PRIORITY = 1;
|
static final int PRIORITY = 1;
|
||||||
private static final int[] FALLBACK_X = {0, 1, -1, 0, 0};
|
private static final int[] FALLBACK_X = {0, 1, -1, 0, 0};
|
||||||
private static final int[] FALLBACK_Z = {0, 0, 0, 1, -1};
|
private static final int[] FALLBACK_Z = {0, 0, 0, 1, -1};
|
||||||
@@ -73,6 +85,11 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
|
|||||||
return PREREQUISITES;
|
return PREREQUISITES;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isInputGenerationLazy() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getInputRadius() {
|
public int getInputRadius() {
|
||||||
if (!getDimension().isCarvingEnabled()
|
if (!getDimension().isCarvingEnabled()
|
||||||
@@ -87,6 +104,122 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
|
|||||||
return inputRadius(runtime.caveSettings(), tunnelHalo(runtime));
|
return inputRadius(runtime.caveSettings(), tunnelHalo(runtime));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getInputRadius(
|
||||||
|
int targetChunkX,
|
||||||
|
int targetChunkZ,
|
||||||
|
int invocationChunkRadius,
|
||||||
|
ChunkContext context
|
||||||
|
) {
|
||||||
|
if (!getDimension().isCarvingEnabled()
|
||||||
|
|| getDimension().getRivers() == null
|
||||||
|
|| !getDimension().getRivers().isEnabled()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (context == null || context.getComplex() == null) {
|
||||||
|
return getInputRadius();
|
||||||
|
}
|
||||||
|
IrisRiverRuntime runtime = context.getComplex().getRiverRuntime();
|
||||||
|
if (runtime == null) {
|
||||||
|
return getInputRadius();
|
||||||
|
}
|
||||||
|
IrisRiverCaves caves = runtime.caveSettings();
|
||||||
|
int tunnelRadius = tunnelHalo(runtime);
|
||||||
|
int radius = hasRiverFootprint(
|
||||||
|
runtime,
|
||||||
|
targetChunkX,
|
||||||
|
targetChunkZ,
|
||||||
|
invocationChunkRadius,
|
||||||
|
tunnelRadius
|
||||||
|
) ? tunnelRadius : 0;
|
||||||
|
if (caves.getMode() != IrisRiverCaveMode.SEALED
|
||||||
|
&& caves.getMaximumPerReach() > 0) {
|
||||||
|
radius = Math.max(radius, hasAcceptedCaveAnchor(
|
||||||
|
runtime,
|
||||||
|
caves,
|
||||||
|
targetChunkX,
|
||||||
|
targetChunkZ,
|
||||||
|
invocationChunkRadius
|
||||||
|
) ? planningHalo(caves) : 0);
|
||||||
|
}
|
||||||
|
IrisRiverDeepPools deepPools = caves.getDeepPools();
|
||||||
|
if (deepPools != null
|
||||||
|
&& deepPools.isEnabled()
|
||||||
|
&& deepPools.getMaximumPerReach() > 0) {
|
||||||
|
radius = Math.max(radius, hasAcceptedDeepPoolAnchor(
|
||||||
|
runtime,
|
||||||
|
deepPools,
|
||||||
|
targetChunkX,
|
||||||
|
targetChunkZ,
|
||||||
|
invocationChunkRadius
|
||||||
|
) ? deepPoolPlanningHalo(deepPools) : 0);
|
||||||
|
}
|
||||||
|
return radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean hasRiverFootprint(
|
||||||
|
IrisRiverRuntime runtime,
|
||||||
|
int targetChunkX,
|
||||||
|
int targetChunkZ,
|
||||||
|
int invocationChunkRadius,
|
||||||
|
int tunnelRadius
|
||||||
|
) {
|
||||||
|
return runtime.hasRiverFootprint(
|
||||||
|
((targetChunkX - invocationChunkRadius) << 4) - tunnelRadius,
|
||||||
|
((targetChunkZ - invocationChunkRadius) << 4) - tunnelRadius,
|
||||||
|
((targetChunkX + invocationChunkRadius + 1) << 4) + tunnelRadius,
|
||||||
|
((targetChunkZ + invocationChunkRadius + 1) << 4) + tunnelRadius
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean hasAcceptedCaveAnchor(
|
||||||
|
IrisRiverRuntime runtime,
|
||||||
|
IrisRiverCaves caves,
|
||||||
|
int targetChunkX,
|
||||||
|
int targetChunkZ,
|
||||||
|
int invocationChunkRadius
|
||||||
|
) {
|
||||||
|
int candidateHalo = candidateHalo(caves);
|
||||||
|
List<RiverAnchor> anchors = runtime.candidateAnchors(
|
||||||
|
((targetChunkX - invocationChunkRadius) << 4) - candidateHalo,
|
||||||
|
((targetChunkZ - invocationChunkRadius) << 4) - candidateHalo,
|
||||||
|
((targetChunkX + invocationChunkRadius + 1) << 4) + candidateHalo,
|
||||||
|
((targetChunkZ + invocationChunkRadius + 1) << 4) + candidateHalo,
|
||||||
|
caves.getMinimumSpacing(),
|
||||||
|
CANDIDATE_SALT
|
||||||
|
);
|
||||||
|
for (RiverAnchor anchor : anchors) {
|
||||||
|
if (runtime.acceptsCaveAnchor(anchor)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean hasAcceptedDeepPoolAnchor(
|
||||||
|
IrisRiverRuntime runtime,
|
||||||
|
IrisRiverDeepPools deepPools,
|
||||||
|
int targetChunkX,
|
||||||
|
int targetChunkZ,
|
||||||
|
int invocationChunkRadius
|
||||||
|
) {
|
||||||
|
int candidateHalo = deepPoolCandidateHalo(deepPools);
|
||||||
|
List<RiverAnchor> anchors = runtime.candidateAnchors(
|
||||||
|
((targetChunkX - invocationChunkRadius) << 4) - candidateHalo,
|
||||||
|
((targetChunkZ - invocationChunkRadius) << 4) - candidateHalo,
|
||||||
|
((targetChunkX + invocationChunkRadius + 1) << 4) + candidateHalo,
|
||||||
|
((targetChunkZ + invocationChunkRadius + 1) << 4) + candidateHalo,
|
||||||
|
deepPools.getMinimumSpacing(),
|
||||||
|
DEEP_POOL_CANDIDATE_SALT
|
||||||
|
);
|
||||||
|
for (RiverAnchor anchor : anchors) {
|
||||||
|
if (runtime.acceptsDeepPoolAnchor(anchor)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void generateLayer(MantleWriter writer, int chunkX, int chunkZ, ChunkContext context) {
|
public void generateLayer(MantleWriter writer, int chunkX, int chunkZ, ChunkContext context) {
|
||||||
IrisRiverRuntime runtime = context.getComplex().getRiverRuntime();
|
IrisRiverRuntime runtime = context.getComplex().getRiverRuntime();
|
||||||
@@ -96,11 +229,21 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
|
|||||||
publishTunnels(writer, context, runtime, chunkX, chunkZ);
|
publishTunnels(writer, context, runtime, chunkX, chunkZ);
|
||||||
|
|
||||||
IrisRiverCaves caves = runtime.caveSettings();
|
IrisRiverCaves caves = runtime.caveSettings();
|
||||||
if (caves.getMode() == IrisRiverCaveMode.SEALED || caves.getMaximumPerReach() <= 0) {
|
if (caves.getMode() != IrisRiverCaveMode.SEALED && caves.getMaximumPerReach() > 0) {
|
||||||
return;
|
publishCaveConnections(writer, context, runtime, caves, chunkX, chunkZ);
|
||||||
|
}
|
||||||
|
publishDeepPools(writer, context, runtime, caves.getDeepPools(), chunkX, chunkZ);
|
||||||
}
|
}
|
||||||
|
|
||||||
MantleRiverCaveVoxelView view = createView(writer, context);
|
private void publishCaveConnections(
|
||||||
|
MantleWriter writer,
|
||||||
|
ChunkContext context,
|
||||||
|
IrisRiverRuntime runtime,
|
||||||
|
IrisRiverCaves caves,
|
||||||
|
int chunkX,
|
||||||
|
int chunkZ
|
||||||
|
) {
|
||||||
|
MantleRiverCaveVoxelView view = createView(writer, context, RiverCaveFluidKind.RIVER);
|
||||||
int candidateHalo = candidateHalo(caves);
|
int candidateHalo = candidateHalo(caves);
|
||||||
int minimumX = (chunkX << 4) - candidateHalo;
|
int minimumX = (chunkX << 4) - candidateHalo;
|
||||||
int minimumZ = (chunkZ << 4) - candidateHalo;
|
int minimumZ = (chunkZ << 4) - candidateHalo;
|
||||||
@@ -146,11 +289,202 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
RiverCavePlanningResult result = planner.planAll(view, sources, settings);
|
RiverCavePlanningResult result = planner.planAll(view, sources, settings);
|
||||||
MantleRiverCaveVoxelView revalidationView = createView(writer, context);
|
MantleRiverCaveVoxelView revalidationView = createView(
|
||||||
|
writer,
|
||||||
|
context,
|
||||||
|
RiverCaveFluidKind.RIVER
|
||||||
|
);
|
||||||
if (!preconditionsHold(revalidationView, result.baselinePreconditions())) {
|
if (!preconditionsHold(revalidationView, result.baselinePreconditions())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
publishLocal(writer, chunkX, chunkZ, result, floodedBiomes);
|
publishLocal(
|
||||||
|
writer,
|
||||||
|
chunkX,
|
||||||
|
chunkZ,
|
||||||
|
result,
|
||||||
|
floodedBiomes,
|
||||||
|
RiverCaveFluidKind.RIVER
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void publishDeepPools(
|
||||||
|
MantleWriter writer,
|
||||||
|
ChunkContext context,
|
||||||
|
IrisRiverRuntime runtime,
|
||||||
|
IrisRiverDeepPools deepPools,
|
||||||
|
int chunkX,
|
||||||
|
int chunkZ
|
||||||
|
) {
|
||||||
|
if (deepPools == null || !deepPools.isEnabled() || deepPools.getMaximumPerReach() <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
MantleRiverCaveVoxelView view = createView(
|
||||||
|
writer,
|
||||||
|
context,
|
||||||
|
RiverCaveFluidKind.DEEP_POOL
|
||||||
|
);
|
||||||
|
int candidateHalo = deepPoolCandidateHalo(deepPools);
|
||||||
|
int minimumX = (chunkX << 4) - candidateHalo;
|
||||||
|
int minimumZ = (chunkZ << 4) - candidateHalo;
|
||||||
|
int maximumX = ((chunkX + 1) << 4) + candidateHalo;
|
||||||
|
int maximumZ = ((chunkZ + 1) << 4) + candidateHalo;
|
||||||
|
List<RiverAnchor> anchors = runtime.candidateAnchors(
|
||||||
|
minimumX,
|
||||||
|
minimumZ,
|
||||||
|
maximumX,
|
||||||
|
maximumZ,
|
||||||
|
deepPools.getMinimumSpacing(),
|
||||||
|
DEEP_POOL_CANDIDATE_SALT
|
||||||
|
);
|
||||||
|
if (anchors.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RiverCavePlannerSettings settings = deepPoolPlannerSettings(deepPools, seed(), getData());
|
||||||
|
List<RiverCaveSource> sources = new ArrayList<>();
|
||||||
|
Map<Long, String> floodedBiomes = new HashMap<>();
|
||||||
|
for (RiverAnchor anchor : anchors) {
|
||||||
|
if (!runtime.acceptsDeepPoolAnchor(anchor)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
RiverCaveSource source = deepPoolSourceFor(
|
||||||
|
view,
|
||||||
|
deepPools,
|
||||||
|
anchor,
|
||||||
|
getDimension().getMinHeight(),
|
||||||
|
seed()
|
||||||
|
);
|
||||||
|
if (source == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
sources.add(source);
|
||||||
|
floodedBiomes.put(source.sourceId(), runtime.selectFloodedCaveBiome(anchor));
|
||||||
|
}
|
||||||
|
if (sources.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RiverCavePlanningResult result = planner.planAll(view, sources, settings);
|
||||||
|
MantleRiverCaveVoxelView revalidationView = createView(
|
||||||
|
writer,
|
||||||
|
context,
|
||||||
|
RiverCaveFluidKind.DEEP_POOL
|
||||||
|
);
|
||||||
|
if (!preconditionsHold(revalidationView, result.baselinePreconditions())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
publishLocal(
|
||||||
|
writer,
|
||||||
|
chunkX,
|
||||||
|
chunkZ,
|
||||||
|
result,
|
||||||
|
floodedBiomes,
|
||||||
|
RiverCaveFluidKind.DEEP_POOL
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static RiverCavePlannerSettings deepPoolPlannerSettings(
|
||||||
|
IrisRiverDeepPools deepPools,
|
||||||
|
long seed,
|
||||||
|
IrisData data
|
||||||
|
) {
|
||||||
|
int verticalDepth = deepPools.getVerticalRadius() * 2 - deepPools.getDryHeadroom() + 1;
|
||||||
|
int proofRadius = (int) StrictMath.ceil(
|
||||||
|
StrictMath.sqrt(2D) * deepPools.getHorizontalRadius()
|
||||||
|
) + 1;
|
||||||
|
return new RiverCavePlannerSettings(
|
||||||
|
proofRadius,
|
||||||
|
verticalDepth,
|
||||||
|
deepPools.getMaximumVolume(),
|
||||||
|
deepPools.getVerticalRadius(),
|
||||||
|
1,
|
||||||
|
deepPools.getHorizontalRadius(),
|
||||||
|
deepPools.getVerticalRadius(),
|
||||||
|
deepPools.getDryHeadroom(),
|
||||||
|
RiverCaveFluidPolicy.REJECT_EXISTING,
|
||||||
|
new ConfiguredRiverGrottoShape(
|
||||||
|
seed ^ DEEP_POOL_POSITION_SALT,
|
||||||
|
data,
|
||||||
|
deepPools.getShapeStyle(),
|
||||||
|
deepPools.getWarpStyle(),
|
||||||
|
deepPools.getWarpStrength(),
|
||||||
|
deepPools.getShapeVariation()
|
||||||
|
),
|
||||||
|
proofRadius,
|
||||||
|
verticalDepth
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static RiverCaveSource deepPoolSourceFor(
|
||||||
|
CaveVoxelView view,
|
||||||
|
IrisRiverDeepPools deepPools,
|
||||||
|
RiverAnchor anchor,
|
||||||
|
int worldMinimumY,
|
||||||
|
long seed
|
||||||
|
) {
|
||||||
|
int minimumHead = deepPools.getMinimumFluidY() - worldMinimumY;
|
||||||
|
int maximumHead = deepPools.getMaximumFluidY() - worldMinimumY;
|
||||||
|
int headRange = maximumHead - minimumHead + 1;
|
||||||
|
if (headRange <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int searchRadius = deepPools.getSearchRadius();
|
||||||
|
int searchWidth = searchRadius * 2 + 1;
|
||||||
|
int targetDepth = Math.max(
|
||||||
|
1,
|
||||||
|
deepPools.getVerticalRadius() - deepPools.getDryHeadroom()
|
||||||
|
);
|
||||||
|
for (int attempt = 0; attempt < deepPools.getSearchAttempts(); attempt++) {
|
||||||
|
long hash = RiverNetwork.mix(
|
||||||
|
seed
|
||||||
|
^ anchor.stableId()
|
||||||
|
^ DEEP_POOL_POSITION_SALT
|
||||||
|
^ (long) attempt * 0x9E3779B97F4A7C15L
|
||||||
|
);
|
||||||
|
int offsetX = searchRadius == 0
|
||||||
|
? 0
|
||||||
|
: Math.floorMod((int) hash, searchWidth) - searchRadius;
|
||||||
|
int offsetZ = searchRadius == 0
|
||||||
|
? 0
|
||||||
|
: Math.floorMod((int) (hash >>> 32), searchWidth) - searchRadius;
|
||||||
|
if ((long) offsetX * offsetX + (long) offsetZ * offsetZ
|
||||||
|
> (long) searchRadius * searchRadius) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int x = (int) StrictMath.floor(anchor.x()) + offsetX;
|
||||||
|
int z = (int) StrictMath.floor(anchor.z()) + offsetZ;
|
||||||
|
int startOffset = (int) StrictMath.floor(unit(hash) * headRange);
|
||||||
|
for (int scanned = 0; scanned < headRange; scanned++) {
|
||||||
|
int headY = maximumHead - Math.floorMod(startOffset + scanned, headRange);
|
||||||
|
CavePosition floor = new CavePosition(x, headY, z);
|
||||||
|
CavePosition above = new CavePosition(x, headY + 1, z);
|
||||||
|
CavePosition target = new CavePosition(x, headY - targetDepth, z);
|
||||||
|
if (!view.isInWorld(target)
|
||||||
|
|| !view.isInWorld(above)
|
||||||
|
|| view.voxelAt(floor) != CaveVoxel.SOLID
|
||||||
|
|| view.voxelAt(above) != CaveVoxel.CAVE_AIR
|
||||||
|
|| view.isOpenToSurface(above)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
long sourceId = RiverNetwork.mix(
|
||||||
|
anchor.stableId()
|
||||||
|
^ BlockPosition.toLong(x, headY, z)
|
||||||
|
^ DEEP_POOL_POSITION_SALT
|
||||||
|
);
|
||||||
|
return new RiverCaveSource(
|
||||||
|
sourceId,
|
||||||
|
floor,
|
||||||
|
target,
|
||||||
|
headY,
|
||||||
|
RiverCaveMode.DEEP_POOL
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double unit(long hash) {
|
||||||
|
return (hash >>> 11) * 0x1.0p-53;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -176,9 +510,15 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
IrisRiverCaves caves = dimension.getRivers().getCaves();
|
IrisRiverCaves caves = dimension.getRivers().getCaves();
|
||||||
return caves != null
|
if (caves == null) {
|
||||||
&& caves.getMode() != IrisRiverCaveMode.SEALED
|
return false;
|
||||||
|
}
|
||||||
|
boolean caveConnections = caves.getMode() != IrisRiverCaveMode.SEALED
|
||||||
&& caves.getMaximumPerReach() > 0;
|
&& caves.getMaximumPerReach() > 0;
|
||||||
|
IrisRiverDeepPools deepPools = caves.getDeepPools();
|
||||||
|
return caveConnections || deepPools != null
|
||||||
|
&& deepPools.isEnabled()
|
||||||
|
&& deepPools.getMaximumPerReach() > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int planningHalo(IrisRiverCaves caves) {
|
static int planningHalo(IrisRiverCaves caves) {
|
||||||
@@ -186,16 +526,33 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static int inputRadius(IrisRiverCaves caves, int tunnelRadius) {
|
static int inputRadius(IrisRiverCaves caves, int tunnelRadius) {
|
||||||
if (caves.getMode() == IrisRiverCaveMode.SEALED || caves.getMaximumPerReach() <= 0) {
|
int radius = tunnelRadius;
|
||||||
return tunnelRadius;
|
if (caves.getMode() != IrisRiverCaveMode.SEALED && caves.getMaximumPerReach() > 0) {
|
||||||
|
radius = Math.max(radius, planningHalo(caves));
|
||||||
}
|
}
|
||||||
return Math.max(tunnelRadius, planningHalo(caves));
|
IrisRiverDeepPools deepPools = caves.getDeepPools();
|
||||||
|
if (deepPools != null && deepPools.isEnabled() && deepPools.getMaximumPerReach() > 0) {
|
||||||
|
radius = Math.max(radius, deepPoolPlanningHalo(deepPools));
|
||||||
|
}
|
||||||
|
return radius;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int candidateHalo(IrisRiverCaves caves) {
|
static int candidateHalo(IrisRiverCaves caves) {
|
||||||
return cavePublicationRadius(caves) * 3;
|
return cavePublicationRadius(caves) * 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int deepPoolPlanningHalo(IrisRiverDeepPools deepPools) {
|
||||||
|
return deepPoolPublicationRadius(deepPools) * 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int deepPoolCandidateHalo(IrisRiverDeepPools deepPools) {
|
||||||
|
return deepPoolPublicationRadius(deepPools) * 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int deepPoolPublicationRadius(IrisRiverDeepPools deepPools) {
|
||||||
|
return deepPools.getSearchRadius() + deepPools.getHorizontalRadius() + 1;
|
||||||
|
}
|
||||||
|
|
||||||
static int cavePublicationRadius(IrisRiverCaves caves) {
|
static int cavePublicationRadius(IrisRiverCaves caves) {
|
||||||
int generatedRadius = generatedGrottoPublicationRadius(caves);
|
int generatedRadius = generatedGrottoPublicationRadius(caves);
|
||||||
return switch (caves.getMode()) {
|
return switch (caves.getMode()) {
|
||||||
@@ -296,13 +653,13 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
|
|||||||
boolean changed;
|
boolean changed;
|
||||||
do {
|
do {
|
||||||
changed = false;
|
changed = false;
|
||||||
Set<CavePosition> candidateActions = mergeActions(containedColumns).keySet();
|
Long2ObjectOpenHashMap<TunnelColumn> candidateColumns = indexColumns(containedColumns);
|
||||||
for (int index = containedColumns.size() - 1; index >= 0; index--) {
|
for (int index = containedColumns.size() - 1; index >= 0; index--) {
|
||||||
TunnelColumn column = containedColumns.get(index);
|
TunnelColumn column = containedColumns.get(index);
|
||||||
if (!isTunnelColumnContained(
|
if (!isTunnelColumnContained(
|
||||||
view,
|
view,
|
||||||
column,
|
column,
|
||||||
candidateActions,
|
candidateColumns,
|
||||||
dryHeadroom,
|
dryHeadroom,
|
||||||
surfaceSampler
|
surfaceSampler
|
||||||
)) {
|
)) {
|
||||||
@@ -348,7 +705,10 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new TunnelPlan(Map.copyOf(actions), Map.copyOf(preconditions));
|
return new TunnelPlan(
|
||||||
|
Collections.unmodifiableMap(actions),
|
||||||
|
Collections.unmodifiableMap(preconditions)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TunnelColumn createTunnelColumn(
|
private static TunnelColumn createTunnelColumn(
|
||||||
@@ -360,8 +720,9 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
|
|||||||
if (sample == null) {
|
if (sample == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
LinkedHashMap<CavePosition, RiverCaveAction> actions = new LinkedHashMap<>();
|
int minimumY = sample.bedY() + 1;
|
||||||
for (int y = sample.bedY() + 1; y <= sample.ceilingY(); y++) {
|
int maximumY = sample.ceilingY();
|
||||||
|
for (int y = minimumY; y <= maximumY; y++) {
|
||||||
CavePosition position = new CavePosition(x, y, z);
|
CavePosition position = new CavePosition(x, y, z);
|
||||||
RiverCaveAction action = y <= sample.waterHeadY()
|
RiverCaveAction action = y <= sample.waterHeadY()
|
||||||
? RiverCaveAction.WET_SOURCE
|
? RiverCaveAction.WET_SOURCE
|
||||||
@@ -371,22 +732,24 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
|
|||||||
&& !matchesPublishedAction(view, position, action))) {
|
&& !matchesPublishedAction(view, position, action))) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
actions.put(position, action);
|
|
||||||
}
|
}
|
||||||
return actions.isEmpty() ? null : new TunnelColumn(actions);
|
return minimumY > maximumY
|
||||||
|
? null
|
||||||
|
: new TunnelColumn(x, z, minimumY, sample.waterHeadY(), maximumY);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isTunnelColumnContained(
|
private static boolean isTunnelColumnContained(
|
||||||
CaveVoxelView view,
|
CaveVoxelView view,
|
||||||
TunnelColumn column,
|
TunnelColumn column,
|
||||||
Set<CavePosition> candidateActions,
|
Long2ObjectOpenHashMap<TunnelColumn> candidateColumns,
|
||||||
int dryHeadroom,
|
int dryHeadroom,
|
||||||
SurfaceSampler surfaceSampler
|
SurfaceSampler surfaceSampler
|
||||||
) {
|
) {
|
||||||
for (CavePosition position : column.actions().keySet()) {
|
for (int y = column.minimumY(); y <= column.maximumY(); y++) {
|
||||||
|
CavePosition position = new CavePosition(column.x(), y, column.z());
|
||||||
for (int[] offset : NEIGHBORS) {
|
for (int[] offset : NEIGHBORS) {
|
||||||
CavePosition neighbor = offset(position, offset);
|
CavePosition neighbor = offset(position, offset);
|
||||||
if (candidateActions.contains(neighbor)) {
|
if (containsAction(candidateColumns, neighbor)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!view.isInWorld(neighbor)) {
|
if (!view.isInWorld(neighbor)) {
|
||||||
@@ -417,7 +780,9 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
|
|||||||
SurfaceSampler surfaceSampler
|
SurfaceSampler surfaceSampler
|
||||||
) {
|
) {
|
||||||
IrisRiverSurfaceSample sample = surfaceSampler.sample(position.x(), position.z());
|
IrisRiverSurfaceSample sample = surfaceSampler.sample(position.x(), position.z());
|
||||||
if (!isWetChannelBed(sample) || sample.subterranean()) {
|
if (!sample.river().present()
|
||||||
|
|| sample.river().state() != RiverRouteState.WET
|
||||||
|
|| sample.subterranean()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
int bedY = (int) Math.round(sample.terrainHeight());
|
int bedY = (int) Math.round(sample.terrainHeight());
|
||||||
@@ -428,18 +793,48 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
|
|||||||
private static Map<CavePosition, RiverCaveAction> mergeActions(List<TunnelColumn> columns) {
|
private static Map<CavePosition, RiverCaveAction> mergeActions(List<TunnelColumn> columns) {
|
||||||
LinkedHashMap<CavePosition, RiverCaveAction> actions = new LinkedHashMap<>();
|
LinkedHashMap<CavePosition, RiverCaveAction> actions = new LinkedHashMap<>();
|
||||||
for (TunnelColumn column : columns) {
|
for (TunnelColumn column : columns) {
|
||||||
actions.putAll(column.actions());
|
for (int y = column.minimumY(); y <= column.maximumY(); y++) {
|
||||||
|
actions.put(
|
||||||
|
new CavePosition(column.x(), y, column.z()),
|
||||||
|
y <= column.waterHeadY()
|
||||||
|
? RiverCaveAction.WET_SOURCE
|
||||||
|
: RiverCaveAction.DRY_AIR
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return actions;
|
return actions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Long2ObjectOpenHashMap<TunnelColumn> indexColumns(List<TunnelColumn> columns) {
|
||||||
|
Long2ObjectOpenHashMap<TunnelColumn> indexed = new Long2ObjectOpenHashMap<>(columns.size());
|
||||||
|
for (TunnelColumn column : columns) {
|
||||||
|
indexed.put(Cache.key(column.x(), column.z()), column);
|
||||||
|
}
|
||||||
|
return indexed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean containsAction(
|
||||||
|
Long2ObjectOpenHashMap<TunnelColumn> columns,
|
||||||
|
CavePosition position
|
||||||
|
) {
|
||||||
|
TunnelColumn column = columns.get(Cache.key(position.x(), position.z()));
|
||||||
|
return column != null
|
||||||
|
&& position.y() >= column.minimumY()
|
||||||
|
&& position.y() <= column.maximumY();
|
||||||
|
}
|
||||||
|
|
||||||
private static boolean matchesPublishedAction(
|
private static boolean matchesPublishedAction(
|
||||||
CaveVoxelView view,
|
CaveVoxelView view,
|
||||||
CavePosition position,
|
CavePosition position,
|
||||||
RiverCaveAction action
|
RiverCaveAction action
|
||||||
) {
|
) {
|
||||||
return view instanceof TunnelVoxelView tunnelView
|
if (!(view instanceof TunnelVoxelView tunnelView)) {
|
||||||
&& tunnelView.riverActionAt(position) == action;
|
return false;
|
||||||
|
}
|
||||||
|
RiverCaveHydrology hydrology = tunnelView.riverHydrologyAt(position);
|
||||||
|
return hydrology != null
|
||||||
|
&& hydrology.action() == action
|
||||||
|
&& hydrology.fluidKind() == RiverCaveFluidKind.RIVER;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CavePosition offset(CavePosition position, int[] offset) {
|
private static CavePosition offset(CavePosition position, int[] offset) {
|
||||||
@@ -450,15 +845,39 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private MantleRiverCaveVoxelView createView(MantleWriter writer, ChunkContext context) {
|
private MantleRiverCaveVoxelView createView(
|
||||||
|
MantleWriter writer,
|
||||||
|
ChunkContext context,
|
||||||
|
RiverCaveFluidKind fluidKind
|
||||||
|
) {
|
||||||
return new MantleRiverCaveVoxelView(
|
return new MantleRiverCaveVoxelView(
|
||||||
writer.getMantle(),
|
writer.getMantle(),
|
||||||
writer.getMantle().getWorldHeight(),
|
writer.getMantle().getWorldHeight(),
|
||||||
(x, z) -> context.getComplex().getRoundedHeighteightStream().get(x, z),
|
(x, z) -> context.getComplex().getRoundedHeighteightStream().get(x, z),
|
||||||
(x, z) -> context.getComplex().getFluidStream().get(x, z)
|
(x, z) -> context.getComplex().resolveRiverCaveFluid(fluidKind, x, z),
|
||||||
|
fluidKind,
|
||||||
|
(chunkX, chunkZ) -> generateCarvingInput(writer, context, chunkX, chunkZ)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void generateCarvingInput(
|
||||||
|
MantleWriter writer,
|
||||||
|
ChunkContext context,
|
||||||
|
int chunkX,
|
||||||
|
int chunkZ
|
||||||
|
) {
|
||||||
|
MantleComponent carving = getEngineMantle().getRegisteredComponents().get(ReservedFlag.CARVED);
|
||||||
|
if (carving == null || !carving.isEnabled()) {
|
||||||
|
throw new IllegalStateException("River hydrology requires the carving component");
|
||||||
|
}
|
||||||
|
MantleChunk<Matter> chunk = writer.acquireChunk(chunkX, chunkZ);
|
||||||
|
if (chunk == null) {
|
||||||
|
throw new IllegalStateException("River hydrology read exceeded the prepared mantle radius at "
|
||||||
|
+ chunkX + "," + chunkZ);
|
||||||
|
}
|
||||||
|
chunk.raiseFlagSuspend(ReservedFlag.CARVED, () -> carving.generateLayer(writer, chunkX, chunkZ, context));
|
||||||
|
}
|
||||||
|
|
||||||
private void publishTunnels(
|
private void publishTunnels(
|
||||||
MantleWriter writer,
|
MantleWriter writer,
|
||||||
ChunkContext context,
|
ChunkContext context,
|
||||||
@@ -467,7 +886,11 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
|
|||||||
int chunkZ
|
int chunkZ
|
||||||
) {
|
) {
|
||||||
for (int attempt = 0; attempt < 2; attempt++) {
|
for (int attempt = 0; attempt < 2; attempt++) {
|
||||||
MantleRiverCaveVoxelView view = createView(writer, context);
|
MantleRiverCaveVoxelView view = createView(
|
||||||
|
writer,
|
||||||
|
context,
|
||||||
|
RiverCaveFluidKind.RIVER
|
||||||
|
);
|
||||||
TunnelPlan plan = planTunnels(
|
TunnelPlan plan = planTunnels(
|
||||||
view,
|
view,
|
||||||
chunkX,
|
chunkX,
|
||||||
@@ -478,7 +901,11 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
|
|||||||
runtime::sampleTunnel,
|
runtime::sampleTunnel,
|
||||||
runtime::sample
|
runtime::sample
|
||||||
);
|
);
|
||||||
MantleRiverCaveVoxelView revalidationView = createView(writer, context);
|
MantleRiverCaveVoxelView revalidationView = createView(
|
||||||
|
writer,
|
||||||
|
context,
|
||||||
|
RiverCaveFluidKind.RIVER
|
||||||
|
);
|
||||||
if (preconditionsHold(revalidationView, plan.preconditions())) {
|
if (preconditionsHold(revalidationView, plan.preconditions())) {
|
||||||
publishTunnelLocal(writer, chunkX, chunkZ, plan);
|
publishTunnelLocal(writer, chunkX, chunkZ, plan);
|
||||||
return;
|
return;
|
||||||
@@ -505,7 +932,8 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
|
|||||||
data,
|
data,
|
||||||
caves.getGrottoShapeStyle(),
|
caves.getGrottoShapeStyle(),
|
||||||
caves.getGrottoWarpStyle(),
|
caves.getGrottoWarpStyle(),
|
||||||
caves.getGrottoWarpStrength()
|
caves.getGrottoWarpStrength(),
|
||||||
|
0.2D
|
||||||
),
|
),
|
||||||
caves.getMaxFloodRadius(),
|
caves.getMaxFloodRadius(),
|
||||||
caves.getMaxFloodDepth()
|
caves.getMaxFloodDepth()
|
||||||
@@ -673,7 +1101,8 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
|
|||||||
int chunkX,
|
int chunkX,
|
||||||
int chunkZ,
|
int chunkZ,
|
||||||
RiverCavePlanningResult result,
|
RiverCavePlanningResult result,
|
||||||
Map<Long, String> floodedBiomes
|
Map<Long, String> floodedBiomes,
|
||||||
|
RiverCaveFluidKind fluidKind
|
||||||
) {
|
) {
|
||||||
Map<CavePosition, RiverCaveSource> owners = actionOwners(result);
|
Map<CavePosition, RiverCaveSource> owners = actionOwners(result);
|
||||||
ArrayList<Map.Entry<CavePosition, RiverCaveAction>> actions = new ArrayList<>(result.actions().entrySet());
|
ArrayList<Map.Entry<CavePosition, RiverCaveAction>> actions = new ArrayList<>(result.actions().entrySet());
|
||||||
@@ -692,7 +1121,7 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
|
|||||||
position.x(),
|
position.x(),
|
||||||
position.y(),
|
position.y(),
|
||||||
position.z(),
|
position.z(),
|
||||||
new RiverCaveHydrology(entry.getValue(), biome)
|
new RiverCaveHydrology(entry.getValue(), biome, fluidKind)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -708,7 +1137,12 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
|
|||||||
for (Map.Entry<CavePosition, RiverCaveAction> entry : actions) {
|
for (Map.Entry<CavePosition, RiverCaveAction> entry : actions) {
|
||||||
CavePosition position = entry.getKey();
|
CavePosition position = entry.getKey();
|
||||||
if (owns(chunkX, chunkZ, position)) {
|
if (owns(chunkX, chunkZ, position)) {
|
||||||
writer.setData(position.x(), position.y(), position.z(), RiverCaveHydrology.of(entry.getValue()));
|
writer.setData(
|
||||||
|
position.x(),
|
||||||
|
position.y(),
|
||||||
|
position.z(),
|
||||||
|
RiverCaveHydrology.of(entry.getValue(), RiverCaveFluidKind.RIVER)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -749,7 +1183,7 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface TunnelVoxelView extends CaveVoxelView {
|
interface TunnelVoxelView extends CaveVoxelView {
|
||||||
RiverCaveAction riverActionAt(CavePosition position);
|
RiverCaveHydrology riverHydrologyAt(CavePosition position);
|
||||||
}
|
}
|
||||||
|
|
||||||
record TunnelPlan(
|
record TunnelPlan(
|
||||||
@@ -761,6 +1195,12 @@ public final class MantleRiverHydrologyComponent extends IrisMantleComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private record TunnelColumn(Map<CavePosition, RiverCaveAction> actions) {
|
private record TunnelColumn(
|
||||||
|
int x,
|
||||||
|
int z,
|
||||||
|
int minimumY,
|
||||||
|
int waterHeadY,
|
||||||
|
int maximumY
|
||||||
|
) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,11 @@ public class ModeOverworld extends IrisEngineMode implements EngineMode {
|
|||||||
if (shouldBypassMantleStages()) {
|
if (shouldBypassMantleStages()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
generateMatter(x >> 4, z >> 4, m, c);
|
generateMatter(
|
||||||
|
x >> 4,
|
||||||
|
z >> 4,
|
||||||
|
m || getEngine().isStudio(),
|
||||||
|
c);
|
||||||
};
|
};
|
||||||
EngineStage sTerrain = (x, z, k, p, m, c) -> terrain.actuate(x, z, k, m, c);
|
EngineStage sTerrain = (x, z, k, p, m, c) -> terrain.actuate(x, z, k, m, c);
|
||||||
EngineStage sDecorant = (x, z, k, p, m, c) -> decorant.actuate(x, z, k, m, c);
|
EngineStage sDecorant = (x, z, k, p, m, c) -> decorant.actuate(x, z, k, m, c);
|
||||||
|
|||||||
@@ -117,7 +117,9 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
|||||||
columnMasks,
|
columnMasks,
|
||||||
upperSurfaceHeights,
|
upperSurfaceHeights,
|
||||||
worldHeightSpan,
|
worldHeightSpan,
|
||||||
caveLavaHeight
|
caveLavaHeight,
|
||||||
|
chunkBlockX,
|
||||||
|
chunkBlockZ
|
||||||
);
|
);
|
||||||
CarveResolver carveResolver = new CarveResolver(resolutionContext);
|
CarveResolver carveResolver = new CarveResolver(resolutionContext);
|
||||||
mantleChunk.iterate(MatterCavern.class, (xx, yy, zz, cavern) -> carveResolver.apply(
|
mantleChunk.iterate(MatterCavern.class, (xx, yy, zz, cavern) -> carveResolver.apply(
|
||||||
@@ -166,8 +168,6 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
|||||||
});
|
});
|
||||||
|
|
||||||
for (int columnIndex = 0; columnIndex < 256; columnIndex++) {
|
for (int columnIndex = 0; columnIndex < 256; columnIndex++) {
|
||||||
int localX = PowerOfTwoCoordinates.unpackLocal16X(columnIndex);
|
|
||||||
int localZ = columnIndex & 15;
|
|
||||||
processColumnFromMask(
|
processColumnFromMask(
|
||||||
output,
|
output,
|
||||||
mantleChunk,
|
mantleChunk,
|
||||||
@@ -178,8 +178,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
|||||||
z,
|
z,
|
||||||
resolverState,
|
resolverState,
|
||||||
caveBiomeCache,
|
caveBiomeCache,
|
||||||
customBiomeCache,
|
customBiomeCache
|
||||||
context.getFluid().get(localX, localZ)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,7 +278,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
|||||||
}
|
}
|
||||||
return switch (hydrology.action()) {
|
return switch (hydrology.action()) {
|
||||||
case WET_SOURCE -> fluid;
|
case WET_SOURCE -> fluid;
|
||||||
case FALLING_WATER -> fallingFluidState(fluid);
|
case FALLING_FLUID -> fallingFluidState(fluid);
|
||||||
case DRY_AIR -> air;
|
case DRY_AIR -> air;
|
||||||
case SEAL_GUARD -> normalizeWaterlogging(current, null);
|
case SEAL_GUARD -> normalizeWaterlogging(current, null);
|
||||||
};
|
};
|
||||||
@@ -369,9 +368,16 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
PlatformBlockState fluid = isFluidIntent(cavern)
|
PlatformBlockState fluid = null;
|
||||||
|
if (isFluidIntent(cavern)) {
|
||||||
|
fluid = hydrology == null
|
||||||
? context.chunkContext().getFluid().get(localX, localZ)
|
? context.chunkContext().getFluid().get(localX, localZ)
|
||||||
: null;
|
: getComplex().resolveRiverCaveFluid(
|
||||||
|
hydrology.fluidKind(),
|
||||||
|
context.chunkBlockX() + localX,
|
||||||
|
context.chunkBlockZ() + localZ
|
||||||
|
);
|
||||||
|
}
|
||||||
if (hydrology != null) {
|
if (hydrology != null) {
|
||||||
context.output().setRaw(localX, y, localZ,
|
context.output().setRaw(localX, y, localZ,
|
||||||
resolveHydrologyState(hydrology, current, fluid, AIR));
|
resolveHydrologyState(hydrology, current, fluid, AIR));
|
||||||
@@ -395,7 +401,9 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
|||||||
CarveColumnMask[] columnMasks,
|
CarveColumnMask[] columnMasks,
|
||||||
int[] upperSurfaceHeights,
|
int[] upperSurfaceHeights,
|
||||||
int worldHeightSpan,
|
int worldHeightSpan,
|
||||||
int caveLavaHeight
|
int caveLavaHeight,
|
||||||
|
int chunkBlockX,
|
||||||
|
int chunkBlockZ
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -572,8 +580,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
|||||||
int chunkZ,
|
int chunkZ,
|
||||||
IrisDimensionCarvingResolver.State resolverState,
|
IrisDimensionCarvingResolver.State resolverState,
|
||||||
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache,
|
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache,
|
||||||
Map<String, IrisBiome> customBiomeCache,
|
Map<String, IrisBiome> customBiomeCache
|
||||||
PlatformBlockState columnFluid
|
|
||||||
) {
|
) {
|
||||||
if (columnMask == null || columnMask.isEmpty()) {
|
if (columnMask == null || columnMask.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
@@ -601,7 +608,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
|||||||
} else {
|
} else {
|
||||||
if (zone.isValid(getEngine())) {
|
if (zone.isValid(getEngine())) {
|
||||||
processZone(output, mc, mantle, zone, rx, rz, worldX, worldZ, resolverState,
|
processZone(output, mc, mantle, zone, rx, rz, worldX, worldZ, resolverState,
|
||||||
caveBiomeCache, customBiomeCache, columnFluid);
|
caveBiomeCache, customBiomeCache);
|
||||||
}
|
}
|
||||||
zone = new CaveZone();
|
zone = new CaveZone();
|
||||||
zone.setFloor(y);
|
zone.setFloor(y);
|
||||||
@@ -614,7 +621,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
|||||||
|
|
||||||
if (zone.isValid(getEngine())) {
|
if (zone.isValid(getEngine())) {
|
||||||
processZone(output, mc, mantle, zone, rx, rz, worldX, worldZ, resolverState,
|
processZone(output, mc, mantle, zone, rx, rz, worldX, worldZ, resolverState,
|
||||||
caveBiomeCache, customBiomeCache, columnFluid);
|
caveBiomeCache, customBiomeCache);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -767,8 +774,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
|||||||
CaveZone zone, int rx, int rz, int xx, int zz,
|
CaveZone zone, int rx, int rz, int xx, int zz,
|
||||||
IrisDimensionCarvingResolver.State resolverState,
|
IrisDimensionCarvingResolver.State resolverState,
|
||||||
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache,
|
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache,
|
||||||
Map<String, IrisBiome> customBiomeCache,
|
Map<String, IrisBiome> customBiomeCache) {
|
||||||
PlatformBlockState columnFluid) {
|
|
||||||
int maxY = output.getHeight();
|
int maxY = output.getHeight();
|
||||||
|
|
||||||
if (zone.ceiling + 1 < maxY && B.isDecorant(output.getRaw(rx, zone.ceiling + 1, rz))) {
|
if (zone.ceiling + 1 < maxY && B.isDecorant(output.getRaw(rx, zone.ceiling + 1, rz))) {
|
||||||
@@ -793,7 +799,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
|||||||
IrisBiome floorBiome = resolveCaveBoundaryBiome(mc, rx, zone.floor, rz, xx, zz, resolverState, caveBiomeCache, customBiomeCache);
|
IrisBiome floorBiome = resolveCaveBoundaryBiome(mc, rx, zone.floor, rz, xx, zz, resolverState, caveBiomeCache, customBiomeCache);
|
||||||
IrisBiome ceilingBiome = resolveCaveBoundaryBiome(mc, rx, zone.ceiling, rz, xx, zz, resolverState, caveBiomeCache, customBiomeCache);
|
IrisBiome ceilingBiome = resolveCaveBoundaryBiome(mc, rx, zone.ceiling, rz, xx, zz, resolverState, caveBiomeCache, customBiomeCache);
|
||||||
if (floorBiome == null && ceilingBiome == null) {
|
if (floorBiome == null && ceilingBiome == null) {
|
||||||
normalizeCaveZoneWaterlogging(output, mc, zone, rx, rz, columnFluid);
|
normalizeCaveZoneWaterlogging(output, mc, zone, rx, rz, xx, zz);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -865,7 +871,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
|||||||
decorant.getCeilingDecorator().decorate(rx, rz, xx, xx, xx, zz, zz, zz, output, ceilingBiome, InferredType.CAVE, zone.getCeiling(), zone.airThickness());
|
decorant.getCeilingDecorator().decorate(rx, rz, xx, xx, xx, zz, zz, zz, output, ceilingBiome, InferredType.CAVE, zone.getCeiling(), zone.airThickness());
|
||||||
}
|
}
|
||||||
|
|
||||||
normalizeCaveZoneWaterlogging(output, mc, zone, rx, rz, columnFluid);
|
normalizeCaveZoneWaterlogging(output, mc, zone, rx, rz, xx, zz);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void normalizeCaveZoneWaterlogging(
|
private void normalizeCaveZoneWaterlogging(
|
||||||
@@ -874,7 +880,8 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
|||||||
CaveZone zone,
|
CaveZone zone,
|
||||||
int localX,
|
int localX,
|
||||||
int localZ,
|
int localZ,
|
||||||
PlatformBlockState columnFluid
|
int worldX,
|
||||||
|
int worldZ
|
||||||
) {
|
) {
|
||||||
int minimumY = Math.max(0, zone.floor - 1);
|
int minimumY = Math.max(0, zone.floor - 1);
|
||||||
int maximumY = Math.min(output.getHeight() - 1, zone.ceiling + 1);
|
int maximumY = Math.min(output.getHeight() - 1, zone.ceiling + 1);
|
||||||
@@ -887,6 +894,11 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
|||||||
MatterCavern baseline = dataIfPresent(
|
MatterCavern baseline = dataIfPresent(
|
||||||
mantleChunk, localX, y, localZ, MatterCavern.class);
|
mantleChunk, localX, y, localZ, MatterCavern.class);
|
||||||
PlatformBlockState current = output.getRaw(localX, y, localZ);
|
PlatformBlockState current = output.getRaw(localX, y, localZ);
|
||||||
|
PlatformBlockState columnFluid = getComplex().resolveRiverCaveFluid(
|
||||||
|
hydrology.fluidKind(),
|
||||||
|
worldX,
|
||||||
|
worldZ
|
||||||
|
);
|
||||||
PlatformBlockState normalized = normalizeHydrologyWaterlogging(
|
PlatformBlockState normalized = normalizeHydrologyWaterlogging(
|
||||||
current,
|
current,
|
||||||
baseline,
|
baseline,
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ import lombok.experimental.Accessors;
|
|||||||
@Desc("A two-dimensional image-map coordinate")
|
@Desc("A two-dimensional image-map coordinate")
|
||||||
@Data
|
@Data
|
||||||
public class IrisImageMapOrigin {
|
public class IrisImageMapOrigin {
|
||||||
|
@Desc("X coordinate in world blocks for origin, or source pixels for sourceOrigin")
|
||||||
private double x = 0D;
|
private double x = 0D;
|
||||||
|
|
||||||
|
@Desc("Z coordinate in world blocks for origin, or source image Y pixels for sourceOrigin")
|
||||||
private double z = 0D;
|
private double z = 0D;
|
||||||
|
|
||||||
public IrisImageMapOrigin(double x, double z) {
|
public IrisImageMapOrigin(double x, double z) {
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ import art.arcane.iris.util.common.math.IrisBlockVector;
|
|||||||
import art.arcane.iris.util.common.math.IrisVector;
|
import art.arcane.iris.util.common.math.IrisVector;
|
||||||
import art.arcane.iris.util.project.interpolation.Interpolation3D;
|
import art.arcane.iris.util.project.interpolation.Interpolation3D;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Geometric transforms for {@link IrisObject}: rotation, scaling and the interpolated upscalers.
|
* Geometric transforms for {@link IrisObject}: rotation, scaling and the interpolated upscalers.
|
||||||
*/
|
*/
|
||||||
@@ -133,6 +138,7 @@ final class IrisObjectTransforms {
|
|||||||
VectorMap<PlatformBlockState> b = new VectorMap<>();
|
VectorMap<PlatformBlockState> b = new VectorMap<>();
|
||||||
IrisPosition min = self.getAABB().min();
|
IrisPosition min = self.getAABB().min();
|
||||||
IrisPosition max = self.getAABB().max();
|
IrisPosition max = self.getAABB().max();
|
||||||
|
NearestBlockIndex nearestBlocks = NearestBlockIndex.create(v);
|
||||||
|
|
||||||
for (int x = min.getX(); x <= max.getX(); x++) {
|
for (int x = min.getX(); x <= max.getX(); x++) {
|
||||||
for (int y = min.getY(); y <= max.getY(); y++) {
|
for (int y = min.getY(); y <= max.getY(); y++) {
|
||||||
@@ -146,7 +152,7 @@ final class IrisObjectTransforms {
|
|||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}) >= 0.5) {
|
}) >= 0.5) {
|
||||||
b.put(new IrisBlockVector(x, y, z), nearestBlockData(self, x, y, z));
|
b.put(new IrisBlockVector(x, y, z), nearestBlockData(v, nearestBlocks, x, y, z));
|
||||||
} else {
|
} else {
|
||||||
b.put(new IrisBlockVector(x, y, z), IrisObject.States.AIR);
|
b.put(new IrisBlockVector(x, y, z), IrisObject.States.AIR);
|
||||||
}
|
}
|
||||||
@@ -169,6 +175,7 @@ final class IrisObjectTransforms {
|
|||||||
VectorMap<PlatformBlockState> b = new VectorMap<>();
|
VectorMap<PlatformBlockState> b = new VectorMap<>();
|
||||||
IrisPosition min = self.getAABB().min();
|
IrisPosition min = self.getAABB().min();
|
||||||
IrisPosition max = self.getAABB().max();
|
IrisPosition max = self.getAABB().max();
|
||||||
|
NearestBlockIndex nearestBlocks = NearestBlockIndex.create(v);
|
||||||
|
|
||||||
for (int x = min.getX(); x <= max.getX(); x++) {
|
for (int x = min.getX(); x <= max.getX(); x++) {
|
||||||
for (int y = min.getY(); y <= max.getY(); y++) {
|
for (int y = min.getY(); y <= max.getY(); y++) {
|
||||||
@@ -182,7 +189,7 @@ final class IrisObjectTransforms {
|
|||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}) >= 0.5) {
|
}) >= 0.5) {
|
||||||
b.put(new IrisBlockVector(x, y, z), nearestBlockData(self, x, y, z));
|
b.put(new IrisBlockVector(x, y, z), nearestBlockData(v, nearestBlocks, x, y, z));
|
||||||
} else {
|
} else {
|
||||||
b.put(new IrisBlockVector(x, y, z), IrisObject.States.AIR);
|
b.put(new IrisBlockVector(x, y, z), IrisObject.States.AIR);
|
||||||
}
|
}
|
||||||
@@ -209,6 +216,7 @@ final class IrisObjectTransforms {
|
|||||||
VectorMap<PlatformBlockState> b = new VectorMap<>();
|
VectorMap<PlatformBlockState> b = new VectorMap<>();
|
||||||
IrisPosition min = self.getAABB().min();
|
IrisPosition min = self.getAABB().min();
|
||||||
IrisPosition max = self.getAABB().max();
|
IrisPosition max = self.getAABB().max();
|
||||||
|
NearestBlockIndex nearestBlocks = NearestBlockIndex.create(v);
|
||||||
|
|
||||||
for (int x = min.getX(); x <= max.getX(); x++) {
|
for (int x = min.getX(); x <= max.getX(); x++) {
|
||||||
for (int y = min.getY(); y <= max.getY(); y++) {
|
for (int y = min.getY(); y <= max.getY(); y++) {
|
||||||
@@ -222,7 +230,7 @@ final class IrisObjectTransforms {
|
|||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}, tension, bias) >= 0.5) {
|
}, tension, bias) >= 0.5) {
|
||||||
b.put(new IrisBlockVector(x, y, z), nearestBlockData(self, x, y, z));
|
b.put(new IrisBlockVector(x, y, z), nearestBlockData(v, nearestBlocks, x, y, z));
|
||||||
} else {
|
} else {
|
||||||
b.put(new IrisBlockVector(x, y, z), IrisObject.States.AIR);
|
b.put(new IrisBlockVector(x, y, z), IrisObject.States.AIR);
|
||||||
}
|
}
|
||||||
@@ -238,36 +246,124 @@ final class IrisObjectTransforms {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static PlatformBlockState nearestBlockData(IrisObject self, int x, int y, int z) {
|
private static PlatformBlockState nearestBlockData(VectorMap<PlatformBlockState> blocks,
|
||||||
|
NearestBlockIndex nearestBlocks,
|
||||||
|
int x, int y, int z) {
|
||||||
IrisBlockVector vv = new IrisBlockVector(x, y, z);
|
IrisBlockVector vv = new IrisBlockVector(x, y, z);
|
||||||
self.readLock.lock();
|
PlatformBlockState direct = blocks.get(vv);
|
||||||
try {
|
if (!B.isAir(direct)) {
|
||||||
PlatformBlockState r = self.blocks.get(vv);
|
return direct;
|
||||||
|
}
|
||||||
if (!B.isAir(r)) {
|
return nearestBlocks.nearest(x, y, z, direct);
|
||||||
return r;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
double d = Double.MAX_VALUE;
|
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);
|
||||||
|
|
||||||
for (var entry : self.blocks) {
|
private final NearestNode root;
|
||||||
PlatformBlockState dat = entry.getValue();
|
private PlatformBlockState bestState;
|
||||||
|
private double bestDistance;
|
||||||
|
private int bestRank;
|
||||||
|
|
||||||
if (B.isAir(dat)) {
|
private NearestBlockIndex(NearestNode root) {
|
||||||
continue;
|
this.root = root;
|
||||||
}
|
}
|
||||||
|
|
||||||
double dx = entry.getKey().distanceSquared(vv);
|
static NearestBlockIndex create(VectorMap<PlatformBlockState> blocks) {
|
||||||
|
List<NearestBlock> points = new ArrayList<>(blocks.size());
|
||||||
if (dx < d) {
|
VectorMap<PlatformBlockState>.Cursor cursor = blocks.cursor();
|
||||||
d = dx;
|
int rank = 0;
|
||||||
r = dat;
|
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++;
|
||||||
}
|
}
|
||||||
|
|
||||||
return r;
|
NearestBlock[] pointArray = points.toArray(new NearestBlock[0]);
|
||||||
} finally {
|
return new NearestBlockIndex(build(pointArray, 0, pointArray.length, 0));
|
||||||
self.readLock.unlock();
|
}
|
||||||
|
|
||||||
|
PlatformBlockState nearest(int x, int y, int z, PlatformBlockState fallback) {
|
||||||
|
if (root == null) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.")
|
@Desc("The policy for fluid already present in a candidate contained cave body.")
|
||||||
private IrisRiverExistingFluidPolicy existingFluidPolicy = IrisRiverExistingFluidPolicy.REJECT;
|
private IrisRiverExistingFluidPolicy existingFluidPolicy = IrisRiverExistingFluidPolicy.REJECT;
|
||||||
|
|
||||||
|
@Desc("Sparse, independently filled cave-floor pools generated at deep river-network anchors.")
|
||||||
|
private IrisRiverDeepPools deepPools = new IrisRiverDeepPools();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package art.arcane.iris.engine.object;
|
||||||
|
|
||||||
|
import art.arcane.iris.engine.object.annotations.Desc;
|
||||||
|
import art.arcane.iris.engine.object.annotations.MaxNumber;
|
||||||
|
import art.arcane.iris.engine.object.annotations.MinNumber;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
@Accessors(chain = true)
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Desc("Controls sparse, river-anchored fluid pools attached to deep cave floors.")
|
||||||
|
@Data
|
||||||
|
public class IrisRiverDeepPools {
|
||||||
|
@Desc("Enables independently configured deep cave pools along eligible wet river reaches.")
|
||||||
|
private boolean enabled = false;
|
||||||
|
|
||||||
|
@Desc("Selects complete wet river reaches that may host deep pools.")
|
||||||
|
private IrisRiverNoiseChance reach = new IrisRiverNoiseChance()
|
||||||
|
.setChance(1D / 3D)
|
||||||
|
.setStyle(new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(4096D))
|
||||||
|
.setInfluence(0.08D);
|
||||||
|
|
||||||
|
@MinNumber(16)
|
||||||
|
@MaxNumber(4096)
|
||||||
|
@Desc("The minimum distance in blocks between deep-pool candidates.")
|
||||||
|
private int minimumSpacing = 768;
|
||||||
|
|
||||||
|
@MinNumber(0)
|
||||||
|
@MaxNumber(16)
|
||||||
|
@Desc("The maximum accepted deep pools on one river reach.")
|
||||||
|
private int maximumPerReach = 1;
|
||||||
|
|
||||||
|
@MinNumber(-2048)
|
||||||
|
@MaxNumber(2048)
|
||||||
|
@Desc("The lowest absolute world Y considered for the pool fluid surface.")
|
||||||
|
private int minimumFluidY = -224;
|
||||||
|
|
||||||
|
@MinNumber(-2048)
|
||||||
|
@MaxNumber(2048)
|
||||||
|
@Desc("The highest absolute world Y considered for the pool fluid surface.")
|
||||||
|
private int maximumFluidY = -104;
|
||||||
|
|
||||||
|
@MinNumber(0)
|
||||||
|
@MaxNumber(256)
|
||||||
|
@Desc("The horizontal distance searched from a river anchor for a contained cave floor.")
|
||||||
|
private int searchRadius = 16;
|
||||||
|
|
||||||
|
@MinNumber(1)
|
||||||
|
@MaxNumber(64)
|
||||||
|
@Desc("The number of deterministic nearby columns tested for a contained cave floor.")
|
||||||
|
private int searchAttempts = 12;
|
||||||
|
|
||||||
|
@MinNumber(2)
|
||||||
|
@MaxNumber(128)
|
||||||
|
@Desc("The horizontal radius of the generated deep-pool chamber.")
|
||||||
|
private int horizontalRadius = 18;
|
||||||
|
|
||||||
|
@MinNumber(2)
|
||||||
|
@MaxNumber(64)
|
||||||
|
@Desc("The vertical radius of the generated deep-pool chamber.")
|
||||||
|
private int verticalRadius = 8;
|
||||||
|
|
||||||
|
@MinNumber(1)
|
||||||
|
@MaxNumber(63)
|
||||||
|
@Desc("The dry chamber height retained above the deep-pool fluid surface.")
|
||||||
|
private int dryHeadroom = 4;
|
||||||
|
|
||||||
|
@Desc("Noise shaping the deep-pool chamber boundary.")
|
||||||
|
private IrisGeneratorStyle shapeStyle = new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(12D);
|
||||||
|
|
||||||
|
@MinNumber(0)
|
||||||
|
@MaxNumber(0.75)
|
||||||
|
@Desc("The proportional noise displacement applied to the deep-pool chamber boundary.")
|
||||||
|
private double shapeVariation = 0.5D;
|
||||||
|
|
||||||
|
@Desc("Noise warping the deep-pool chamber coordinate field.")
|
||||||
|
private IrisGeneratorStyle warpStyle = new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(24D);
|
||||||
|
|
||||||
|
@MinNumber(0)
|
||||||
|
@MaxNumber(64)
|
||||||
|
@Desc("The maximum coordinate warp applied to the deep-pool chamber in blocks.")
|
||||||
|
private double warpStrength = 6D;
|
||||||
|
|
||||||
|
@MinNumber(64)
|
||||||
|
@MaxNumber(1048576)
|
||||||
|
@Desc("The greatest generated deep-pool chamber volume that may be transactionally published.")
|
||||||
|
private int maximumVolume = 32768;
|
||||||
|
|
||||||
|
@Desc("The fluid palette used only by accepted deep pools.")
|
||||||
|
private IrisMaterialPalette fluidPalette = new IrisMaterialPalette().qclear().qadd("lava");
|
||||||
|
}
|
||||||
@@ -24,6 +24,11 @@ public class IrisRiverTerrain {
|
|||||||
@Desc("The wet-bed depth below the local water surface, or dry-channel depth below natural terrain, in blocks.")
|
@Desc("The wet-bed depth below the local water surface, or dry-channel depth below natural terrain, in blocks.")
|
||||||
private IrisStyledRange depth = range(2D, 7D, NoiseStyle.IRIS, 768D);
|
private IrisStyledRange depth = range(2D, 7D, NoiseStyle.IRIS, 768D);
|
||||||
|
|
||||||
|
@MinNumber(0)
|
||||||
|
@MaxNumber(64)
|
||||||
|
@Desc("The radius added to every channel after worm, regional, local, and stream-order shaping.")
|
||||||
|
private double channelRadiusBonus = 0D;
|
||||||
|
|
||||||
@MinNumber(1)
|
@MinNumber(1)
|
||||||
@MaxNumber(2048)
|
@MaxNumber(2048)
|
||||||
@Desc("The final wet-channel width cap after region, biome, and stream-order scaling.")
|
@Desc("The final wet-channel width cap after region, biome, and stream-order scaling.")
|
||||||
@@ -64,7 +69,7 @@ public class IrisRiverTerrain {
|
|||||||
|
|
||||||
@MinNumber(0)
|
@MinNumber(0)
|
||||||
@MaxNumber(16)
|
@MaxNumber(16)
|
||||||
@Desc("The extra lateral tunnel-mouth blend carved on each side where a surface river enters or exits solid terrain.")
|
@Desc("The longitudinal transition length and maximum lateral and roof flare where a surface river enters or exits solid terrain.")
|
||||||
private double tunnelMouthBlend = 2D;
|
private double tunnelMouthBlend = 2D;
|
||||||
|
|
||||||
@Desc("Noise modulating the submerged floor of river tunnels.")
|
@Desc("Noise modulating the submerged floor of river tunnels.")
|
||||||
|
|||||||
@@ -13,7 +13,15 @@ import lombok.experimental.Accessors;
|
|||||||
@Data
|
@Data
|
||||||
public class IrisRiverWater {
|
public class IrisRiverWater {
|
||||||
@Desc("The strategy used to determine river water-surface height.")
|
@Desc("The strategy used to determine river water-surface height.")
|
||||||
private IrisRiverWaterMode mode = IrisRiverWaterMode.SEA_LEVEL;
|
private IrisRiverWaterMode mode = IrisRiverWaterMode.FIXED;
|
||||||
|
|
||||||
|
@MinNumber(-2048)
|
||||||
|
@MaxNumber(2048)
|
||||||
|
@Desc("The base river fluid surface in absolute world Y, independent of the dimension ocean height.")
|
||||||
|
private int fluidHeight = 63;
|
||||||
|
|
||||||
|
@Desc("The river fluid palette used by surface channels, contained tunnels, grottos, and waterfall throats.")
|
||||||
|
private IrisMaterialPalette fluidPalette = new IrisMaterialPalette().qclear().qadd("water");
|
||||||
|
|
||||||
@MinNumber(8)
|
@MinNumber(8)
|
||||||
@MaxNumber(4096)
|
@MaxNumber(4096)
|
||||||
@@ -22,7 +30,7 @@ public class IrisRiverWater {
|
|||||||
|
|
||||||
@MinNumber(0)
|
@MinNumber(0)
|
||||||
@MaxNumber(64)
|
@MaxNumber(64)
|
||||||
@Desc("The greatest river water height permitted above the dimension fluid height.")
|
@Desc("The greatest terraced river height permitted above fluidHeight.")
|
||||||
private int maximumPoolRise = 4;
|
private int maximumPoolRise = 4;
|
||||||
|
|
||||||
@MinNumber(1)
|
@MinNumber(1)
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import art.arcane.iris.engine.object.annotations.Desc;
|
|||||||
|
|
||||||
@Desc("Selects how a river determines its surface fluid height.")
|
@Desc("Selects how a river determines its surface fluid height.")
|
||||||
public enum IrisRiverWaterMode {
|
public enum IrisRiverWaterMode {
|
||||||
@Desc("Use the dimension fluid height for every wet river reach.")
|
@Desc("Use the river water configuration's fixed fluid height for every wet reach.")
|
||||||
SEA_LEVEL,
|
FIXED,
|
||||||
|
|
||||||
@Desc("Use flat pools connected by controlled vertical drops.")
|
@Desc("Use flat pools connected by controlled vertical drops.")
|
||||||
TERRACED
|
TERRACED
|
||||||
|
|||||||
@@ -72,16 +72,21 @@ public class IrisRiverWorm {
|
|||||||
@Desc("Depth multiplier for reaches selecting this worm.")
|
@Desc("Depth multiplier for reaches selecting this worm.")
|
||||||
private double depthMultiplier = 1D;
|
private double depthMultiplier = 1D;
|
||||||
|
|
||||||
@MinNumber(32)
|
@MinNumber(8)
|
||||||
@MaxNumber(16384)
|
@MaxNumber(16384)
|
||||||
@Desc("Primary world-space wavelength controlling longitudinal body swelling and pinching.")
|
@Desc("Primary world-space wavelength controlling longitudinal body swelling and pinching.")
|
||||||
private double bodyWavelength = 512D;
|
private double bodyWavelength = 512D;
|
||||||
|
|
||||||
@MinNumber(32)
|
@MinNumber(8)
|
||||||
@MaxNumber(16384)
|
@MaxNumber(16384)
|
||||||
@Desc("Detail wavelength adding smaller changes to the longitudinal body profile.")
|
@Desc("Detail wavelength adding smaller changes to the longitudinal body profile.")
|
||||||
private double bodyDetailWavelength = 128D;
|
private double bodyDetailWavelength = 128D;
|
||||||
|
|
||||||
|
@MinNumber(0)
|
||||||
|
@MaxNumber(1)
|
||||||
|
@Desc("Share of the longitudinal body field supplied by bodyDetailWavelength; the remainder uses bodyWavelength.")
|
||||||
|
private double bodyDetailInfluence = 0.3D;
|
||||||
|
|
||||||
@MinNumber(0)
|
@MinNumber(0)
|
||||||
@MaxNumber(0.875)
|
@MaxNumber(0.875)
|
||||||
@Desc("Maximum proportional channel-width variation along this style's body.")
|
@Desc("Maximum proportional channel-width variation along this style's body.")
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
|||||||
private final AtomicInteger a = new AtomicInteger(0);
|
private final AtomicInteger a = new AtomicInteger(0);
|
||||||
private volatile long lastChunkGenTime = 0L;
|
private volatile long lastChunkGenTime = 0L;
|
||||||
private final CompletableFuture<Integer> spawnChunks = new CompletableFuture<>();
|
private final CompletableFuture<Integer> spawnChunks = new CompletableFuture<>();
|
||||||
|
private final CompletableFuture<Void> initialSpawnReady = new CompletableFuture<>();
|
||||||
private final AtomicCache<EngineTarget> targetCache = new AtomicCache<>();
|
private final AtomicCache<EngineTarget> targetCache = new AtomicCache<>();
|
||||||
private final AtomicReference<CompletableFuture<Void>> closeFuture = new AtomicReference<>();
|
private final AtomicReference<CompletableFuture<Void>> closeFuture = new AtomicReference<>();
|
||||||
private volatile Engine engine;
|
private volatile Engine engine;
|
||||||
@@ -194,11 +195,19 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
|||||||
engine.getPlatformHooks().applyWorldBoundary(engine);
|
engine.getPlatformHooks().applyWorldBoundary(engine);
|
||||||
IrisLogging.debug("Injected Iris Biome Source into " + world.getName());
|
IrisLogging.debug("Injected Iris Biome Source into " + world.getName());
|
||||||
if (!studio) {
|
if (!studio) {
|
||||||
J.s(() -> updateSpawnLocation(world), 1);
|
J.sfut(() -> updateSpawnLocation(world), 1)
|
||||||
|
.whenComplete((ignored, failure) -> {
|
||||||
|
if (failure != null) {
|
||||||
|
initialSpawnReady.completeExceptionally(failure);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
initialSpawnReady.complete(null);
|
||||||
}
|
}
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
initializationFailure = e;
|
initializationFailure = e;
|
||||||
spawnChunks.completeExceptionally(e);
|
spawnChunks.completeExceptionally(e);
|
||||||
|
initialSpawnReady.completeExceptionally(e);
|
||||||
IrisLogging.reportError(e);
|
IrisLogging.reportError(e);
|
||||||
IrisLogging.error("Failed to initialize Iris generator for " + world.getName());
|
IrisLogging.error("Failed to initialize Iris generator for " + world.getName());
|
||||||
if (e instanceof RuntimeException runtimeException) {
|
if (e instanceof RuntimeException runtimeException) {
|
||||||
@@ -231,16 +240,58 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void updateSpawnLocation(World world) {
|
private void updateSpawnLocation(World world) {
|
||||||
|
try {
|
||||||
Location initialSpawn = getInitialSpawnLocation(world);
|
Location initialSpawn = getInitialSpawnLocation(world);
|
||||||
int chunkX = initialSpawn.getBlockX() >> 4;
|
int chunkX = initialSpawn.getBlockX() >> 4;
|
||||||
int chunkZ = initialSpawn.getBlockZ() >> 4;
|
int chunkZ = initialSpawn.getBlockZ() >> 4;
|
||||||
CompletableFuture<Chunk> chunkFuture = requestChunkAsync(world, chunkX, chunkZ, true);
|
CompletableFuture<Chunk> chunkFuture = requestChunkAsync(world, chunkX, chunkZ, true);
|
||||||
if (chunkFuture == null) {
|
if (chunkFuture == null) {
|
||||||
|
initialSpawnReady.completeExceptionally(new IllegalStateException(
|
||||||
|
"Initial spawn chunk request returned no completion future for world \""
|
||||||
|
+ world.getName() + "\"."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
chunkFuture.thenAccept(chunk ->
|
chunkFuture.whenComplete((chunk, failure) -> {
|
||||||
J.runRegion(chunk.getWorld(), chunk.getX(), chunk.getZ(), () -> applySpawnLocation(chunk.getWorld(), initialSpawn)));
|
try {
|
||||||
|
if (failure != null) {
|
||||||
|
initialSpawnReady.completeExceptionally(failure);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (chunk == null) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Initial spawn chunk request completed without a chunk for world \""
|
||||||
|
+ world.getName() + "\".");
|
||||||
|
}
|
||||||
|
J.runRegionFuture(
|
||||||
|
chunk.getWorld(),
|
||||||
|
chunk.getX(),
|
||||||
|
chunk.getZ(),
|
||||||
|
() -> completeSpawnLocation(chunk.getWorld(), initialSpawn))
|
||||||
|
.whenComplete((ignored, scheduleFailure) -> {
|
||||||
|
if (scheduleFailure != null) {
|
||||||
|
initialSpawnReady.completeExceptionally(scheduleFailure);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Throwable callbackFailure) {
|
||||||
|
initialSpawnReady.completeExceptionally(new IllegalStateException(
|
||||||
|
"Initial spawn preparation failed for world \""
|
||||||
|
+ world.getName() + "\".",
|
||||||
|
callbackFailure));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Throwable failure) {
|
||||||
|
initialSpawnReady.completeExceptionally(failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void completeSpawnLocation(World world, Location initialSpawn) {
|
||||||
|
try {
|
||||||
|
applySpawnLocation(world, initialSpawn);
|
||||||
|
initialSpawnReady.complete(null);
|
||||||
|
} catch (Throwable failure) {
|
||||||
|
initialSpawnReady.completeExceptionally(failure);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applySpawnLocation(World world, Location initialSpawn) {
|
private void applySpawnLocation(World world, Location initialSpawn) {
|
||||||
@@ -255,10 +306,27 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
|||||||
|
|
||||||
int minY = world.getMinHeight() + 1;
|
int minY = world.getMinHeight() + 1;
|
||||||
int maxY = world.getMaxHeight() - 2;
|
int maxY = world.getMaxHeight() - 2;
|
||||||
int y = Math.max(minY, Math.min(maxY, world.getHighestBlockYAt(initialSpawn) + 1));
|
int y = resolveInitialSpawnY(world, initialSpawn, minY, maxY);
|
||||||
world.setSpawnLocation(new Location(world, initialSpawn.getX(), y, initialSpawn.getZ(), initialSpawn.getYaw(), initialSpawn.getPitch()));
|
world.setSpawnLocation(new Location(world, initialSpawn.getX(), y, initialSpawn.getZ(), initialSpawn.getYaw(), initialSpawn.getPitch()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int resolveInitialSpawnY(World world, Location initialSpawn, int minY, int maxY) {
|
||||||
|
Engine activeEngine = engine;
|
||||||
|
if (activeEngine != null && activeEngine.getComplex() != null && activeEngine.getComplex().getHeightStream() != null) {
|
||||||
|
int generatedY = activeEngine.getMinHeight()
|
||||||
|
+ activeEngine.getComplex().getHeightStream().get(initialSpawn.getX(), initialSpawn.getZ()).intValue()
|
||||||
|
+ 1;
|
||||||
|
return Math.max(minY, Math.min(maxY, generatedY));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.max(minY, Math.min(maxY, world.getHighestBlockYAt(initialSpawn) + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<Void> getInitialSpawnReady() {
|
||||||
|
return initialSpawnReady;
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
private CompletableFuture<Chunk> requestChunkAsync(World world, int chunkX, int chunkZ, boolean generate) {
|
private CompletableFuture<Chunk> requestChunkAsync(World world, int chunkX, int chunkZ, boolean generate) {
|
||||||
try {
|
try {
|
||||||
@@ -377,6 +445,11 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
|||||||
this.hotloader = shouldRunStudioHotload(studio, closing, jigsawStudioActive) ? new Looper() {
|
this.hotloader = shouldRunStudioHotload(studio, closing, jigsawStudioActive) ? new Looper() {
|
||||||
@Override
|
@Override
|
||||||
protected long loop() {
|
protected long loop() {
|
||||||
|
Engine activeEngine = engine;
|
||||||
|
if (activeEngine instanceof IrisEngine irisEngine
|
||||||
|
&& irisEngine.isGenerationCacheWarmPending()) {
|
||||||
|
return HOTLOAD_LOOP_DELAY_MS;
|
||||||
|
}
|
||||||
if (shouldThrottleHotload()) {
|
if (shouldThrottleHotload()) {
|
||||||
return HOTLOAD_MAINTENANCE_DELAY_MS;
|
return HOTLOAD_MAINTENANCE_DELAY_MS;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,4 +76,8 @@ public interface PlatformChunkGenerator extends Hotloadable, DataProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
CompletableFuture<Integer> getSpawnChunks();
|
CompletableFuture<Integer> getSpawnChunks();
|
||||||
|
|
||||||
|
default CompletableFuture<Void> getInitialSpawnReady() {
|
||||||
|
return CompletableFuture.completedFuture(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,6 +119,15 @@ public final class RiverBodyProfile {
|
|||||||
return roofScales[index];
|
return roofScales[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int intervalIndex(double alongReach) {
|
||||||
|
double position = StrictMath.max(0D, StrictMath.min(1D, alongReach));
|
||||||
|
int index = Arrays.binarySearch(positions, position);
|
||||||
|
if (index >= 0) {
|
||||||
|
return StrictMath.min(index, positions.length - 2);
|
||||||
|
}
|
||||||
|
return StrictMath.max(0, StrictMath.min(-index - 2, positions.length - 2));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object object) {
|
public boolean equals(Object object) {
|
||||||
if (this == object) {
|
if (this == object) {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ public final class RiverNetwork {
|
|||||||
private static final long BRANCH_SLOT_SALT = 0x94D049BB133111EBL;
|
private static final long BRANCH_SLOT_SALT = 0x94D049BB133111EBL;
|
||||||
private static final long BRANCH_GATE_SALT = 0x2545F4914F6CDD1DL;
|
private static final long BRANCH_GATE_SALT = 0x2545F4914F6CDD1DL;
|
||||||
private static final int MINIMUM_BODY_PROFILE_SAMPLES = 12;
|
private static final int MINIMUM_BODY_PROFILE_SAMPLES = 12;
|
||||||
private static final int MAXIMUM_BODY_PROFILE_SAMPLES = 32;
|
private static final int MAXIMUM_BODY_PROFILE_SAMPLES = 512;
|
||||||
private static final double PERLIN_NORMALIZATION = 1.4142135623730951D;
|
private static final double PERLIN_NORMALIZATION = 1.4142135623730951D;
|
||||||
|
|
||||||
private final RiverNetworkOptions options;
|
private final RiverNetworkOptions options;
|
||||||
@@ -670,7 +670,8 @@ public final class RiverNetwork {
|
|||||||
worm.bodyDetailWavelength(),
|
worm.bodyDetailWavelength(),
|
||||||
worm.seed() ^ detailSalt
|
worm.seed() ^ detailSalt
|
||||||
);
|
);
|
||||||
return primary * 0.7D + detail * 0.3D;
|
double detailInfluence = worm.bodyDetailInfluence();
|
||||||
|
return primary * (1D - detailInfluence) + detail * detailInfluence;
|
||||||
}
|
}
|
||||||
|
|
||||||
private FlowTangent resolveFlowTangent(RiverNode node, RiverTerrainSampler terrain) {
|
private FlowTangent resolveFlowTangent(RiverNode node, RiverTerrainSampler terrain) {
|
||||||
@@ -1263,6 +1264,7 @@ public final class RiverNetwork {
|
|||||||
BODY_WIDTH_DETAIL_SALT,
|
BODY_WIDTH_DETAIL_SALT,
|
||||||
worm.widthVariation()
|
worm.widthVariation()
|
||||||
)
|
)
|
||||||
|
+ options.channelRadiusBonus() * 2D
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
double baseBankWidth = nonNegativeOrFallback(
|
double baseBankWidth = nonNegativeOrFallback(
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ public record RiverNetworkOptions(
|
|||||||
double channelWidth,
|
double channelWidth,
|
||||||
double bankWidth,
|
double bankWidth,
|
||||||
double depth,
|
double depth,
|
||||||
|
double channelRadiusBonus,
|
||||||
double maxChannelWidth,
|
double maxChannelWidth,
|
||||||
double maxBankWidth,
|
double maxBankWidth,
|
||||||
double maxDepth,
|
double maxDepth,
|
||||||
@@ -60,6 +61,7 @@ public record RiverNetworkOptions(
|
|||||||
requirePositive(channelWidth, "channelWidth");
|
requirePositive(channelWidth, "channelWidth");
|
||||||
requireFiniteNonNegative(bankWidth, "bankWidth");
|
requireFiniteNonNegative(bankWidth, "bankWidth");
|
||||||
requirePositive(depth, "depth");
|
requirePositive(depth, "depth");
|
||||||
|
requireFiniteNonNegative(channelRadiusBonus, "channelRadiusBonus");
|
||||||
requirePositive(maxChannelWidth, "maxChannelWidth");
|
requirePositive(maxChannelWidth, "maxChannelWidth");
|
||||||
requireFiniteNonNegative(maxBankWidth, "maxBankWidth");
|
requireFiniteNonNegative(maxBankWidth, "maxBankWidth");
|
||||||
requirePositive(maxDepth, "maxDepth");
|
requirePositive(maxDepth, "maxDepth");
|
||||||
@@ -215,6 +217,7 @@ public record RiverNetworkOptions(
|
|||||||
private double channelWidth;
|
private double channelWidth;
|
||||||
private double bankWidth;
|
private double bankWidth;
|
||||||
private double depth;
|
private double depth;
|
||||||
|
private double channelRadiusBonus;
|
||||||
private double maxChannelWidth;
|
private double maxChannelWidth;
|
||||||
private double maxBankWidth;
|
private double maxBankWidth;
|
||||||
private double maxDepth;
|
private double maxDepth;
|
||||||
@@ -269,6 +272,7 @@ public record RiverNetworkOptions(
|
|||||||
1D,
|
1D,
|
||||||
512D,
|
512D,
|
||||||
128D,
|
128D,
|
||||||
|
0.3D,
|
||||||
0D,
|
0D,
|
||||||
0D,
|
0D,
|
||||||
0D,
|
0D,
|
||||||
@@ -397,6 +401,11 @@ public record RiverNetworkOptions(
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Builder channelRadiusBonus(double value) {
|
||||||
|
channelRadiusBonus = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public Builder maxChannelWidth(double value) {
|
public Builder maxChannelWidth(double value) {
|
||||||
maxChannelWidth = value;
|
maxChannelWidth = value;
|
||||||
return this;
|
return this;
|
||||||
@@ -461,6 +470,7 @@ public record RiverNetworkOptions(
|
|||||||
channelWidth,
|
channelWidth,
|
||||||
bankWidth,
|
bankWidth,
|
||||||
depth,
|
depth,
|
||||||
|
channelRadiusBonus,
|
||||||
maxChannelWidth,
|
maxChannelWidth,
|
||||||
maxBankWidth,
|
maxBankWidth,
|
||||||
maxDepth,
|
maxDepth,
|
||||||
|
|||||||
@@ -325,25 +325,35 @@ public final class RiverTile {
|
|||||||
double additionalRadius
|
double additionalRadius
|
||||||
) {
|
) {
|
||||||
RiverPolyline polyline = reach.polyline();
|
RiverPolyline polyline = reach.polyline();
|
||||||
if (polyline.length() == 0D) {
|
RiverBodyProfile bodyProfile = reach.bodyProfile();
|
||||||
|
double polylineLength = polyline.length();
|
||||||
|
if (polylineLength == 0D) {
|
||||||
double distanceSquared = squared(x - polyline.x(0)) + squared(z - polyline.z(0));
|
double distanceSquared = squared(x - polyline.x(0)) + squared(z - polyline.z(0));
|
||||||
double radius = reach.widthAt(0D) * 0.5D + reach.bankWidthAt(0D) + additionalRadius;
|
double radius = reach.widthAt(0D) * 0.5D + reach.bankWidthAt(0D) + additionalRadius;
|
||||||
return distanceSquared <= radius * radius ? new ClosestPoint(distanceSquared, 0D) : null;
|
return distanceSquared <= radius * radius ? new ClosestPoint(distanceSquared, 0D) : null;
|
||||||
}
|
}
|
||||||
double nearest = Double.POSITIVE_INFINITY;
|
double nearest = Double.POSITIVE_INFINITY;
|
||||||
double nearestAlong = 0.0;
|
double nearestAlong = 0.0;
|
||||||
for (int point = 0; point < polyline.size() - 1; point++) {
|
int pointLimit = polyline.size() - 1;
|
||||||
double segmentStartAlong = polyline.cumulativeLength(point) / polyline.length();
|
int profileLimit = bodyProfile.size() - 1;
|
||||||
double segmentEndAlong = polyline.cumulativeLength(point + 1) / polyline.length();
|
for (int point = 0; point < pointLimit; point++) {
|
||||||
|
double segmentStartAlong = polyline.cumulativeLength(point) / polylineLength;
|
||||||
|
double segmentEndAlong = polyline.cumulativeLength(point + 1) / polylineLength;
|
||||||
double segmentAlongSpan = segmentEndAlong - segmentStartAlong;
|
double segmentAlongSpan = segmentEndAlong - segmentStartAlong;
|
||||||
if (segmentAlongSpan == 0D) {
|
if (segmentAlongSpan == 0D) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
double deltaX = polyline.x(point + 1) - polyline.x(point);
|
double startX = polyline.x(point);
|
||||||
double deltaZ = polyline.z(point + 1) - polyline.z(point);
|
double startZ = polyline.z(point);
|
||||||
for (int profileIndex = 0; profileIndex < reach.bodyProfile().size() - 1; profileIndex++) {
|
double deltaX = polyline.x(point + 1) - startX;
|
||||||
double profileStart = reach.bodyProfile().position(profileIndex);
|
double deltaZ = polyline.z(point + 1) - startZ;
|
||||||
double profileEnd = reach.bodyProfile().position(profileIndex + 1);
|
int firstProfileIndex = bodyProfile.intervalIndex(segmentStartAlong);
|
||||||
|
for (int profileIndex = firstProfileIndex;
|
||||||
|
profileIndex < profileLimit
|
||||||
|
&& bodyProfile.position(profileIndex) <= segmentEndAlong;
|
||||||
|
profileIndex++) {
|
||||||
|
double profileStart = bodyProfile.position(profileIndex);
|
||||||
|
double profileEnd = bodyProfile.position(profileIndex + 1);
|
||||||
double overlapStart = StrictMath.max(segmentStartAlong, profileStart);
|
double overlapStart = StrictMath.max(segmentStartAlong, profileStart);
|
||||||
double overlapEnd = StrictMath.min(segmentEndAlong, profileEnd);
|
double overlapEnd = StrictMath.min(segmentEndAlong, profileEnd);
|
||||||
if (overlapStart > overlapEnd) {
|
if (overlapStart > overlapEnd) {
|
||||||
@@ -351,13 +361,16 @@ public final class RiverTile {
|
|||||||
}
|
}
|
||||||
double intervalStart = (overlapStart - segmentStartAlong) / segmentAlongSpan;
|
double intervalStart = (overlapStart - segmentStartAlong) / segmentAlongSpan;
|
||||||
double intervalEnd = (overlapEnd - segmentStartAlong) / segmentAlongSpan;
|
double intervalEnd = (overlapEnd - segmentStartAlong) / segmentAlongSpan;
|
||||||
double widthSlope = (reach.bodyProfile().widthAtIndex(profileIndex + 1)
|
double profileSpan = profileEnd - profileStart;
|
||||||
- reach.bodyProfile().widthAtIndex(profileIndex)) / (profileEnd - profileStart);
|
double profileWidth = bodyProfile.widthAtIndex(profileIndex);
|
||||||
double bankSlope = (reach.bodyProfile().bankWidthAtIndex(profileIndex + 1)
|
double profileBankWidth = bodyProfile.bankWidthAtIndex(profileIndex);
|
||||||
- reach.bodyProfile().bankWidthAtIndex(profileIndex)) / (profileEnd - profileStart);
|
double widthSlope = (bodyProfile.widthAtIndex(profileIndex + 1)
|
||||||
double radiusBase = (reach.bodyProfile().widthAtIndex(profileIndex)
|
- profileWidth) / profileSpan;
|
||||||
|
double bankSlope = (bodyProfile.bankWidthAtIndex(profileIndex + 1)
|
||||||
|
- profileBankWidth) / profileSpan;
|
||||||
|
double radiusBase = (profileWidth
|
||||||
+ widthSlope * (segmentStartAlong - profileStart)) * 0.5D
|
+ widthSlope * (segmentStartAlong - profileStart)) * 0.5D
|
||||||
+ reach.bodyProfile().bankWidthAtIndex(profileIndex)
|
+ profileBankWidth
|
||||||
+ bankSlope * (segmentStartAlong - profileStart)
|
+ bankSlope * (segmentStartAlong - profileStart)
|
||||||
+ additionalRadius;
|
+ additionalRadius;
|
||||||
double radiusSlope = (widthSlope * 0.5D + bankSlope) * segmentAlongSpan;
|
double radiusSlope = (widthSlope * 0.5D + bankSlope) * segmentAlongSpan;
|
||||||
@@ -365,9 +378,9 @@ public final class RiverTile {
|
|||||||
intervalStart,
|
intervalStart,
|
||||||
intervalEnd,
|
intervalEnd,
|
||||||
deltaX,
|
deltaX,
|
||||||
polyline.x(point) - x,
|
startX - x,
|
||||||
deltaZ,
|
deltaZ,
|
||||||
polyline.z(point) - z,
|
startZ - z,
|
||||||
radiusSlope,
|
radiusSlope,
|
||||||
radiusBase,
|
radiusBase,
|
||||||
segmentStartAlong,
|
segmentStartAlong,
|
||||||
@@ -390,7 +403,9 @@ public final class RiverTile {
|
|||||||
double maximumZ
|
double maximumZ
|
||||||
) {
|
) {
|
||||||
RiverPolyline polyline = reach.polyline();
|
RiverPolyline polyline = reach.polyline();
|
||||||
if (polyline.length() == 0D) {
|
RiverBodyProfile bodyProfile = reach.bodyProfile();
|
||||||
|
double polylineLength = polyline.length();
|
||||||
|
if (polylineLength == 0D) {
|
||||||
double distanceSquared = pointRectangleDistanceSquared(
|
double distanceSquared = pointRectangleDistanceSquared(
|
||||||
polyline.x(0),
|
polyline.x(0),
|
||||||
polyline.z(0),
|
polyline.z(0),
|
||||||
@@ -404,9 +419,11 @@ public final class RiverTile {
|
|||||||
}
|
}
|
||||||
double nearest = Double.POSITIVE_INFINITY;
|
double nearest = Double.POSITIVE_INFINITY;
|
||||||
double nearestAlong = 0.0;
|
double nearestAlong = 0.0;
|
||||||
for (int point = 0; point < polyline.size() - 1; point++) {
|
int pointLimit = polyline.size() - 1;
|
||||||
double segmentStartAlong = polyline.cumulativeLength(point) / polyline.length();
|
int profileLimit = bodyProfile.size() - 1;
|
||||||
double segmentEndAlong = polyline.cumulativeLength(point + 1) / polyline.length();
|
for (int point = 0; point < pointLimit; point++) {
|
||||||
|
double segmentStartAlong = polyline.cumulativeLength(point) / polylineLength;
|
||||||
|
double segmentEndAlong = polyline.cumulativeLength(point + 1) / polylineLength;
|
||||||
double segmentAlongSpan = segmentEndAlong - segmentStartAlong;
|
double segmentAlongSpan = segmentEndAlong - segmentStartAlong;
|
||||||
if (segmentAlongSpan == 0D) {
|
if (segmentAlongSpan == 0D) {
|
||||||
continue;
|
continue;
|
||||||
@@ -415,9 +432,13 @@ public final class RiverTile {
|
|||||||
double startZ = polyline.z(point);
|
double startZ = polyline.z(point);
|
||||||
double deltaX = polyline.x(point + 1) - startX;
|
double deltaX = polyline.x(point + 1) - startX;
|
||||||
double deltaZ = polyline.z(point + 1) - startZ;
|
double deltaZ = polyline.z(point + 1) - startZ;
|
||||||
for (int profileIndex = 0; profileIndex < reach.bodyProfile().size() - 1; profileIndex++) {
|
int firstProfileIndex = bodyProfile.intervalIndex(segmentStartAlong);
|
||||||
double profileStart = reach.bodyProfile().position(profileIndex);
|
for (int profileIndex = firstProfileIndex;
|
||||||
double profileEnd = reach.bodyProfile().position(profileIndex + 1);
|
profileIndex < profileLimit
|
||||||
|
&& bodyProfile.position(profileIndex) <= segmentEndAlong;
|
||||||
|
profileIndex++) {
|
||||||
|
double profileStart = bodyProfile.position(profileIndex);
|
||||||
|
double profileEnd = bodyProfile.position(profileIndex + 1);
|
||||||
double overlapStart = StrictMath.max(segmentStartAlong, profileStart);
|
double overlapStart = StrictMath.max(segmentStartAlong, profileStart);
|
||||||
double overlapEnd = StrictMath.min(segmentEndAlong, profileEnd);
|
double overlapEnd = StrictMath.min(segmentEndAlong, profileEnd);
|
||||||
if (overlapStart > overlapEnd) {
|
if (overlapStart > overlapEnd) {
|
||||||
@@ -425,13 +446,16 @@ public final class RiverTile {
|
|||||||
}
|
}
|
||||||
double intervalStart = (overlapStart - segmentStartAlong) / segmentAlongSpan;
|
double intervalStart = (overlapStart - segmentStartAlong) / segmentAlongSpan;
|
||||||
double intervalEnd = (overlapEnd - segmentStartAlong) / segmentAlongSpan;
|
double intervalEnd = (overlapEnd - segmentStartAlong) / segmentAlongSpan;
|
||||||
double widthSlope = (reach.bodyProfile().widthAtIndex(profileIndex + 1)
|
double profileSpan = profileEnd - profileStart;
|
||||||
- reach.bodyProfile().widthAtIndex(profileIndex)) / (profileEnd - profileStart);
|
double profileWidth = bodyProfile.widthAtIndex(profileIndex);
|
||||||
double bankSlope = (reach.bodyProfile().bankWidthAtIndex(profileIndex + 1)
|
double profileBankWidth = bodyProfile.bankWidthAtIndex(profileIndex);
|
||||||
- reach.bodyProfile().bankWidthAtIndex(profileIndex)) / (profileEnd - profileStart);
|
double widthSlope = (bodyProfile.widthAtIndex(profileIndex + 1)
|
||||||
double radiusBase = (reach.bodyProfile().widthAtIndex(profileIndex)
|
- profileWidth) / profileSpan;
|
||||||
|
double bankSlope = (bodyProfile.bankWidthAtIndex(profileIndex + 1)
|
||||||
|
- profileBankWidth) / profileSpan;
|
||||||
|
double radiusBase = (profileWidth
|
||||||
+ widthSlope * (segmentStartAlong - profileStart)) * 0.5D
|
+ widthSlope * (segmentStartAlong - profileStart)) * 0.5D
|
||||||
+ reach.bodyProfile().bankWidthAtIndex(profileIndex)
|
+ profileBankWidth
|
||||||
+ bankSlope * (segmentStartAlong - profileStart);
|
+ bankSlope * (segmentStartAlong - profileStart);
|
||||||
double radiusSlope = (widthSlope * 0.5D + bankSlope) * segmentAlongSpan;
|
double radiusSlope = (widthSlope * 0.5D + bankSlope) * segmentAlongSpan;
|
||||||
double cursor = intervalStart;
|
double cursor = intervalStart;
|
||||||
@@ -626,8 +650,7 @@ public final class RiverTile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private List<RiverReach> indexedReaches(double x, double z) {
|
private List<RiverReach> indexedReaches(double x, double z) {
|
||||||
List<RiverReach> indexed = spatialIndex.get(bucketKey(bucket(x), bucket(z)));
|
return spatialIndex.getOrDefault(bucketKey(bucket(x), bucket(z)), List.of());
|
||||||
return indexed == null ? List.of() : indexed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<RiverReach> indexedReaches(
|
private List<RiverReach> indexedReaches(
|
||||||
@@ -641,12 +664,15 @@ public final class RiverTile {
|
|||||||
int maximumBucketX = bucket(StrictMath.nextDown(queryMaximumX));
|
int maximumBucketX = bucket(StrictMath.nextDown(queryMaximumX));
|
||||||
int minimumBucketZ = bucket(queryMinimumZ);
|
int minimumBucketZ = bucket(queryMinimumZ);
|
||||||
int maximumBucketZ = bucket(StrictMath.nextDown(queryMaximumZ));
|
int maximumBucketZ = bucket(StrictMath.nextDown(queryMaximumZ));
|
||||||
|
if (minimumBucketX == maximumBucketX && minimumBucketZ == maximumBucketZ) {
|
||||||
|
return spatialIndex.getOrDefault(
|
||||||
|
bucketKey(minimumBucketX, minimumBucketZ),
|
||||||
|
List.of()
|
||||||
|
);
|
||||||
|
}
|
||||||
for (int bucketX = minimumBucketX; bucketX <= maximumBucketX; bucketX++) {
|
for (int bucketX = minimumBucketX; bucketX <= maximumBucketX; bucketX++) {
|
||||||
for (int bucketZ = minimumBucketZ; bucketZ <= maximumBucketZ; bucketZ++) {
|
for (int bucketZ = minimumBucketZ; bucketZ <= maximumBucketZ; bucketZ++) {
|
||||||
List<RiverReach> bucketReaches = spatialIndex.get(bucketKey(bucketX, bucketZ));
|
indexed.addAll(spatialIndex.getOrDefault(bucketKey(bucketX, bucketZ), List.of()));
|
||||||
if (bucketReaches != null) {
|
|
||||||
indexed.addAll(bucketReaches);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return List.copyOf(indexed);
|
return List.copyOf(indexed);
|
||||||
@@ -662,26 +688,13 @@ public final class RiverTile {
|
|||||||
int maximumBucketX = bucket(queryMaximumX);
|
int maximumBucketX = bucket(queryMaximumX);
|
||||||
int minimumBucketZ = bucket(queryMinimumZ);
|
int minimumBucketZ = bucket(queryMinimumZ);
|
||||||
int maximumBucketZ = bucket(queryMaximumZ);
|
int maximumBucketZ = bucket(queryMaximumZ);
|
||||||
if (spatialIndex.isEmpty()) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
if (minimumBucketX == maximumBucketX && minimumBucketZ == maximumBucketZ) {
|
if (minimumBucketX == maximumBucketX && minimumBucketZ == maximumBucketZ) {
|
||||||
List<RiverReach> bucketReaches = spatialIndex.get(bucketKey(minimumBucketX, minimumBucketZ));
|
return indexedReaches(queryMinimumX, queryMinimumZ);
|
||||||
return bucketReaches == null ? List.of() : bucketReaches;
|
|
||||||
}
|
|
||||||
long bucketWidth = (long) maximumBucketX - minimumBucketX + 1L;
|
|
||||||
long bucketDepth = (long) maximumBucketZ - minimumBucketZ + 1L;
|
|
||||||
if (bucketWidth > spatialIndex.size() / bucketDepth
|
|
||||||
|| bucketWidth * bucketDepth >= spatialIndex.size()) {
|
|
||||||
return reaches;
|
|
||||||
}
|
}
|
||||||
LinkedHashSet<RiverReach> indexed = new LinkedHashSet<>();
|
LinkedHashSet<RiverReach> indexed = new LinkedHashSet<>();
|
||||||
for (int bucketX = minimumBucketX; bucketX <= maximumBucketX; bucketX++) {
|
for (int bucketX = minimumBucketX; bucketX <= maximumBucketX; bucketX++) {
|
||||||
for (int bucketZ = minimumBucketZ; bucketZ <= maximumBucketZ; bucketZ++) {
|
for (int bucketZ = minimumBucketZ; bucketZ <= maximumBucketZ; bucketZ++) {
|
||||||
List<RiverReach> bucketReaches = spatialIndex.get(bucketKey(bucketX, bucketZ));
|
indexed.addAll(spatialIndex.getOrDefault(bucketKey(bucketX, bucketZ), List.of()));
|
||||||
if (bucketReaches != null) {
|
|
||||||
indexed.addAll(bucketReaches);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return List.copyOf(indexed);
|
return List.copyOf(indexed);
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ public record RiverWorm(
|
|||||||
double depthMultiplier,
|
double depthMultiplier,
|
||||||
double bodyWavelength,
|
double bodyWavelength,
|
||||||
double bodyDetailWavelength,
|
double bodyDetailWavelength,
|
||||||
|
double bodyDetailInfluence,
|
||||||
double widthVariation,
|
double widthVariation,
|
||||||
double bankVariation,
|
double bankVariation,
|
||||||
double depthVariation,
|
double depthVariation,
|
||||||
@@ -44,8 +45,9 @@ public record RiverWorm(
|
|||||||
requireRange(widthMultiplier, 0.125D, 8D, "widthMultiplier");
|
requireRange(widthMultiplier, 0.125D, 8D, "widthMultiplier");
|
||||||
requireRange(bankMultiplier, 0.125D, 8D, "bankMultiplier");
|
requireRange(bankMultiplier, 0.125D, 8D, "bankMultiplier");
|
||||||
requireRange(depthMultiplier, 0.125D, 8D, "depthMultiplier");
|
requireRange(depthMultiplier, 0.125D, 8D, "depthMultiplier");
|
||||||
requireRange(bodyWavelength, 32D, 16384D, "bodyWavelength");
|
requireRange(bodyWavelength, 8D, 16384D, "bodyWavelength");
|
||||||
requireRange(bodyDetailWavelength, 32D, 16384D, "bodyDetailWavelength");
|
requireRange(bodyDetailWavelength, 8D, 16384D, "bodyDetailWavelength");
|
||||||
|
requireRange(bodyDetailInfluence, 0D, 1D, "bodyDetailInfluence");
|
||||||
requireRange(widthVariation, 0D, 0.875D, "widthVariation");
|
requireRange(widthVariation, 0D, 0.875D, "widthVariation");
|
||||||
requireRange(bankVariation, 0D, 0.875D, "bankVariation");
|
requireRange(bankVariation, 0D, 0.875D, "bankVariation");
|
||||||
requireRange(depthVariation, 0D, 0.875D, "depthVariation");
|
requireRange(depthVariation, 0D, 0.875D, "depthVariation");
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package art.arcane.iris.engine.river.cave;
|
|||||||
|
|
||||||
public enum RiverCaveAction {
|
public enum RiverCaveAction {
|
||||||
WET_SOURCE,
|
WET_SOURCE,
|
||||||
FALLING_WATER,
|
FALLING_FLUID,
|
||||||
DRY_AIR,
|
DRY_AIR,
|
||||||
SEAL_GUARD
|
SEAL_GUARD
|
||||||
}
|
}
|
||||||
|
|||||||
+107
-1
@@ -70,6 +70,7 @@ public final class RiverCaveContainmentPlanner {
|
|||||||
settings,
|
settings,
|
||||||
throat.positions()
|
throat.positions()
|
||||||
);
|
);
|
||||||
|
case DEEP_POOL -> planDeepPool(view, source, settings, throat.positions());
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,6 +303,111 @@ public final class RiverCaveContainmentPlanner {
|
|||||||
return accepted(view, source, actions);
|
return accepted(view, source, actions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private RiverCavePlan planDeepPool(
|
||||||
|
CaveVoxelView view,
|
||||||
|
RiverCaveSource source,
|
||||||
|
RiverCavePlannerSettings settings,
|
||||||
|
List<CavePosition> throat
|
||||||
|
) {
|
||||||
|
GrottoResult grotto = buildGrotto(source, settings);
|
||||||
|
if (grotto.rejection() != RiverCaveRejection.NONE) {
|
||||||
|
return rejected(source, grotto.rejection());
|
||||||
|
}
|
||||||
|
Set<CavePosition> chamber = grotto.positions();
|
||||||
|
Set<CavePosition> carve = new HashSet<>(chamber.size() + throat.size());
|
||||||
|
carve.addAll(chamber);
|
||||||
|
carve.addAll(throat);
|
||||||
|
|
||||||
|
RiverCaveRejection carveRejection = validateDeepPoolCarve(view, source, settings, carve);
|
||||||
|
if (carveRejection != RiverCaveRejection.NONE) {
|
||||||
|
return rejected(source, carveRejection);
|
||||||
|
}
|
||||||
|
BoundaryResult boundary = validateDeepPoolBoundary(view, source, settings, carve);
|
||||||
|
if (boundary.rejection() != RiverCaveRejection.NONE) {
|
||||||
|
return rejected(source, boundary.rejection());
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<CavePosition, RiverCaveAction> actions = new HashMap<>();
|
||||||
|
addChamberActions(actions, chamber, source.waterHeadY());
|
||||||
|
addThroatActions(actions, throat, source);
|
||||||
|
for (CavePosition position : boundary.sealGuards()) {
|
||||||
|
actions.put(position, RiverCaveAction.SEAL_GUARD);
|
||||||
|
}
|
||||||
|
return accepted(view, source, actions);
|
||||||
|
}
|
||||||
|
|
||||||
|
private RiverCaveRejection validateDeepPoolCarve(
|
||||||
|
CaveVoxelView view,
|
||||||
|
RiverCaveSource source,
|
||||||
|
RiverCavePlannerSettings settings,
|
||||||
|
Set<CavePosition> carve
|
||||||
|
) {
|
||||||
|
for (CavePosition position : carve) {
|
||||||
|
if (!view.isInWorld(position)) {
|
||||||
|
return RiverCaveRejection.WORLD_BOUNDARY;
|
||||||
|
}
|
||||||
|
RiverCaveRejection boundsRejection = validateBounds(source, settings, position);
|
||||||
|
if (boundsRejection != RiverCaveRejection.NONE) {
|
||||||
|
return boundsRejection;
|
||||||
|
}
|
||||||
|
CaveVoxel voxel = voxelAt(view, position);
|
||||||
|
RiverCaveRejection hazard = rejectionForHazard(voxel, settings);
|
||||||
|
if (hazard != RiverCaveRejection.NONE) {
|
||||||
|
return hazard;
|
||||||
|
}
|
||||||
|
if (voxel == CaveVoxel.SOLID) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (position.y() > source.waterHeadY()
|
||||||
|
&& voxel == CaveVoxel.CAVE_AIR
|
||||||
|
&& !view.isOpenToSurface(position)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return RiverCaveRejection.GROTTO_INTERSECTION;
|
||||||
|
}
|
||||||
|
return RiverCaveRejection.NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private BoundaryResult validateDeepPoolBoundary(
|
||||||
|
CaveVoxelView view,
|
||||||
|
RiverCaveSource source,
|
||||||
|
RiverCavePlannerSettings settings,
|
||||||
|
Set<CavePosition> carve
|
||||||
|
) {
|
||||||
|
Set<CavePosition> guards = new HashSet<>();
|
||||||
|
for (CavePosition position : carve) {
|
||||||
|
for (CavePosition direction : DIRECTIONS) {
|
||||||
|
CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z());
|
||||||
|
if (carve.contains(neighbor)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!view.isInWorld(neighbor)) {
|
||||||
|
return BoundaryResult.rejected(RiverCaveRejection.WORLD_BOUNDARY);
|
||||||
|
}
|
||||||
|
RiverCaveRejection boundsRejection = validateBounds(source, settings, neighbor);
|
||||||
|
if (boundsRejection != RiverCaveRejection.NONE) {
|
||||||
|
return BoundaryResult.rejected(boundsRejection);
|
||||||
|
}
|
||||||
|
CaveVoxel voxel = voxelAt(view, neighbor);
|
||||||
|
RiverCaveRejection hazard = rejectionForHazard(voxel, settings);
|
||||||
|
if (hazard != RiverCaveRejection.NONE) {
|
||||||
|
return BoundaryResult.rejected(hazard);
|
||||||
|
}
|
||||||
|
if (voxel == CaveVoxel.SOLID) {
|
||||||
|
guards.add(neighbor);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (neighbor.y() > source.waterHeadY()
|
||||||
|
&& voxel == CaveVoxel.CAVE_AIR
|
||||||
|
&& !view.isOpenToSurface(neighbor)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return BoundaryResult.rejected(RiverCaveRejection.GROTTO_SHELL_OPEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return BoundaryResult.accepted(guards);
|
||||||
|
}
|
||||||
|
|
||||||
private ComponentResult resolveClosedComponent(
|
private ComponentResult resolveClosedComponent(
|
||||||
CaveVoxelView view,
|
CaveVoxelView view,
|
||||||
RiverCaveSource source,
|
RiverCaveSource source,
|
||||||
@@ -732,7 +838,7 @@ public final class RiverCaveContainmentPlanner {
|
|||||||
action = RiverCaveAction.WET_SOURCE;
|
action = RiverCaveAction.WET_SOURCE;
|
||||||
} else if (source.mode() == RiverCaveMode.WATERFALL_POOL
|
} else if (source.mode() == RiverCaveMode.WATERFALL_POOL
|
||||||
|| source.mode() == RiverCaveMode.GENERATED_GROTTO) {
|
|| source.mode() == RiverCaveMode.GENERATED_GROTTO) {
|
||||||
action = RiverCaveAction.FALLING_WATER;
|
action = RiverCaveAction.FALLING_FLUID;
|
||||||
} else {
|
} else {
|
||||||
action = RiverCaveAction.DRY_AIR;
|
action = RiverCaveAction.DRY_AIR;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package art.arcane.iris.engine.river.cave;
|
||||||
|
|
||||||
|
public enum RiverCaveFluidKind {
|
||||||
|
RIVER,
|
||||||
|
DEEP_POOL
|
||||||
|
}
|
||||||
@@ -11,20 +11,30 @@ public final class RiverCaveHydrology {
|
|||||||
|
|
||||||
private final RiverCaveAction action;
|
private final RiverCaveAction action;
|
||||||
private final String floodedBiomeKey;
|
private final String floodedBiomeKey;
|
||||||
|
private final RiverCaveFluidKind fluidKind;
|
||||||
private final MatterCavern cavern;
|
private final MatterCavern cavern;
|
||||||
|
|
||||||
public RiverCaveHydrology(RiverCaveAction action, String floodedBiomeKey) {
|
public RiverCaveHydrology(
|
||||||
|
RiverCaveAction action,
|
||||||
|
String floodedBiomeKey,
|
||||||
|
RiverCaveFluidKind fluidKind
|
||||||
|
) {
|
||||||
this.action = Objects.requireNonNull(action);
|
this.action = Objects.requireNonNull(action);
|
||||||
this.floodedBiomeKey = floodedBiomeKey == null ? "" : floodedBiomeKey.trim();
|
this.floodedBiomeKey = floodedBiomeKey == null ? "" : floodedBiomeKey.trim();
|
||||||
|
this.fluidKind = Objects.requireNonNull(fluidKind);
|
||||||
this.cavern = switch (action) {
|
this.cavern = switch (action) {
|
||||||
case WET_SOURCE, FALLING_WATER -> new MatterCavern(true, this.floodedBiomeKey, LIQUID_FLUID);
|
case WET_SOURCE, FALLING_FLUID -> new MatterCavern(true, this.floodedBiomeKey, LIQUID_FLUID);
|
||||||
case DRY_AIR -> new MatterCavern(true, this.floodedBiomeKey, LIQUID_FORCED_AIR);
|
case DRY_AIR -> new MatterCavern(true, this.floodedBiomeKey, LIQUID_FORCED_AIR);
|
||||||
case SEAL_GUARD -> null;
|
case SEAL_GUARD -> null;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static RiverCaveHydrology of(RiverCaveAction action) {
|
public static RiverCaveHydrology of(RiverCaveAction action) {
|
||||||
return new RiverCaveHydrology(action, "");
|
return new RiverCaveHydrology(action, "", RiverCaveFluidKind.RIVER);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RiverCaveHydrology of(RiverCaveAction action, RiverCaveFluidKind fluidKind) {
|
||||||
|
return new RiverCaveHydrology(action, "", fluidKind);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<String> floodedBiome() {
|
public Optional<String> floodedBiome() {
|
||||||
@@ -36,11 +46,11 @@ public final class RiverCaveHydrology {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isWet() {
|
public boolean isWet() {
|
||||||
return action == RiverCaveAction.WET_SOURCE || action == RiverCaveAction.FALLING_WATER;
|
return action == RiverCaveAction.WET_SOURCE || action == RiverCaveAction.FALLING_FLUID;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isFalling() {
|
public boolean isFalling() {
|
||||||
return action == RiverCaveAction.FALLING_WATER;
|
return action == RiverCaveAction.FALLING_FLUID;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean protectsPlacement() {
|
public boolean protectsPlacement() {
|
||||||
@@ -59,6 +69,10 @@ public final class RiverCaveHydrology {
|
|||||||
return floodedBiomeKey;
|
return floodedBiomeKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RiverCaveFluidKind fluidKind() {
|
||||||
|
return fluidKind;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object object) {
|
public boolean equals(Object object) {
|
||||||
if (this == object) {
|
if (this == object) {
|
||||||
@@ -67,16 +81,19 @@ public final class RiverCaveHydrology {
|
|||||||
if (!(object instanceof RiverCaveHydrology hydrology)) {
|
if (!(object instanceof RiverCaveHydrology hydrology)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return action == hydrology.action && floodedBiomeKey.equals(hydrology.floodedBiomeKey);
|
return action == hydrology.action
|
||||||
|
&& floodedBiomeKey.equals(hydrology.floodedBiomeKey)
|
||||||
|
&& fluidKind == hydrology.fluidKind;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return (31 * action.hashCode()) + floodedBiomeKey.hashCode();
|
return (31 * ((31 * action.hashCode()) + floodedBiomeKey.hashCode())) + fluidKind.hashCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "RiverCaveHydrology[action=" + action + ", floodedBiomeKey=" + floodedBiomeKey + "]";
|
return "RiverCaveHydrology[action=" + action + ", floodedBiomeKey=" + floodedBiomeKey
|
||||||
|
+ ", fluidKind=" + fluidKind + "]";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,5 +4,6 @@ public enum RiverCaveMode {
|
|||||||
CLOSED_COMPONENT,
|
CLOSED_COMPONENT,
|
||||||
GENERATED_GROTTO,
|
GENERATED_GROTTO,
|
||||||
GROTTO_OR_CLOSED_COMPONENT,
|
GROTTO_OR_CLOSED_COMPONENT,
|
||||||
WATERFALL_POOL
|
WATERFALL_POOL,
|
||||||
|
DEEP_POOL
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import art.arcane.iris.engine.object.IrisRegion;
|
|||||||
import art.arcane.iris.engine.object.IrisRiverNetwork;
|
import art.arcane.iris.engine.object.IrisRiverNetwork;
|
||||||
import art.arcane.iris.engine.object.IrisRiverCaveMode;
|
import art.arcane.iris.engine.object.IrisRiverCaveMode;
|
||||||
import art.arcane.iris.engine.object.IrisRiverCaves;
|
import art.arcane.iris.engine.object.IrisRiverCaves;
|
||||||
|
import art.arcane.iris.engine.object.IrisRiverDeepPools;
|
||||||
import art.arcane.iris.engine.object.IrisRiverNoiseChance;
|
import art.arcane.iris.engine.object.IrisRiverNoiseChance;
|
||||||
import art.arcane.iris.engine.object.IrisRiverRoutingPolicy;
|
import art.arcane.iris.engine.object.IrisRiverRoutingPolicy;
|
||||||
import art.arcane.iris.engine.object.IrisRiverTerminalMode;
|
import art.arcane.iris.engine.object.IrisRiverTerminalMode;
|
||||||
@@ -41,6 +42,8 @@ import art.arcane.iris.util.project.noise.CNG;
|
|||||||
import art.arcane.iris.util.project.stream.ProceduralStream;
|
import art.arcane.iris.util.project.stream.ProceduralStream;
|
||||||
import art.arcane.volmlib.util.collection.KList;
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
import art.arcane.volmlib.util.math.RNG;
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
|
import com.github.benmanes.caffeine.cache.Cache;
|
||||||
|
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
@@ -48,6 +51,7 @@ import java.util.List;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.atomic.AtomicReferenceArray;
|
||||||
|
|
||||||
public final class IrisRiverRuntime implements AutoCloseable {
|
public final class IrisRiverRuntime implements AutoCloseable {
|
||||||
static final int MAXIMUM_REACH_FEASIBILITY_SAMPLES = 65;
|
static final int MAXIMUM_REACH_FEASIBILITY_SAMPLES = 65;
|
||||||
@@ -66,17 +70,22 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
|||||||
private static final long BIOME_NOISE_SALT = 0x2FFD72DBD01ADFB7L;
|
private static final long BIOME_NOISE_SALT = 0x2FFD72DBD01ADFB7L;
|
||||||
private static final long CAVE_ENTRY_NOISE_SALT = 0xB8E1AFED6A267E96L;
|
private static final long CAVE_ENTRY_NOISE_SALT = 0xB8E1AFED6A267E96L;
|
||||||
private static final long CAVE_ENTRY_GATE_SALT = 0xBA7C9045F12C7F99L;
|
private static final long CAVE_ENTRY_GATE_SALT = 0xBA7C9045F12C7F99L;
|
||||||
|
private static final long DEEP_POOL_REACH_NOISE_SALT = 0x7137449123EF65CDL;
|
||||||
|
private static final long DEEP_POOL_REACH_GATE_SALT = 0xE9B5DBA58189DBBCL;
|
||||||
private static final long TUNNEL_FLOOR_NOISE_SALT = 0x8CB92BA72F3D8DD7L;
|
private static final long TUNNEL_FLOOR_NOISE_SALT = 0x8CB92BA72F3D8DD7L;
|
||||||
private static final long TUNNEL_ROOF_NOISE_SALT = 0xDB4F0B9175AE2165L;
|
private static final long TUNNEL_ROOF_NOISE_SALT = 0xDB4F0B9175AE2165L;
|
||||||
private static final long TUNNEL_WIDTH_NOISE_SALT = 0xC6EF372FE94F82BEL;
|
private static final long TUNNEL_WIDTH_NOISE_SALT = 0xC6EF372FE94F82BEL;
|
||||||
private static final long FLOODED_CAVE_BIOME_SALT = 0x24A19947B3916CF7L;
|
private static final long FLOODED_CAVE_BIOME_SALT = 0x24A19947B3916CF7L;
|
||||||
private static final long TERMINAL_CAVE_ANCHOR_SALT = 0x9E3779B97F4A7C15L;
|
private static final long TERMINAL_CAVE_ANCHOR_SALT = 0x9E3779B97F4A7C15L;
|
||||||
private static final int TILE_CACHE_SIZE = 32;
|
private static final int TILE_CACHE_SIZE = 32;
|
||||||
|
private static final int TUNNEL_SAMPLE_CHUNK_CACHE_SIZE = 4_096;
|
||||||
|
private static final Object ABSENT_TUNNEL_SAMPLE = new Object();
|
||||||
|
|
||||||
private final long seed;
|
private final long seed;
|
||||||
private final IrisRiverNetwork configuration;
|
private final IrisRiverNetwork configuration;
|
||||||
private final IrisData data;
|
private final IrisData data;
|
||||||
private final int fluidHeight;
|
private final int riverFluidHeight;
|
||||||
|
private final int dimensionFluidHeight;
|
||||||
private final boolean boreMantleActive;
|
private final boolean boreMantleActive;
|
||||||
private final boolean caveHydrologyActive;
|
private final boolean caveHydrologyActive;
|
||||||
private final boolean blockingRoutingPossible;
|
private final boolean blockingRoutingPossible;
|
||||||
@@ -101,12 +110,14 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
|||||||
private final CNG bedNoise;
|
private final CNG bedNoise;
|
||||||
private final CNG biomeNoise;
|
private final CNG biomeNoise;
|
||||||
private final CNG caveEntryNoise;
|
private final CNG caveEntryNoise;
|
||||||
|
private final CNG deepPoolReachNoise;
|
||||||
private final CNG tunnelFloorNoise;
|
private final CNG tunnelFloorNoise;
|
||||||
private final CNG tunnelRoofNoise;
|
private final CNG tunnelRoofNoise;
|
||||||
private final CNG tunnelWidthNoise;
|
private final CNG tunnelWidthNoise;
|
||||||
private final art.arcane.iris.engine.river.RiverNetwork network;
|
private final art.arcane.iris.engine.river.RiverNetwork network;
|
||||||
private final RuntimeTerrainSampler terrainSampler;
|
private final RuntimeTerrainSampler terrainSampler;
|
||||||
private final RiverTileCache tileCache;
|
private final RiverTileCache tileCache;
|
||||||
|
private final Cache<Long, TunnelSampleChunk> tunnelSampleCache;
|
||||||
private final ConcurrentHashMap<IdentitySettingsKey, EffectiveRiverSettings> settingsCache;
|
private final ConcurrentHashMap<IdentitySettingsKey, EffectiveRiverSettings> settingsCache;
|
||||||
private final ConcurrentHashMap<BiomePoolKey, List<IrisBiome>> biomePoolCache;
|
private final ConcurrentHashMap<BiomePoolKey, List<IrisBiome>> biomePoolCache;
|
||||||
|
|
||||||
@@ -115,7 +126,8 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
|||||||
seed = context.seed();
|
seed = context.seed();
|
||||||
configuration = context.configuration();
|
configuration = context.configuration();
|
||||||
data = context.data();
|
data = context.data();
|
||||||
fluidHeight = context.fluidHeight();
|
riverFluidHeight = context.riverFluidHeight();
|
||||||
|
dimensionFluidHeight = context.dimensionFluidHeight();
|
||||||
boreMantleActive = context.boreMantleActive();
|
boreMantleActive = context.boreMantleActive();
|
||||||
caveHydrologyActive = context.caveHydrologyActive();
|
caveHydrologyActive = context.caveHydrologyActive();
|
||||||
blockingRoutingPossible = context.blockingRoutingPossible();
|
blockingRoutingPossible = context.blockingRoutingPossible();
|
||||||
@@ -146,6 +158,11 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
|||||||
bedNoise = noise(terrain.getBedRoughnessStyle(), BED_NOISE_SALT);
|
bedNoise = noise(terrain.getBedRoughnessStyle(), BED_NOISE_SALT);
|
||||||
biomeNoise = noise(configuration.getBiomes().getSelectionStyle(), BIOME_NOISE_SALT);
|
biomeNoise = noise(configuration.getBiomes().getSelectionStyle(), BIOME_NOISE_SALT);
|
||||||
caveEntryNoise = noise(caves.getEntry(), CAVE_ENTRY_NOISE_SALT);
|
caveEntryNoise = noise(caves.getEntry(), CAVE_ENTRY_NOISE_SALT);
|
||||||
|
IrisRiverDeepPools deepPools = caves.getDeepPools();
|
||||||
|
deepPoolReachNoise = noise(
|
||||||
|
deepPools == null ? null : deepPools.getReach(),
|
||||||
|
DEEP_POOL_REACH_NOISE_SALT
|
||||||
|
);
|
||||||
tunnelFloorNoise = noise(terrain.getTunnelFloorStyle(), TUNNEL_FLOOR_NOISE_SALT);
|
tunnelFloorNoise = noise(terrain.getTunnelFloorStyle(), TUNNEL_FLOOR_NOISE_SALT);
|
||||||
tunnelRoofNoise = noise(terrain.getTunnelRoofStyle(), TUNNEL_ROOF_NOISE_SALT);
|
tunnelRoofNoise = noise(terrain.getTunnelRoofStyle(), TUNNEL_ROOF_NOISE_SALT);
|
||||||
tunnelWidthNoise = noise(terrain.getTunnelWidthMultiplier(), TUNNEL_WIDTH_NOISE_SALT);
|
tunnelWidthNoise = noise(terrain.getTunnelWidthMultiplier(), TUNNEL_WIDTH_NOISE_SALT);
|
||||||
@@ -158,12 +175,15 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
|||||||
TILE_CACHE_SIZE,
|
TILE_CACHE_SIZE,
|
||||||
(tileX, tileZ) -> network.buildTile(tileX, tileZ, terrainSampler)
|
(tileX, tileZ) -> network.buildTile(tileX, tileZ, terrainSampler)
|
||||||
);
|
);
|
||||||
|
tunnelSampleCache = Caffeine.newBuilder()
|
||||||
|
.maximumSize(TUNNEL_SAMPLE_CHUNK_CACHE_SIZE)
|
||||||
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public IrisRiverSurfaceSample sample(double x, double z) {
|
public IrisRiverSurfaceSample sample(double x, double z) {
|
||||||
ResolvedRiverColumn column = resolveColumn(x, z);
|
ResolvedRiverColumn column = resolveColumn(x, z);
|
||||||
if (column == null) {
|
if (column == null) {
|
||||||
return IrisRiverSurfaceSample.none(naturalHeight.get(x, z), fluidHeight);
|
return IrisRiverSurfaceSample.none(naturalHeight.get(x, z), dimensionFluidHeight);
|
||||||
}
|
}
|
||||||
if (column.subterranean()) {
|
if (column.subterranean()) {
|
||||||
return new IrisRiverSurfaceSample(
|
return new IrisRiverSurfaceSample(
|
||||||
@@ -229,9 +249,8 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
|||||||
if (!column.subterranean()) {
|
if (!column.subterranean()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (isTunnelMouth(column, mouthBlend)) {
|
double mouthFactor = tunnelMouthFactor(column, mouthBlend);
|
||||||
channelRadius += mouthBlend;
|
channelRadius += mouthBlend * mouthFactor;
|
||||||
}
|
|
||||||
if (column.river().distance() > channelRadius) {
|
if (column.river().distance() > channelRadius) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -255,13 +274,20 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
|||||||
);
|
);
|
||||||
int ceilingY = shapedTunnelCeilingY(
|
int ceilingY = shapedTunnelCeilingY(
|
||||||
waterHeadY,
|
waterHeadY,
|
||||||
caves.getDryHeadroom() * column.reach().roofScaleAt(column.river().alongReach()),
|
caves.getDryHeadroom() * column.reach().roofScaleAt(column.river().alongReach())
|
||||||
|
+ mouthBlend * mouthFactor,
|
||||||
profile,
|
profile,
|
||||||
roofOffset
|
roofOffset
|
||||||
);
|
);
|
||||||
return new IrisRiverTunnelSample(column.river(), bedY, waterHeadY, ceilingY);
|
return new IrisRiverTunnelSample(column.river(), bedY, waterHeadY, ceilingY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IrisRiverTunnelSample sampleTunnel(int x, int z) {
|
||||||
|
long chunkKey = ((long) (x >> 4) << 32) ^ ((z >> 4) & 0xFFFFFFFFL);
|
||||||
|
TunnelSampleChunk chunk = tunnelSampleCache.get(chunkKey, ignored -> new TunnelSampleChunk());
|
||||||
|
return chunk.sample(this, x, z);
|
||||||
|
}
|
||||||
|
|
||||||
static int shapedTunnelBedY(
|
static int shapedTunnelBedY(
|
||||||
int waterHeadY,
|
int waterHeadY,
|
||||||
double baseBedY,
|
double baseBedY,
|
||||||
@@ -331,18 +357,44 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isTunnelMouth(ResolvedRiverColumn column, double mouthBlend) {
|
private double tunnelMouthFactor(ResolvedRiverColumn column, double mouthBlend) {
|
||||||
if (mouthBlend <= 0D) {
|
if (mouthBlend <= 0D) {
|
||||||
return false;
|
return 0D;
|
||||||
}
|
}
|
||||||
double length = column.reach().polyline().length();
|
double length = column.reach().polyline().length();
|
||||||
if (length <= 0D) {
|
if (length <= 0D) {
|
||||||
return false;
|
return 0D;
|
||||||
}
|
}
|
||||||
double offset = mouthBlend / length;
|
double offset = mouthBlend / length;
|
||||||
double alongReach = column.river().alongReach();
|
double alongReach = column.river().alongReach();
|
||||||
return !isCenterlineSubterranean(column.reach(), clamp01(alongReach - offset))
|
return Math.max(
|
||||||
|| !isCenterlineSubterranean(column.reach(), clamp01(alongReach + offset));
|
tunnelMouthFactor(column.reach(), alongReach, clamp01(alongReach - offset), mouthBlend),
|
||||||
|
tunnelMouthFactor(column.reach(), alongReach, clamp01(alongReach + offset), mouthBlend)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private double tunnelMouthFactor(
|
||||||
|
RiverReach reach,
|
||||||
|
double subterraneanAlong,
|
||||||
|
double candidateOpenAlong,
|
||||||
|
double mouthBlend
|
||||||
|
) {
|
||||||
|
if (candidateOpenAlong == subterraneanAlong
|
||||||
|
|| isCenterlineSubterranean(reach, candidateOpenAlong)) {
|
||||||
|
return 0D;
|
||||||
|
}
|
||||||
|
double openAlong = candidateOpenAlong;
|
||||||
|
double solidAlong = subterraneanAlong;
|
||||||
|
for (int iteration = 0; iteration < 5; iteration++) {
|
||||||
|
double midpoint = (openAlong + solidAlong) * 0.5D;
|
||||||
|
if (isCenterlineSubterranean(reach, midpoint)) {
|
||||||
|
solidAlong = midpoint;
|
||||||
|
} else {
|
||||||
|
openAlong = midpoint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
double distance = StrictMath.abs(solidAlong - subterraneanAlong) * reach.polyline().length();
|
||||||
|
return clamp01(1D - distance / mouthBlend);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isCenterlineSubterranean(RiverReach reach, double alongReach) {
|
private boolean isCenterlineSubterranean(RiverReach reach, double alongReach) {
|
||||||
@@ -401,6 +453,35 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
|||||||
return tileAt(centerX, centerZ).sampleFootprint(minimumX, minimumZ, maximumX, maximumZ);
|
return tileAt(centerX, centerZ).sampleFootprint(minimumX, minimumZ, maximumX, maximumZ);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean hasRiverFootprint(
|
||||||
|
int minimumX,
|
||||||
|
int minimumZ,
|
||||||
|
int maximumX,
|
||||||
|
int maximumZ
|
||||||
|
) {
|
||||||
|
if (minimumX >= maximumX || minimumZ >= maximumZ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int minimumTileX = network.tileXForBlock(minimumX);
|
||||||
|
int minimumTileZ = network.tileZForBlock(minimumZ);
|
||||||
|
int maximumTileX = network.tileXForBlock(maximumX - 1);
|
||||||
|
int maximumTileZ = network.tileZForBlock(maximumZ - 1);
|
||||||
|
for (int tileX = minimumTileX; tileX <= maximumTileX; tileX++) {
|
||||||
|
for (int tileZ = minimumTileZ; tileZ <= maximumTileZ; tileZ++) {
|
||||||
|
RiverSample sample = tileCache.get(tileX, tileZ).sampleFootprint(
|
||||||
|
minimumX,
|
||||||
|
minimumZ,
|
||||||
|
maximumX,
|
||||||
|
maximumZ
|
||||||
|
);
|
||||||
|
if (sample.present()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public List<RiverAnchor> candidateAnchors(
|
public List<RiverAnchor> candidateAnchors(
|
||||||
int minimumX,
|
int minimumX,
|
||||||
int minimumZ,
|
int minimumZ,
|
||||||
@@ -481,7 +562,9 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public int maximumTunnelHeadroom() {
|
public int maximumTunnelHeadroom() {
|
||||||
return caves.getDryHeadroom() + (int) StrictMath.ceil(terrain.getTunnelRoofVariation());
|
return caves.getDryHeadroom()
|
||||||
|
+ (int) StrictMath.ceil(terrain.getTunnelRoofVariation())
|
||||||
|
+ (int) StrictMath.ceil(terrain.getTunnelMouthBlend());
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean acceptsCaveAnchor(RiverAnchor anchor) {
|
public boolean acceptsCaveAnchor(RiverAnchor anchor) {
|
||||||
@@ -535,6 +618,52 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean acceptsDeepPoolAnchor(RiverAnchor anchor) {
|
||||||
|
Objects.requireNonNull(anchor);
|
||||||
|
IrisRiverDeepPools deepPools = caves.getDeepPools();
|
||||||
|
if (deepPools == null
|
||||||
|
|| !deepPools.isEnabled()
|
||||||
|
|| deepPools.getMaximumPerReach() <= 0
|
||||||
|
|| anchor.state() != RiverRouteState.WET
|
||||||
|
|| !caveHydrologyActive) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
RiverReach reach = tileAt(anchor.x(), anchor.z()).reach(anchor.reachId());
|
||||||
|
if (reach == null || reach.state() != RiverRouteState.WET) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!deepPoolReachEligible(deepPools, reach)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
TerminalCaveAnchor terminal = terminalCaveAnchor(reach);
|
||||||
|
if (terminal != null && anchor.stableId() == terminal.stableId()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
double firstDistance = unit(art.arcane.iris.engine.river.RiverNetwork.mix(
|
||||||
|
reach.id().stableId() ^ anchor.samplingSalt()
|
||||||
|
)) * anchor.samplingSpacing();
|
||||||
|
double anchorDistance = firstDistance + anchor.index() * anchor.samplingSpacing();
|
||||||
|
if (anchorDistance >= reach.polyline().length()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int accepted = 0;
|
||||||
|
for (int index = 0; index <= anchor.index(); index++) {
|
||||||
|
long stableId = art.arcane.iris.engine.river.RiverNetwork.mix(
|
||||||
|
reach.id().stableId()
|
||||||
|
^ anchor.samplingSalt()
|
||||||
|
^ (long) index * 0x9E3779B97F4A7C15L
|
||||||
|
);
|
||||||
|
if (index == anchor.index()) {
|
||||||
|
return stableId == anchor.stableId() && accepted < deepPools.getMaximumPerReach();
|
||||||
|
}
|
||||||
|
accepted++;
|
||||||
|
if (accepted >= deepPools.getMaximumPerReach()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isCaveAnchorSourceable(double x, double z) {
|
private boolean isCaveAnchorSourceable(double x, double z) {
|
||||||
IrisRiverSurfaceSample surface = sample(x, z);
|
IrisRiverSurfaceSample surface = sample(x, z);
|
||||||
if (surface.river().present()
|
if (surface.river().present()
|
||||||
@@ -603,6 +732,7 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
|||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
tileCache.close();
|
tileCache.close();
|
||||||
|
tunnelSampleCache.invalidateAll();
|
||||||
settingsCache.clear();
|
settingsCache.clear();
|
||||||
biomePoolCache.clear();
|
biomePoolCache.clear();
|
||||||
}
|
}
|
||||||
@@ -620,7 +750,7 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
|||||||
.routingDeviationScaleCells(topology.getRoutingDeviationScaleCells())
|
.routingDeviationScaleCells(topology.getRoutingDeviationScaleCells())
|
||||||
.routingDeviationStrengthCells(topology.getRoutingDeviationStrengthCells())
|
.routingDeviationStrengthCells(topology.getRoutingDeviationStrengthCells())
|
||||||
.routingPlateauHeight(topology.getRoutingPlateauHeight())
|
.routingPlateauHeight(topology.getRoutingPlateauHeight())
|
||||||
.hydraulicBaseHeight(fluidHeight)
|
.hydraulicBaseHeight(riverFluidHeight)
|
||||||
.requireOcean(topology.isRequireOcean())
|
.requireOcean(topology.isRequireOcean())
|
||||||
.sourceChance(chance(topology.getSource()))
|
.sourceChance(chance(topology.getSource()))
|
||||||
.reachChance(chance(topology.getContinuation()))
|
.reachChance(chance(topology.getContinuation()))
|
||||||
@@ -633,6 +763,7 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
|||||||
.channelWidth(mid(riverTerrain.getChannelWidth(), 12D))
|
.channelWidth(mid(riverTerrain.getChannelWidth(), 12D))
|
||||||
.bankWidth(mid(riverTerrain.getBankWidth(), 8D))
|
.bankWidth(mid(riverTerrain.getBankWidth(), 8D))
|
||||||
.depth(mid(riverTerrain.getDepth(), 4D))
|
.depth(mid(riverTerrain.getDepth(), 4D))
|
||||||
|
.channelRadiusBonus(riverTerrain.getChannelRadiusBonus())
|
||||||
.maxChannelWidth(riverTerrain.getMaxChannelWidth())
|
.maxChannelWidth(riverTerrain.getMaxChannelWidth())
|
||||||
.maxBankWidth(riverTerrain.getMaxBankWidth())
|
.maxBankWidth(riverTerrain.getMaxBankWidth())
|
||||||
.maxDepth(riverTerrain.getMaxDepth())
|
.maxDepth(riverTerrain.getMaxDepth())
|
||||||
@@ -683,6 +814,7 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
|||||||
configured.getDepthMultiplier(),
|
configured.getDepthMultiplier(),
|
||||||
configured.getBodyWavelength(),
|
configured.getBodyWavelength(),
|
||||||
configured.getBodyDetailWavelength(),
|
configured.getBodyDetailWavelength(),
|
||||||
|
configured.getBodyDetailInfluence(),
|
||||||
configured.getWidthVariation(),
|
configured.getWidthVariation(),
|
||||||
configured.getBankVariation(),
|
configured.getBankVariation(),
|
||||||
configured.getDepthVariation(),
|
configured.getDepthVariation(),
|
||||||
@@ -757,6 +889,24 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
|||||||
return unit(hash) < chance;
|
return unit(hash) < chance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean deepPoolReachEligible(
|
||||||
|
IrisRiverDeepPools deepPools,
|
||||||
|
RiverReach reach
|
||||||
|
) {
|
||||||
|
CenterlinePosition center = centerlinePosition(reach, 0.5D);
|
||||||
|
EffectiveRiverSettings settings = settingsAt(center.x(), center.z());
|
||||||
|
double chance = clamp01(effectiveChance(
|
||||||
|
deepPools.getReach(),
|
||||||
|
deepPoolReachNoise,
|
||||||
|
(int) StrictMath.floor(center.x()),
|
||||||
|
(int) StrictMath.floor(center.z())
|
||||||
|
) * settings.caveEntryMultiplier());
|
||||||
|
long hash = art.arcane.iris.engine.river.RiverNetwork.mix(
|
||||||
|
seed ^ reach.id().stableId() ^ DEEP_POOL_REACH_GATE_SALT
|
||||||
|
);
|
||||||
|
return unit(hash) < chance;
|
||||||
|
}
|
||||||
|
|
||||||
private TerminalCaveAnchor terminalCaveAnchor(RiverReach reach) {
|
private TerminalCaveAnchor terminalCaveAnchor(RiverReach reach) {
|
||||||
if (!caveHydrologyActive || !reach.terminal() || reach.state() != RiverRouteState.WET) {
|
if (!caveHydrologyActive || !reach.terminal() || reach.state() != RiverRouteState.WET) {
|
||||||
return null;
|
return null;
|
||||||
@@ -809,8 +959,11 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
double waterSurface(RiverReach reach, double alongReach, boolean naturalOcean) {
|
double waterSurface(RiverReach reach, double alongReach, boolean naturalOcean) {
|
||||||
if (reach == null || water.getMode() == IrisRiverWaterMode.SEA_LEVEL || naturalOcean) {
|
if (naturalOcean) {
|
||||||
return fluidHeight;
|
return dimensionFluidHeight;
|
||||||
|
}
|
||||||
|
if (reach == null || water.getMode() == IrisRiverWaterMode.FIXED) {
|
||||||
|
return riverFluidHeight;
|
||||||
}
|
}
|
||||||
return terracedWaterSurface(
|
return terracedWaterSurface(
|
||||||
reach.from().hydraulicHeight(),
|
reach.from().hydraulicHeight(),
|
||||||
@@ -883,10 +1036,10 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
|||||||
|
|
||||||
private int nodeWaterHead(double naturalNodeHeight, int dropHeight) {
|
private int nodeWaterHead(double naturalNodeHeight, int dropHeight) {
|
||||||
int availableRise = Math.max(0, water.getMaximumPoolRise());
|
int availableRise = Math.max(0, water.getMaximumPoolRise());
|
||||||
int maximumHead = fluidHeight + availableRise;
|
int maximumHead = riverFluidHeight + availableRise;
|
||||||
int naturalHead = (int) StrictMath.floor(naturalNodeHeight - 1D);
|
int naturalHead = (int) StrictMath.floor(naturalNodeHeight - 1D);
|
||||||
int clamped = Math.max(fluidHeight, Math.min(maximumHead, naturalHead));
|
int clamped = Math.max(riverFluidHeight, Math.min(maximumHead, naturalHead));
|
||||||
return fluidHeight + Math.floorDiv(clamped - fluidHeight, dropHeight) * dropHeight;
|
return riverFluidHeight + Math.floorDiv(clamped - riverFluidHeight, dropHeight) * dropHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isNaturalOcean(IrisBiome biome) {
|
private static boolean isNaturalOcean(IrisBiome biome) {
|
||||||
@@ -1011,6 +1164,25 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
|||||||
return (hash >>> 11) * 0x1.0p-53;
|
return (hash >>> 11) * 0x1.0p-53;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final class TunnelSampleChunk {
|
||||||
|
private final AtomicReferenceArray<Object> samples = new AtomicReferenceArray<>(256);
|
||||||
|
|
||||||
|
private IrisRiverTunnelSample sample(IrisRiverRuntime runtime, int x, int z) {
|
||||||
|
int index = ((x & 15) << 4) | (z & 15);
|
||||||
|
Object cached = samples.get(index);
|
||||||
|
if (cached == null) {
|
||||||
|
IrisRiverTunnelSample computed = runtime.sampleTunnel((double) x, (double) z);
|
||||||
|
Object encoded = computed == null ? ABSENT_TUNNEL_SAMPLE : computed;
|
||||||
|
if (samples.compareAndSet(index, null, encoded)) {
|
||||||
|
cached = encoded;
|
||||||
|
} else {
|
||||||
|
cached = samples.get(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cached == ABSENT_TUNNEL_SAMPLE ? null : (IrisRiverTunnelSample) cached;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private final class RuntimeTerrainSampler implements RiverTerrainSampler {
|
private final class RuntimeTerrainSampler implements RiverTerrainSampler {
|
||||||
private final IrisRiverTopology topology;
|
private final IrisRiverTopology topology;
|
||||||
|
|
||||||
@@ -1022,11 +1194,11 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
|||||||
public RiverTerrainNodeSample sampleNode(int blockX, int blockZ) {
|
public RiverTerrainNodeSample sampleNode(int blockX, int blockZ) {
|
||||||
boolean oceanIntent = Boolean.TRUE.equals(naturalOcean.get(blockX, blockZ));
|
boolean oceanIntent = Boolean.TRUE.equals(naturalOcean.get(blockX, blockZ));
|
||||||
boolean naturalHeightRequired = topology.getTerrainHeightWeight() > 0D
|
boolean naturalHeightRequired = topology.getTerrainHeightWeight() > 0D
|
||||||
|| water.getMode() != IrisRiverWaterMode.SEA_LEVEL
|
|| water.getMode() != IrisRiverWaterMode.FIXED
|
||||||
|| oceanIntent;
|
|| oceanIntent;
|
||||||
double sampledNaturalHeight = naturalHeightRequired
|
double sampledNaturalHeight = naturalHeightRequired
|
||||||
? naturalHeight.get(blockX, blockZ)
|
? naturalHeight.get(blockX, blockZ)
|
||||||
: fluidHeight;
|
: dimensionFluidHeight;
|
||||||
boolean sampledOcean = oceanIntent && isSubmergedOutlet(sampledNaturalHeight);
|
boolean sampledOcean = oceanIntent && isSubmergedOutlet(sampledNaturalHeight);
|
||||||
IrisRegion sampledRegion = region.get(blockX, blockZ);
|
IrisRegion sampledRegion = region.get(blockX, blockZ);
|
||||||
IrisBiome sampledBiome = biomeRiverOverridesPossible ? naturalBiome.get(blockX, blockZ) : null;
|
IrisBiome sampledBiome = biomeRiverOverridesPossible ? naturalBiome.get(blockX, blockZ) : null;
|
||||||
@@ -1068,7 +1240,7 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean isSubmergedOutlet(double sampledNaturalHeight) {
|
private boolean isSubmergedOutlet(double sampledNaturalHeight) {
|
||||||
return Math.round(sampledNaturalHeight) < Math.round(fluidHeight);
|
return Math.round(sampledNaturalHeight) < Math.round(dimensionFluidHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -1150,8 +1322,8 @@ public final class IrisRiverRuntime implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
maximumIncision *= settings.maxIncisionMultiplier();
|
maximumIncision *= settings.maxIncisionMultiplier();
|
||||||
}
|
}
|
||||||
double head = configuration.getWater().getMode() == IrisRiverWaterMode.SEA_LEVEL
|
double head = configuration.getWater().getMode() == IrisRiverWaterMode.FIXED
|
||||||
? fluidHeight
|
? riverFluidHeight
|
||||||
: terracedWaterSurface(
|
: terracedWaterSurface(
|
||||||
context.from().hydraulicHeight(),
|
context.from().hydraulicHeight(),
|
||||||
context.to().hydraulicHeight(),
|
context.to().hydraulicHeight(),
|
||||||
|
|||||||
+2
-1
@@ -12,7 +12,8 @@ public record IrisRiverRuntimeContext(
|
|||||||
long seed,
|
long seed,
|
||||||
IrisRiverNetwork configuration,
|
IrisRiverNetwork configuration,
|
||||||
IrisData data,
|
IrisData data,
|
||||||
int fluidHeight,
|
int riverFluidHeight,
|
||||||
|
int dimensionFluidHeight,
|
||||||
boolean boreMantleActive,
|
boolean boreMantleActive,
|
||||||
boolean caveHydrologyActive,
|
boolean caveHydrologyActive,
|
||||||
boolean blockingRoutingPossible,
|
boolean blockingRoutingPossible,
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ import org.bukkit.entity.EntityType;
|
|||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.event.Listener;
|
import org.bukkit.event.Listener;
|
||||||
import org.bukkit.event.entity.CreatureSpawnEvent;
|
import org.bukkit.event.entity.CreatureSpawnEvent;
|
||||||
|
import org.bukkit.event.player.PlayerTeleportEvent;
|
||||||
import org.bukkit.inventory.ItemStack;
|
import org.bukkit.inventory.ItemStack;
|
||||||
import org.bukkit.plugin.Plugin;
|
import org.bukkit.plugin.Plugin;
|
||||||
import org.bukkit.util.Vector;
|
import org.bukkit.util.Vector;
|
||||||
@@ -236,6 +237,11 @@ public final class BukkitPlatform implements IrisPlatform {
|
|||||||
return io.papermc.lib.PaperLib.teleportAsync(entity, destination);
|
return io.papermc.lib.PaperLib.teleportAsync(entity, destination);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static java.util.concurrent.CompletableFuture<Boolean> teleportAsync(Entity entity, Location destination,
|
||||||
|
PlayerTeleportEvent.TeleportCause cause) {
|
||||||
|
return io.papermc.lib.PaperLib.teleportAsync(entity, destination, cause);
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean isPaperServer() {
|
public static boolean isPaperServer() {
|
||||||
return io.papermc.lib.PaperLib.isPaper();
|
return io.papermc.lib.PaperLib.isPaper();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,12 +19,55 @@ public class SlimJar {
|
|||||||
private static final ReentrantLock lock = new ReentrantLock();
|
private static final ReentrantLock lock = new ReentrantLock();
|
||||||
private static final AtomicBoolean loaded = new AtomicBoolean();
|
private static final AtomicBoolean loaded = new AtomicBoolean();
|
||||||
|
|
||||||
|
public static void loadBootstrap(Path downloadPath, BootstrapLogger logger) {
|
||||||
|
if (loaded.get()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lock.lock();
|
||||||
|
try {
|
||||||
|
if (loaded.get()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ApplicationBuilder.appending("Iris")
|
||||||
|
.injectableFactory(InjectableFactory.selecting(
|
||||||
|
InjectableFactory.ERROR,
|
||||||
|
InjectableFactory.INJECTABLE,
|
||||||
|
InjectableFactory.WRAPPED,
|
||||||
|
InjectableFactory.UNSAFE))
|
||||||
|
.downloadDirectoryPath(downloadPath)
|
||||||
|
.logger(new ProcessLogger() {
|
||||||
|
@Override
|
||||||
|
public void info(@NotNull String message, @Nullable Object... args) {
|
||||||
|
logger.info(message.formatted(args));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void error(@NotNull String message, @Nullable Object... args) {
|
||||||
|
logger.error(message.formatted(args));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void debug(@NotNull String message, @Nullable Object... args) {
|
||||||
|
logger.debug(message.formatted(args));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
loaded.set(true);
|
||||||
|
} finally {
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void load() {
|
public static void load() {
|
||||||
if (loaded.get()) return;
|
if (loaded.get()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
lock.lock();
|
lock.lock();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (loaded.getAndSet(true)) return;
|
if (loaded.get()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
VolmitPlugin plugin = BukkitPlatform.volmitPlugin();
|
VolmitPlugin plugin = BukkitPlatform.volmitPlugin();
|
||||||
Path downloadPath = plugin.getDataFolder("cache", "libraries").toPath();
|
Path downloadPath = plugin.getDataFolder("cache", "libraries").toPath();
|
||||||
debug(plugin, "Loading libraries...");
|
debug(plugin, "Loading libraries...");
|
||||||
@@ -58,6 +101,7 @@ public class SlimJar {
|
|||||||
})
|
})
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
loaded.set(true);
|
||||||
debug(plugin, "Libraries loaded successfully!");
|
debug(plugin, "Libraries loaded successfully!");
|
||||||
} finally {
|
} finally {
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
@@ -69,4 +113,12 @@ public class SlimJar {
|
|||||||
plugin.getLogger().info("[DEBUG] " + message);
|
plugin.getLogger().info("[DEBUG] " + message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public interface BootstrapLogger {
|
||||||
|
void info(String message);
|
||||||
|
|
||||||
|
void error(String message);
|
||||||
|
|
||||||
|
void debug(String message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -303,6 +303,43 @@ public class J {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static CompletableFuture<Void> runRegionFuture(
|
||||||
|
World world,
|
||||||
|
int chunkX,
|
||||||
|
int chunkZ,
|
||||||
|
Runnable runnable
|
||||||
|
) {
|
||||||
|
if (world == null || runnable == null) {
|
||||||
|
return CompletableFuture.failedFuture(new IllegalArgumentException(
|
||||||
|
"Region task world and runnable are required."));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFolia()) {
|
||||||
|
CompletableFuture<Void> future = new CompletableFuture<>();
|
||||||
|
if (isOwnedByCurrentRegion(world, chunkX, chunkZ)) {
|
||||||
|
settle(future, runnable);
|
||||||
|
return future;
|
||||||
|
}
|
||||||
|
if (!runRegionImmediate(
|
||||||
|
world,
|
||||||
|
chunkX,
|
||||||
|
chunkZ,
|
||||||
|
() -> settle(future, runnable))) {
|
||||||
|
future.completeExceptionally(new IllegalStateException(
|
||||||
|
"Failed to schedule region task for " + world.getName()
|
||||||
|
+ "@" + chunkX + "," + chunkZ + "."));
|
||||||
|
}
|
||||||
|
return future;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPrimaryThread()) {
|
||||||
|
CompletableFuture<Void> future = new CompletableFuture<>();
|
||||||
|
settle(future, runnable);
|
||||||
|
return future;
|
||||||
|
}
|
||||||
|
return sfut(runnable);
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean runGlobal(Runnable runnable) {
|
public static boolean runGlobal(Runnable runnable) {
|
||||||
if (runnable == null) {
|
if (runnable == null) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
+21
-3
@@ -1,6 +1,7 @@
|
|||||||
package art.arcane.iris.util.project.matter.slices;
|
package art.arcane.iris.util.project.matter.slices;
|
||||||
|
|
||||||
import art.arcane.iris.engine.river.cave.RiverCaveAction;
|
import art.arcane.iris.engine.river.cave.RiverCaveAction;
|
||||||
|
import art.arcane.iris.engine.river.cave.RiverCaveFluidKind;
|
||||||
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
|
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
|
||||||
import art.arcane.volmlib.util.data.palette.Palette;
|
import art.arcane.volmlib.util.data.palette.Palette;
|
||||||
import art.arcane.volmlib.util.matter.Sliced;
|
import art.arcane.volmlib.util.matter.Sliced;
|
||||||
@@ -28,19 +29,21 @@ public final class RiverCaveHydrologyMatter extends RawMatter<RiverCaveHydrology
|
|||||||
@Override
|
@Override
|
||||||
public void writeNode(RiverCaveHydrology hydrology, DataOutputStream output) throws IOException {
|
public void writeNode(RiverCaveHydrology hydrology, DataOutputStream output) throws IOException {
|
||||||
output.writeByte(actionCode(hydrology.action()));
|
output.writeByte(actionCode(hydrology.action()));
|
||||||
|
output.writeByte(fluidKindCode(hydrology.fluidKind()));
|
||||||
output.writeUTF(hydrology.floodedBiomeKey());
|
output.writeUTF(hydrology.floodedBiomeKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RiverCaveHydrology readNode(DataInputStream input) throws IOException {
|
public RiverCaveHydrology readNode(DataInputStream input) throws IOException {
|
||||||
RiverCaveAction action = actionFromCode(input.readUnsignedByte());
|
RiverCaveAction action = actionFromCode(input.readUnsignedByte());
|
||||||
return new RiverCaveHydrology(action, input.readUTF());
|
RiverCaveFluidKind fluidKind = fluidKindFromCode(input.readUnsignedByte());
|
||||||
|
return new RiverCaveHydrology(action, input.readUTF(), fluidKind);
|
||||||
}
|
}
|
||||||
|
|
||||||
private int actionCode(RiverCaveAction action) {
|
private int actionCode(RiverCaveAction action) {
|
||||||
return switch (action) {
|
return switch (action) {
|
||||||
case WET_SOURCE -> 1;
|
case WET_SOURCE -> 1;
|
||||||
case FALLING_WATER -> 2;
|
case FALLING_FLUID -> 2;
|
||||||
case DRY_AIR -> 3;
|
case DRY_AIR -> 3;
|
||||||
case SEAL_GUARD -> 4;
|
case SEAL_GUARD -> 4;
|
||||||
};
|
};
|
||||||
@@ -49,10 +52,25 @@ public final class RiverCaveHydrologyMatter extends RawMatter<RiverCaveHydrology
|
|||||||
private RiverCaveAction actionFromCode(int code) throws IOException {
|
private RiverCaveAction actionFromCode(int code) throws IOException {
|
||||||
return switch (code) {
|
return switch (code) {
|
||||||
case 1 -> RiverCaveAction.WET_SOURCE;
|
case 1 -> RiverCaveAction.WET_SOURCE;
|
||||||
case 2 -> RiverCaveAction.FALLING_WATER;
|
case 2 -> RiverCaveAction.FALLING_FLUID;
|
||||||
case 3 -> RiverCaveAction.DRY_AIR;
|
case 3 -> RiverCaveAction.DRY_AIR;
|
||||||
case 4 -> RiverCaveAction.SEAL_GUARD;
|
case 4 -> RiverCaveAction.SEAL_GUARD;
|
||||||
default -> throw new IOException("Unknown river cave hydrology action code " + code);
|
default -> throw new IOException("Unknown river cave hydrology action code " + code);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int fluidKindCode(RiverCaveFluidKind fluidKind) {
|
||||||
|
return switch (fluidKind) {
|
||||||
|
case RIVER -> 1;
|
||||||
|
case DEEP_POOL -> 2;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private RiverCaveFluidKind fluidKindFromCode(int code) throws IOException {
|
||||||
|
return switch (code) {
|
||||||
|
case 1 -> RiverCaveFluidKind.RIVER;
|
||||||
|
case 2 -> RiverCaveFluidKind.DEEP_POOL;
|
||||||
|
default -> throw new IOException("Unknown river cave fluid-kind code " + code);
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -447,6 +447,15 @@ public class ServerConfiguratorDatapackFingerprintTest {
|
|||||||
assertFalse(ServerConfigurator.loadedRegistrySatisfies(
|
assertFalse(ServerConfigurator.loadedRegistrySatisfies(
|
||||||
loaded,
|
loaded,
|
||||||
Map.of("worldgen/biome/overworld:new", "biome-new")));
|
Map.of("worldgen/biome/overworld:new", "biome-new")));
|
||||||
|
assertFalse(ServerConfigurator.runtimeRequiresRegistryRestart(
|
||||||
|
loaded,
|
||||||
|
Map.of(
|
||||||
|
"dimension_type/iris:overworld", "dimension-a",
|
||||||
|
"worldgen/biome/overworld:forest", "biome-a")));
|
||||||
|
assertTrue(ServerConfigurator.runtimeRequiresRegistryRestart(
|
||||||
|
loaded,
|
||||||
|
Map.of("dimension_type/iris:overworld", "dimension-b")));
|
||||||
|
assertFalse(ServerConfigurator.runtimeRequiresRegistryRestart(loaded, Map.of()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
+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.PluralValue;
|
||||||
import art.arcane.volmlib.util.localization.TextValue;
|
import art.arcane.volmlib.util.localization.TextValue;
|
||||||
import art.arcane.volmlib.util.localization.VolmitLocales;
|
import art.arcane.volmlib.util.localization.VolmitLocales;
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
@@ -174,6 +175,29 @@ public class IrisLanguageTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void compactBukkitLocaleUsesSortedCatalogPositionsAndEnglishFallbacks() {
|
||||||
|
List<String> messageIds = IrisLanguage.catalog().ids().stream()
|
||||||
|
.filter(id -> !id.startsWith("iris.modded."))
|
||||||
|
.sorted()
|
||||||
|
.toList();
|
||||||
|
String translatedId = IrisMessages.COMMAND_UNKNOWN.id();
|
||||||
|
JsonArray values = new JsonArray(messageIds.size());
|
||||||
|
for (int i = 0; i < messageIds.size(); i++) {
|
||||||
|
values.add(messageIds.get(i).equals(translatedId) ? "Unbekannter Iris-Befehl" : null);
|
||||||
|
}
|
||||||
|
JsonArray compact = new JsonArray(2);
|
||||||
|
compact.add("de_DE");
|
||||||
|
compact.add(values);
|
||||||
|
|
||||||
|
LocaleOverlay overlay = IrisLanguage.parseOverlay("test", "de_DE", compact.toString());
|
||||||
|
|
||||||
|
assertEquals(Set.of(translatedId), overlay.values().keySet());
|
||||||
|
assertEquals(
|
||||||
|
"Unbekannter Iris-Befehl",
|
||||||
|
((TextValue) overlay.value(translatedId)).template());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void bundledServerResourcesExactlyMatchNonEnglishManifest() throws Exception {
|
public void bundledServerResourcesExactlyMatchNonEnglishManifest() throws Exception {
|
||||||
Set<String> expected = VolmitLocales.nonEnglish().stream()
|
Set<String> expected = VolmitLocales.nonEnglish().stream()
|
||||||
|
|||||||
@@ -651,6 +651,35 @@ public class PackDownloaderTest {
|
|||||||
assertEquals(0, PackDownloader.downloadLockCount());
|
assertEquals(0, PackDownloader.downloadLockCount());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void invalidBuiltInRiverSchemaPreservesExistingTarget() throws Exception {
|
||||||
|
File packsFolder = temp.newFolder("invalid-river-packs");
|
||||||
|
File target = writePack(packsFolder.toPath().resolve("overworld"), "overworld", "old");
|
||||||
|
File extracted = writePack(temp.newFolder("invalid-river-source").toPath(), "overworld", "new");
|
||||||
|
Files.writeString(
|
||||||
|
extracted.toPath().resolve("dimensions/overworld.json"),
|
||||||
|
"{\"name\":\"Overworld\",\"regions\":[\"local\"],\"logicalHeight\":256,"
|
||||||
|
+ "\"dimensionHeight\":{\"min\":-64,\"max\":320},\"rivers\":{\"enabled\":true,"
|
||||||
|
+ "\"terrain\":{},\"water\":{\"mode\":\"SEA_LEVEL\"}}}",
|
||||||
|
StandardCharsets.UTF_8
|
||||||
|
);
|
||||||
|
|
||||||
|
PackDownloader.PackInstallResult result = PackDownloader.installExtractedPack(
|
||||||
|
packsFolder,
|
||||||
|
extracted,
|
||||||
|
true,
|
||||||
|
"overworld",
|
||||||
|
ignored -> {
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assertNull(result);
|
||||||
|
assertEquals("old", Files.readString(target.toPath().resolve("state.txt"), StandardCharsets.UTF_8));
|
||||||
|
assertTrue(Files.isRegularFile(target.toPath().resolve("regions/local.json")));
|
||||||
|
assertTransactionStateClean(packsFolder);
|
||||||
|
assertEquals(0, PackDownloader.downloadLockCount());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void forceOverwriteReplacesEnginelessLoadedPackData() throws Exception {
|
public void forceOverwriteReplacesEnginelessLoadedPackData() throws Exception {
|
||||||
// A registered loader with no engines is a stale catalog registration (startup
|
// A registered loader with no engines is a stale catalog registration (startup
|
||||||
|
|||||||
@@ -127,8 +127,9 @@ public class PackRiverValidatorTest {
|
|||||||
{
|
{
|
||||||
"id": "trunk",
|
"id": "trunk",
|
||||||
"seed": 17,
|
"seed": 17,
|
||||||
"bodyWavelength": 31,
|
"bodyWavelength": 7,
|
||||||
"bodyDetailWavelength": 16385,
|
"bodyDetailWavelength": 16385,
|
||||||
|
"bodyDetailInfluence": 1.1,
|
||||||
"widthVariation": -0.1,
|
"widthVariation": -0.1,
|
||||||
"bankVariation": 0.876,
|
"bankVariation": 0.876,
|
||||||
"depthVariation": -0.1,
|
"depthVariation": -0.1,
|
||||||
@@ -150,8 +151,9 @@ public class PackRiverValidatorTest {
|
|||||||
|
|
||||||
PackRiverValidator.Validation result = validate(pack);
|
PackRiverValidator.Validation result = validate(pack);
|
||||||
|
|
||||||
assertContains(result.errors(), "rivers.terrain.worms[0].bodyWavelength must be at least 32");
|
assertContains(result.errors(), "rivers.terrain.worms[0].bodyWavelength must be at least 8");
|
||||||
assertContains(result.errors(), "rivers.terrain.worms[0].bodyDetailWavelength must be at most 16384");
|
assertContains(result.errors(), "rivers.terrain.worms[0].bodyDetailWavelength must be at most 16384");
|
||||||
|
assertContains(result.errors(), "rivers.terrain.worms[0].bodyDetailInfluence must be at most 1");
|
||||||
assertContains(result.errors(), "rivers.terrain.worms[0].widthVariation must be at least 0");
|
assertContains(result.errors(), "rivers.terrain.worms[0].widthVariation must be at least 0");
|
||||||
assertContains(result.errors(), "rivers.terrain.worms[0].bankVariation must be at most 0.875");
|
assertContains(result.errors(), "rivers.terrain.worms[0].bankVariation must be at most 0.875");
|
||||||
assertContains(result.errors(), "rivers.terrain.worms[0].depthVariation must be at least 0");
|
assertContains(result.errors(), "rivers.terrain.worms[0].depthVariation must be at least 0");
|
||||||
@@ -357,6 +359,140 @@ public class PackRiverValidatorTest {
|
|||||||
assertContains(result.errors(), "rivers.water.dropHeight must not exceed maximumPoolRise");
|
assertContains(result.errors(), "rivers.water.dropHeight must not exceed maximumPoolRise");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void acceptsIndependentCaveLavaRiverHeightAndPalette() throws Exception {
|
||||||
|
File pack = pack("""
|
||||||
|
{
|
||||||
|
"regions": ["region"],
|
||||||
|
"dimensionHeight": {"min": -64, "max": 320},
|
||||||
|
"fluidHeight": 63,
|
||||||
|
"rivers": {
|
||||||
|
"enabled": true,
|
||||||
|
"terrain": {"worms": [{"id": "river"}]},
|
||||||
|
"water": {
|
||||||
|
"mode": "FIXED",
|
||||||
|
"fluidHeight": -48,
|
||||||
|
"fluidPalette": {
|
||||||
|
"palette": [{"block": "minecraft:lava"}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
PackRiverValidator.Validation result = validate(pack);
|
||||||
|
|
||||||
|
assertTrue(result.errors().toString(), result.errors().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void acceptsSparseBlobbyDeepLavaPoolsWithIndependentHeight() throws Exception {
|
||||||
|
File pack = pack("""
|
||||||
|
{
|
||||||
|
"regions": ["region"],
|
||||||
|
"dimensionHeight": {"min": -256, "max": 512},
|
||||||
|
"rivers": {
|
||||||
|
"enabled": true,
|
||||||
|
"terrain": {"worms": [{"id": "river"}]},
|
||||||
|
"caves": {
|
||||||
|
"deepPools": {
|
||||||
|
"enabled": true,
|
||||||
|
"reach": {
|
||||||
|
"chance": 0.08,
|
||||||
|
"influence": 0.04,
|
||||||
|
"style": {"style": "IRIS", "zoom": 4096}
|
||||||
|
},
|
||||||
|
"minimumSpacing": 768,
|
||||||
|
"maximumPerReach": 1,
|
||||||
|
"minimumFluidY": -224,
|
||||||
|
"maximumFluidY": -108,
|
||||||
|
"searchRadius": 20,
|
||||||
|
"searchAttempts": 12,
|
||||||
|
"horizontalRadius": 24,
|
||||||
|
"verticalRadius": 10,
|
||||||
|
"dryHeadroom": 5,
|
||||||
|
"shapeStyle": {"style": "IRIS", "zoom": 12},
|
||||||
|
"shapeVariation": 0.6,
|
||||||
|
"warpStyle": {"style": "IRIS", "zoom": 24},
|
||||||
|
"warpStrength": 8,
|
||||||
|
"maximumVolume": 65536,
|
||||||
|
"fluidPalette": {
|
||||||
|
"palette": [{"block": "minecraft:lava"}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
PackRiverValidator.Validation result = validate(pack);
|
||||||
|
|
||||||
|
assertTrue(result.errors().toString(), result.errors().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rejectsUnsafeDeepPoolEnvelopeShapeAndPalette() throws Exception {
|
||||||
|
File pack = pack("""
|
||||||
|
{
|
||||||
|
"regions": ["region"],
|
||||||
|
"dimensionHeight": {"min": -64, "max": 320},
|
||||||
|
"rivers": {
|
||||||
|
"enabled": true,
|
||||||
|
"terrain": {"worms": [{"id": "river"}]},
|
||||||
|
"caves": {
|
||||||
|
"deepPools": {
|
||||||
|
"enabled": true,
|
||||||
|
"reach": {"chance": 0},
|
||||||
|
"minimumFluidY": -90,
|
||||||
|
"maximumFluidY": -120,
|
||||||
|
"searchRadius": 120,
|
||||||
|
"horizontalRadius": 20,
|
||||||
|
"verticalRadius": 8,
|
||||||
|
"dryHeadroom": 8,
|
||||||
|
"maximumVolume": 64,
|
||||||
|
"fluidPalette": {"palette": []}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
PackRiverValidator.Validation result = validate(pack);
|
||||||
|
|
||||||
|
assertContains(result.errors(), "deepPools.minimumFluidY must not exceed maximumFluidY");
|
||||||
|
assertContains(result.errors(), "deepPools.dryHeadroom must be smaller than verticalRadius");
|
||||||
|
assertContains(result.errors(), "deepPools.searchRadius plus horizontalRadius must not exceed 128");
|
||||||
|
assertContains(result.errors(), "deepPools.maximumVolume must be at least");
|
||||||
|
assertContains(result.errors(), "deepPools.fluidPalette.palette must contain at least one fluid block");
|
||||||
|
assertContains(result.errors(), "deepPools fluid range and chamber envelope must remain inside dimensionHeight");
|
||||||
|
assertContains(result.warnings(), "deepPools is enabled but its reach gate cannot accept any pools");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rejectsRetiredWaterModeInvalidPaletteAndOutOfBoundsHeight() throws Exception {
|
||||||
|
File pack = pack("""
|
||||||
|
{
|
||||||
|
"regions": ["region"],
|
||||||
|
"dimensionHeight": {"min": -64, "max": 320},
|
||||||
|
"rivers": {
|
||||||
|
"enabled": true,
|
||||||
|
"terrain": {"worms": [{"id": "river"}]},
|
||||||
|
"water": {
|
||||||
|
"mode": "SEA_LEVEL",
|
||||||
|
"fluidHeight": -80,
|
||||||
|
"fluidPalette": {"palette": []}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
PackRiverValidator.Validation result = validate(pack);
|
||||||
|
|
||||||
|
assertContains(result.errors(), "rivers.water.mode must be one of");
|
||||||
|
assertContains(result.errors(), "rivers.water.fluidHeight must remain inside dimensionHeight");
|
||||||
|
assertContains(result.errors(), "rivers.water.fluidPalette.palette must contain at least one fluid block");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void rejectsPathologicalCombinedTopologyComplexity() throws Exception {
|
public void rejectsPathologicalCombinedTopologyComplexity() throws Exception {
|
||||||
File pack = pack("""
|
File pack = pack("""
|
||||||
|
|||||||
+33
@@ -3,11 +3,17 @@ package art.arcane.iris.core.pregenerator.methods;
|
|||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.concurrent.Semaphore;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertSame;
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public class AsyncPregenMethodConcurrencyCapTest {
|
public class AsyncPregenMethodConcurrencyCapTest {
|
||||||
@Test
|
@Test
|
||||||
@@ -77,4 +83,31 @@ public class AsyncPregenMethodConcurrencyCapTest {
|
|||||||
assertEquals(0, slowRequests.get());
|
assertEquals(0, slowRequests.get());
|
||||||
assertEquals("generated", request.join());
|
assertEquals("generated", request.join());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void closeDrainWaitsPastWarningIntervalsUntilEveryPermitReturns() throws Exception {
|
||||||
|
Semaphore semaphore = new Semaphore(0);
|
||||||
|
AtomicInteger warnings = new AtomicInteger();
|
||||||
|
ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||||
|
try {
|
||||||
|
Future<Boolean> drain = executor.submit(() -> AsyncPregenMethod.awaitDrain(
|
||||||
|
semaphore,
|
||||||
|
2,
|
||||||
|
10L,
|
||||||
|
TimeUnit.MILLISECONDS,
|
||||||
|
warnings::incrementAndGet
|
||||||
|
));
|
||||||
|
|
||||||
|
while (warnings.get() == 0) {
|
||||||
|
Thread.onSpinWait();
|
||||||
|
}
|
||||||
|
semaphore.release(2);
|
||||||
|
|
||||||
|
assertFalse(drain.get(1L, TimeUnit.SECONDS));
|
||||||
|
assertTrue(warnings.get() > 0);
|
||||||
|
assertEquals(0, semaphore.availablePermits());
|
||||||
|
} finally {
|
||||||
|
executor.shutdownNow();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user