mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-29 23:40:00 +00:00
Compare commits
35
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c700138b66 | ||
|
|
21ab5baf27 | ||
|
|
58ab3fcbbe | ||
|
|
b6e9f8a14d | ||
|
|
0d76d81dab | ||
|
|
aec8f5be8e | ||
|
|
0f9c045524 | ||
|
|
1d4f95829b | ||
|
|
ce4771b609 | ||
|
|
7d73c4d150 | ||
|
|
b3579e623a | ||
|
|
300ecbfc8b | ||
|
|
0c760d9256 | ||
|
|
3dc958edcf | ||
|
|
f64a7c6753 | ||
|
|
7756598057 | ||
|
|
75837d757b | ||
|
|
2690e0878b | ||
|
|
494cdb1a5c | ||
|
|
1d182769d8 | ||
|
|
584e8758a7 | ||
|
|
9f49d29e2b | ||
|
|
e3bc27d50c | ||
|
|
6900f270ee | ||
|
|
db0f98d103 | ||
|
|
547c52540a | ||
|
|
806a33976a | ||
|
|
065c6fed1d | ||
|
|
360f38a819 | ||
|
|
5204d94ebe | ||
|
|
48bfa7d15b | ||
|
|
2acfac36fc | ||
|
|
54213d4b62 | ||
|
|
ff6668a48b | ||
|
|
29cca99947 |
No files matched your search
@@ -81,3 +81,5 @@ BUILDS/
|
||||
/docs/
|
||||
/.perf/
|
||||
/.codegraph/
|
||||
|
||||
.superpowers/
|
||||
+30
-12
@@ -2,8 +2,9 @@ package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyTileCache;
|
||||
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
|
||||
import art.arcane.iris.generation.hydrology.engine.HydrologyPregeneration;
|
||||
import art.arcane.iris.generation.hydrology.plan.WindowKey;
|
||||
import art.arcane.iris.world.pregen.PregenHydrology;
|
||||
import art.arcane.iris.world.pregen.PregenPerformanceProfile;
|
||||
|
||||
import net.minecraft.core.registries.Registries;
|
||||
@@ -31,11 +32,16 @@ import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
public final class OfflineRegionGenerator {
|
||||
private static final int CHUNKS_PER_HYDROLOGY_REGION = WindowKey.SIZE >> 4;
|
||||
|
||||
private OfflineRegionGenerator() {
|
||||
}
|
||||
|
||||
@@ -97,14 +103,13 @@ public final class OfflineRegionGenerator {
|
||||
PregenPerformanceProfile.apply(session.engine());
|
||||
System.out.println("generationProfile=pregen noiseCacheSize=" + IrisSettings.get().getPerformance().getNoiseCacheSize()
|
||||
+ " burstParallelism=" + MultiBurst.burst.parallelism() + " multicore=true");
|
||||
IrisHydrologyRuntime hydrology = session.engine().getComplex().getHydrologyRuntime();
|
||||
try (HydrologyTileCache.PregenerationScope hydrologyScope = hydrology == null ? null
|
||||
: hydrology.preparePregeneration(configuration.hydrologyArea());
|
||||
try (HydrologyPregeneration pregeneration = PregenHydrology.open(session.engine());
|
||||
HeadlessTerrainContext context = HeadlessTerrainContext.create(new HeadlessTerrainContext.Options(
|
||||
session.engine(), runtime, staged.resolve("templates"), configuration.levelKey(),
|
||||
configuration.noiseSettings(), spigot));
|
||||
RegionGenerationWindow workers = new RegionGenerationWindow(configuration.parallelism(),
|
||||
RegionGenerationWindow.Policy.STANDALONE)) {
|
||||
PregenHydrology.prepareAll(pregeneration, configuration.hydrologyRegions());
|
||||
HeadlessRegionTerrain.Session rolling = new HeadlessRegionTerrain.Session(workers);
|
||||
Path regionDirectory = session.engine().getTarget().getWorld().worldFolder().toPath().resolve("region");
|
||||
Files.createDirectory(regionDirectory);
|
||||
@@ -239,14 +244,27 @@ public final class OfflineRegionGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
HydrologyTileCache.PregenerationArea hydrologyArea() {
|
||||
List<long[]> hydrologyRegions() {
|
||||
int halo = NativeRegionPlan.terrainHalo();
|
||||
long minimumX = ((long) chunkX - halo) * 16L;
|
||||
long minimumZ = ((long) chunkZ - halo) * 16L;
|
||||
long maximumX = ((long) chunkX + (long) width * regionsX + halo) * 16L - 1L;
|
||||
long maximumZ = ((long) chunkZ + (long) width * regionsZ + halo) * 16L - 1L;
|
||||
return new HydrologyTileCache.PregenerationArea(Math.toIntExact((minimumX + maximumX) / 2L),
|
||||
Math.toIntExact((minimumZ + maximumZ) / 2L), minimumX, minimumZ, maximumX, maximumZ);
|
||||
Set<Long> seen = new HashSet<>();
|
||||
List<long[]> regions = new ArrayList<>();
|
||||
for (int row = 0; row < regionsZ; row++) {
|
||||
for (int column = 0; column < regionsX; column++) {
|
||||
int regionX = row % 2 == 0 ? column : regionsX - column - 1;
|
||||
int minimumX = Math.floorDiv(chunkX + regionX * width - halo, CHUNKS_PER_HYDROLOGY_REGION);
|
||||
int maximumX = Math.floorDiv(chunkX + (regionX + 1) * width - 1 + halo, CHUNKS_PER_HYDROLOGY_REGION);
|
||||
int minimumZ = Math.floorDiv(chunkZ + row * width - halo, CHUNKS_PER_HYDROLOGY_REGION);
|
||||
int maximumZ = Math.floorDiv(chunkZ + (row + 1) * width - 1 + halo, CHUNKS_PER_HYDROLOGY_REGION);
|
||||
for (int hydrologyZ = minimumZ; hydrologyZ <= maximumZ; hydrologyZ++) {
|
||||
for (int hydrologyX = minimumX; hydrologyX <= maximumX; hydrologyX++) {
|
||||
if (seen.add(((long) hydrologyX << 32) | (hydrologyZ & 0xFFFFFFFFL))) {
|
||||
regions.add(new long[]{hydrologyX, hydrologyZ});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return regions;
|
||||
}
|
||||
|
||||
static Configuration parse(String[] arguments) throws IOException {
|
||||
|
||||
+1
-1
@@ -137,7 +137,7 @@ public class HeadlessChunkGeneratorTest {
|
||||
}
|
||||
Files.writeString(pack.resolve("dimensions/main.json"), """
|
||||
{"name":"Native columns","focus":"swamp","regions":["owned"],"fluidHeight":40,"logicalHeight":256,
|
||||
"landChance":1,"carvingEnabled":false,"hydrology":{"rivers":{"enabled":false}}}
|
||||
"landChance":1,"carvingEnabled":false}
|
||||
""");
|
||||
Files.writeString(pack.resolve("regions/owned.json"), "{\"landBiomes\":[\"swamp\"]}");
|
||||
Files.writeString(pack.resolve("generators/flat.json"), "{\"composite\":[]}");
|
||||
|
||||
+5
-7
@@ -1,7 +1,6 @@
|
||||
package art.arcane.iris.probe;
|
||||
|
||||
import art.arcane.iris.structure.nativegen.NativeStructureVolumeIndex;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyTileCache;
|
||||
import art.arcane.volmlib.nativelib.terrain.structure.NativeStructureVolume;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
@@ -68,11 +67,10 @@ public final class HeadlessRegionTerrainTest {
|
||||
OfflineRegionGenerator.Configuration export = new OfflineRegionGenerator.Configuration(
|
||||
workspace.pack(), "main", 69420L, -3, 2, 1, 2, 3, 4, configuration,
|
||||
"probe", Level.OVERWORLD, NoiseGeneratorSettings.OVERWORLD, root.resolve("export"));
|
||||
HydrologyTileCache.PregenerationArea bounds = export.hydrologyArea();
|
||||
assertEquals(-80L, bounds.minimumBlockX());
|
||||
assertEquals(0L, bounds.minimumBlockZ());
|
||||
assertEquals(15L, bounds.maximumBlockX());
|
||||
assertEquals(111L, bounds.maximumBlockZ());
|
||||
List<long[]> regions = export.hydrologyRegions();
|
||||
assertEquals(2, regions.size());
|
||||
assertArrayEquals(new long[]{-1L, 0L}, regions.get(0));
|
||||
assertArrayEquals(new long[]{0L, 0L}, regions.get(1));
|
||||
assertEquals(1, result.targetChunks());
|
||||
assertEquals(529, result.chunks().size());
|
||||
long terrain = result.chunks().stream().filter(chunk -> chunk.getPersistedStatus() == ChunkStatus.TERRAIN).count();
|
||||
@@ -189,7 +187,7 @@ public final class HeadlessRegionTerrainTest {
|
||||
}
|
||||
Files.writeString(pack.resolve("dimensions/main.json"), """
|
||||
{"name":"Native region","focus":"swamp","regions":["owned"],"fluidHeight":40,"logicalHeight":256,
|
||||
"landChance":1,"carvingEnabled":false,"hydrology":{"rivers":{"enabled":false}}}
|
||||
"landChance":1,"carvingEnabled":false}
|
||||
""");
|
||||
Files.writeString(pack.resolve("regions/owned.json"), "{\"landBiomes\":[\"swamp\"]}");
|
||||
Files.writeString(pack.resolve("generators/flat.json"), "{\"composite\":[]}");
|
||||
|
||||
@@ -14,7 +14,7 @@ dependencies {
|
||||
// log4j-core Filter interface, so its constructor reference needs the type resolvable.
|
||||
compileOnly(libs.log4j.core)
|
||||
testImplementation('junit:junit:4.13.2')
|
||||
testImplementation('org.mockito:mockito-core:5.23.0')
|
||||
testImplementation(libs.mockito.core)
|
||||
testImplementation(libs.paper.api)
|
||||
testImplementation(libs.bstats)
|
||||
testImplementation(project(':core'))
|
||||
@@ -28,6 +28,16 @@ dependencies {
|
||||
}
|
||||
}
|
||||
|
||||
String lwjglVersion = libs.versions.lwjgl.get()
|
||||
List<String> mavenLibraries = [
|
||||
"org.lwjgl:lwjgl:${lwjglVersion}",
|
||||
"org.lwjgl:lwjgl:jar:natives-linux:${lwjglVersion}",
|
||||
"org.lwjgl:lwjgl-opengl:${lwjglVersion}",
|
||||
"org.lwjgl:lwjgl-opengl:jar:natives-linux:${lwjglVersion}",
|
||||
"org.lwjgl:lwjgl-egl:${lwjglVersion}",
|
||||
"org.lwjgl:lwjgl-jawt:${lwjglVersion}",
|
||||
].collect { GString coordinate -> coordinate.toString() }
|
||||
|
||||
tasks.named('processResources').configure {
|
||||
def pluginProperties = [
|
||||
name : rootProject.name,
|
||||
@@ -35,6 +45,7 @@ tasks.named('processResources').configure {
|
||||
apiVersion: apiVersion,
|
||||
main : mainClass,
|
||||
bootstrapper: bootstrapperClass,
|
||||
libraries : mavenLibraries.collect { String coordinate -> "'${coordinate}'" }.join(', '),
|
||||
]
|
||||
inputs.properties(pluginProperties)
|
||||
filesMatching(['**/paper-plugin.yml', '**/plugin.yml']) {
|
||||
|
||||
@@ -57,9 +57,12 @@ import art.arcane.iris.localization.IrisLanguage;
|
||||
import art.arcane.volmlib.util.director.help.DirectorMiniMenu;
|
||||
import art.arcane.volmlib.util.localization.BukkitLanguageSwitcher;
|
||||
import art.arcane.iris.integration.MultiverseCoreLink;
|
||||
import art.arcane.iris.platform.agent.Agent;
|
||||
import art.arcane.iris.platform.agent.AttachSignal;
|
||||
import art.arcane.iris.platform.bukkit.nms.INMS;
|
||||
import art.arcane.volmlib.nativelib.terrain.ServerShutdownBoundary;
|
||||
import art.arcane.iris.studio.view.BukkitGuiHost;
|
||||
import art.arcane.iris.studio.view.GuiHost;
|
||||
import art.arcane.iris.studio.view.PregeneratorJob;
|
||||
import art.arcane.iris.platform.bukkit.BoardSVC;
|
||||
import art.arcane.iris.command.CommandSVC;
|
||||
@@ -200,10 +203,10 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
private final AtomicBoolean postStopFinisherStarted = new AtomicBoolean(false);
|
||||
private final AtomicBoolean serverStopTeardownDeferred = new AtomicBoolean(false);
|
||||
private final AtomicBoolean servicesDisabled = new AtomicBoolean(false);
|
||||
private final AtomicBoolean runtimeTeardownFailed = new AtomicBoolean(false);
|
||||
private final AtomicBoolean sharedRuntimeClosed = new AtomicBoolean(false);
|
||||
private final AtomicBoolean startupBoundaryRestart = new AtomicBoolean(false);
|
||||
private final AtomicBoolean terminalCleanupCompleted = new AtomicBoolean(false);
|
||||
private final AtomicBoolean runtimeTeardownFailed = new AtomicBoolean(false);
|
||||
private volatile boolean generatorDrainCompleted;
|
||||
private volatile boolean enableFailed;
|
||||
private volatile PlaceholderRegistration papiRegistration;
|
||||
@@ -224,6 +227,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
private BukkitDebugDump debugDump;
|
||||
private volatile SettingsHotloadWatch settingsHotloadWatch;
|
||||
private volatile ServerShutdownBoundary serverShutdownBoundary;
|
||||
private boolean shutdownProtectionInitialized;
|
||||
|
||||
public static VolmitSender getSender() {
|
||||
VolmitSender current = sender;
|
||||
@@ -473,10 +477,16 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
}
|
||||
|
||||
if (debug) {
|
||||
String n = e.getClass().getCanonicalName() + "-" + e.getStackTrace()[0].getClassName() + "-" + e.getStackTrace()[0].getLineNumber();
|
||||
String n = e.getClass().getName();
|
||||
StackTraceElement[] trace = e.getStackTrace();
|
||||
if (trace.length > 0) {
|
||||
n += "-" + trace[0].getClassName() + "-" + trace[0].getLineNumber();
|
||||
}
|
||||
|
||||
if (e.getCause() != null) {
|
||||
n += "-" + e.getCause().getStackTrace()[0].getClassName() + "-" + e.getCause().getStackTrace()[0].getLineNumber();
|
||||
StackTraceElement[] causeTrace = e.getCause().getStackTrace();
|
||||
n += "-" + (causeTrace.length == 0 ? e.getCause().getClass().getName()
|
||||
: causeTrace[0].getClassName() + "-" + causeTrace[0].getLineNumber());
|
||||
}
|
||||
|
||||
File f = instance.getDataFile("debug", "caught-exceptions", n + ".txt");
|
||||
@@ -570,6 +580,17 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
this::getIrisVersion,
|
||||
this::getMCVersion));
|
||||
SlimJar.load();
|
||||
restoreAttachSignal();
|
||||
}
|
||||
|
||||
private static void restoreAttachSignal() {
|
||||
try {
|
||||
if (AttachSignal.restoreJvmHandler()) {
|
||||
info("Restored the JVM SIGQUIT handler that the server console replaced");
|
||||
}
|
||||
} catch (RuntimeException failure) {
|
||||
reportError("Could not restore the JVM SIGQUIT handler; the Iris agent may fail to attach.", failure);
|
||||
}
|
||||
}
|
||||
|
||||
private static void bridgeLog(LogLevel level, String message) {
|
||||
@@ -626,10 +647,10 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
postStopFinisherStarted.set(false);
|
||||
serverStopTeardownDeferred.set(false);
|
||||
servicesDisabled.set(false);
|
||||
runtimeTeardownFailed.set(false);
|
||||
sharedRuntimeClosed.set(false);
|
||||
startupBoundaryRestart.set(false);
|
||||
terminalCleanupCompleted.set(false);
|
||||
runtimeTeardownFailed.set(false);
|
||||
generatorDrainCompleted = false;
|
||||
deferredShutdownGenerators.clear();
|
||||
MultiBurst.burst.reopen();
|
||||
@@ -910,6 +931,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
|
||||
public void onEnable() {
|
||||
enableFailed = false;
|
||||
shutdownProtectionInitialized = false;
|
||||
IrisStartupValidation.begin();
|
||||
Bukkit.getPluginManager().registerEvents(new IrisStartupAdmissionListener(), this);
|
||||
try {
|
||||
@@ -921,13 +943,14 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
refuseUnsupportedServer();
|
||||
return;
|
||||
}
|
||||
initializeShutdownProtection();
|
||||
enable();
|
||||
} catch (Throwable failure) {
|
||||
enableFailed = true;
|
||||
if (!FailedEnableLock.engage(failure, FailedEnableLock.irisWorldsPresent(),
|
||||
generatorResolver.refusals().startupWorldsCreated())) {
|
||||
throw failure;
|
||||
}
|
||||
enableFailed = true;
|
||||
return;
|
||||
}
|
||||
BukkitGuiHost.install();
|
||||
@@ -945,6 +968,17 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
reportLockedRuntime();
|
||||
}
|
||||
|
||||
private void initializeShutdownProtection() {
|
||||
if (!INMS.get().supportsIrisWorldGeneration()) {
|
||||
return;
|
||||
}
|
||||
if (!Agent.install()) {
|
||||
throw new IllegalStateException("Iris Java agent is unavailable; shutdown protection cannot be installed.");
|
||||
}
|
||||
INMS.get().initializeLifecycle(getClass().getClassLoader());
|
||||
shutdownProtectionInitialized = true;
|
||||
}
|
||||
|
||||
private void refuseUnsupportedServer() {
|
||||
Throwable bindFailure = INMS.bindFailure();
|
||||
IllegalStateException failure = new IllegalStateException("Iris cannot start: " + (bindFailure == null
|
||||
@@ -988,6 +1022,8 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
}
|
||||
|
||||
public void onDisable() {
|
||||
pendingWorldReplacements.awaitStagingForShutdown();
|
||||
GuiHost.shutdown();
|
||||
preservePendingBlockDrops();
|
||||
papiLifecycleActive = false;
|
||||
if (updateNotifier != null) {
|
||||
@@ -1004,7 +1040,8 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
}
|
||||
IrisLanguage.shutdown();
|
||||
teardownPapi();
|
||||
boolean deferToServerStop = FailedEnableLock.defersTeardownToServerStop(enableFailed, IrisToolbelt.isServerStopping());
|
||||
boolean deferToServerStop = FailedEnableLock.defersTeardownToServerStop(enableFailed, IrisToolbelt.isServerStopping())
|
||||
&& tryDeferPluginClassLoaderClose();
|
||||
boolean restartingAtStartupBoundary = startupBoundaryRestart.get();
|
||||
if (restartingAtStartupBoundary) {
|
||||
teardownRuntime("startup-boundary-restart", 30L);
|
||||
@@ -1036,7 +1073,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
preservePendingBlockDrops();
|
||||
papiLifecycleActive = false;
|
||||
teardownPapi();
|
||||
if (IrisToolbelt.isServerStopping()) {
|
||||
if (IrisToolbelt.isServerStopping() && tryDeferPluginClassLoaderClose()) {
|
||||
quiesceRuntimeForServerShutdown("pre-unload:" + reason);
|
||||
startPostStopFinisher();
|
||||
Iris.debug("Pre-unload hook deferred generator teardown until Paper closes its chunk schedulers.");
|
||||
@@ -1051,6 +1088,19 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
private boolean tryDeferPluginClassLoaderClose() {
|
||||
if (!shutdownProtectionInitialized) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
INMS.get().deferPluginClassLoaderClose();
|
||||
return true;
|
||||
} catch (RuntimeException | LinkageError failure) {
|
||||
Iris.reportError("Failed to retain the Iris plugin class loader; draining shutdown inline.", failure);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Drains the world generators exactly once. Serialized against the JVM shutdown hook so a
|
||||
* second caller cannot rip the pools or services out from under an in-flight drain.
|
||||
@@ -1097,19 +1147,16 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
try {
|
||||
MultiBurst.burst.close();
|
||||
} catch (Throwable failure) {
|
||||
runtimeTeardownFailed.set(true);
|
||||
Iris.reportError("Failed to close Iris generation workers.", failure);
|
||||
}
|
||||
try {
|
||||
MultiBurst.hydrology.close();
|
||||
} catch (Throwable failure) {
|
||||
runtimeTeardownFailed.set(true);
|
||||
Iris.reportError("Failed to close Iris hydrology workers.", failure);
|
||||
}
|
||||
try {
|
||||
MultiBurst.ioBurst.close();
|
||||
} catch (Throwable failure) {
|
||||
runtimeTeardownFailed.set(true);
|
||||
Iris.reportError("Failed to close Iris I/O workers.", failure);
|
||||
}
|
||||
clearQueues();
|
||||
@@ -1128,7 +1175,6 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
|
||||
private void quiesceRuntimeForServerShutdown(String reason) {
|
||||
serverStopTeardownDeferred.set(true);
|
||||
INMS.get().deferPluginClassLoaderClose();
|
||||
JigsawStudioService jigsawStudioService = IrisServices.getOrNull(JigsawStudioService.class);
|
||||
if (jigsawStudioService != null) {
|
||||
try {
|
||||
@@ -1226,30 +1272,37 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
if (!terminalCleanupCompleted.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
J.attempt(() -> INMS.get().uninjectBukkit());
|
||||
try {
|
||||
if (INMS.isBound()) {
|
||||
INMS.get().uninjectBukkit();
|
||||
}
|
||||
} catch (Throwable failure) {
|
||||
Iris.reportError("Failed to remove Iris world-generation hooks.", failure);
|
||||
}
|
||||
try {
|
||||
runPostShutdown();
|
||||
} catch (Throwable e) {
|
||||
runtimeTeardownFailed.set(true);
|
||||
Iris.reportError("Failed to run Iris post-shutdown cleanup.", e);
|
||||
} finally {
|
||||
if (serverStopTeardownDeferred.get() && hasActiveShutdownResources()) {
|
||||
runtimeTeardownFailed.set(true);
|
||||
SHUTDOWN_ERRORS.println("[Iris] Owned background work remains active after cleanup; retaining the plugin class loader until JVM exit.");
|
||||
}
|
||||
boolean activeResources = serverStopTeardownDeferred.get()
|
||||
&& (runtimeTeardownFailed.get() || hasActiveShutdownResources());
|
||||
IrisPlatforms.unbind();
|
||||
if (serverStopTeardownDeferred.get() && runtimeTeardownFailed.get()) {
|
||||
SHUTDOWN_ERRORS.println("[Iris] Runtime cleanup failed; retaining the plugin class loader until JVM exit.");
|
||||
BukkitPlatform.releaseHost();
|
||||
if (activeResources) {
|
||||
SHUTDOWN_ERRORS.println("[Iris] Owned background work has not completed cleanup; retaining the plugin class loader until JVM exit.");
|
||||
} else {
|
||||
try {
|
||||
if (shutdownProtectionInitialized) {
|
||||
INMS.get().releasePluginClassLoaderClose();
|
||||
} else {
|
||||
SlimJar.closeNativeProviders();
|
||||
}
|
||||
} catch (Throwable failure) {
|
||||
SHUTDOWN_ERRORS.println("[Iris] Failed to release the plugin class loader after runtime cleanup.");
|
||||
failure.printStackTrace(SHUTDOWN_ERRORS);
|
||||
}
|
||||
}
|
||||
BukkitPlatform.releaseHost();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,12 +6,22 @@ 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 io.papermc.paper.plugin.loader.library.impl.MavenLibraryResolver;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.eclipse.aether.artifact.DefaultArtifact;
|
||||
import org.eclipse.aether.graph.Dependency;
|
||||
import org.eclipse.aether.repository.RemoteRepository;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
public final class IrisPluginLoader implements PluginLoader {
|
||||
@@ -40,6 +50,21 @@ public final class IrisPluginLoader implements PluginLoader {
|
||||
classpathBuilder.addLibrary(new JarLibrary(library));
|
||||
}
|
||||
}
|
||||
classpathBuilder.addLibrary(mavenResolver(mavenLibraries(context.getPluginSource())));
|
||||
}
|
||||
|
||||
static List<String> mavenLibraries(Path pluginJar) {
|
||||
try (ZipFile jar = new ZipFile(pluginJar.toFile())) {
|
||||
ZipEntry descriptor = jar.getEntry("plugin.yml");
|
||||
if (descriptor == null) {
|
||||
throw new IllegalStateException("The Iris jar " + pluginJar + " has no plugin.yml library list.");
|
||||
}
|
||||
try (Reader reader = new InputStreamReader(jar.getInputStream(descriptor), StandardCharsets.UTF_8)) {
|
||||
return YamlConfiguration.loadConfiguration(reader).getStringList("libraries");
|
||||
}
|
||||
} catch (IOException failure) {
|
||||
throw new IllegalStateException("Unable to read the Iris library list from " + pluginJar + ".", failure);
|
||||
}
|
||||
}
|
||||
|
||||
static List<Path> relocatedLibraries(Path libraryRoot) {
|
||||
@@ -59,6 +84,16 @@ public final class IrisPluginLoader implements PluginLoader {
|
||||
}
|
||||
}
|
||||
|
||||
private static MavenLibraryResolver mavenResolver(List<String> coordinates) {
|
||||
MavenLibraryResolver resolver = new MavenLibraryResolver();
|
||||
resolver.addRepository(new RemoteRepository.Builder("central", "default",
|
||||
MavenLibraryResolver.MAVEN_CENTRAL_DEFAULT_MIRROR).build());
|
||||
for (String coordinate : coordinates) {
|
||||
resolver.addDependency(new Dependency(new DefaultArtifact(coordinate), null));
|
||||
}
|
||||
return resolver;
|
||||
}
|
||||
|
||||
private static boolean isRelocatedLibrary(Path path) {
|
||||
int count = path.getNameCount();
|
||||
for (int i = 0; i < count - 2; i++) {
|
||||
|
||||
@@ -31,8 +31,7 @@ import art.arcane.iris.structure.placement.IrisStructureLocator;
|
||||
import art.arcane.iris.structure.nativegen.NativeStructureGenerationPolicy;
|
||||
import art.arcane.iris.structure.placement.StructureReachability;
|
||||
import art.arcane.iris.world.history.GenerationSemanticQueries;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyFeatureQuery;
|
||||
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
|
||||
import art.arcane.iris.generation.hydrology.query.HydrologyFeatureType;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.structure.nativegen.IrisNativeStructureDecision;
|
||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||
@@ -196,9 +195,9 @@ public class CommandFind implements DirectorExecutor {
|
||||
EngineBukkitOps.gotoPOI(e, type, player(), teleport);
|
||||
}
|
||||
|
||||
@Director(description = "Find an accepted hydrology feature", sync = true)
|
||||
@Director(description = "Find a hydrology water feature", sync = true)
|
||||
public void river(
|
||||
@Param(description = "Feature type: surface, waterfall, underground, grotto, mouth, or a deep-fluid id", customHandler = HydrologyTypeHandler.class)
|
||||
@Param(description = "Feature type: stream, river, major_river, cascade, waterfall, mouth, pond, lake, dam, sea_inlet or cave_pool", customHandler = HydrologyTypeHandler.class)
|
||||
String type,
|
||||
@Param(description = "Should you be teleported", defaultValue = "true")
|
||||
boolean teleport
|
||||
@@ -212,42 +211,32 @@ public class CommandFind implements DirectorExecutor {
|
||||
}
|
||||
return;
|
||||
}
|
||||
IrisHydrologyRuntime runtime = activeEngine.getComplex().getHydrologyRuntime();
|
||||
HydrologyFeatureQuery query;
|
||||
HydrologyFeatureType featureType;
|
||||
try {
|
||||
query = HydrologyFeatureQuery.parse(type);
|
||||
featureType = HydrologyFeatureType.parse(type);
|
||||
} catch (IllegalArgumentException error) {
|
||||
commandSender.sendMessage(C.RED + error.getMessage());
|
||||
return;
|
||||
}
|
||||
String label = featureType.key().replace('_', ' ');
|
||||
Location origin = target.getLocation();
|
||||
World originWorld = origin.getWorld();
|
||||
int requestedDistance = runtime == null
|
||||
? 8192
|
||||
: Math.min(8192, runtime.settings().routing().tileSize() * 15);
|
||||
int maximumDistance = runtime == null
|
||||
? requestedDistance
|
||||
: runtime.maximumFeatureSearchDistance(
|
||||
origin.getBlockX(), origin.getBlockZ(), requestedDistance);
|
||||
commandSender.sendMessage(C.GRAY + "Searching accepted hydrology plans for " + type + "...");
|
||||
int maximumDistance = GenerationSemanticQueries.RIVER_SEARCH_DISTANCE;
|
||||
commandSender.sendMessage(C.GRAY + GenerationSemanticQueries.riverSearchMessage(featureType, maximumDistance));
|
||||
J.a(() -> {
|
||||
try {
|
||||
GenerationSemanticQueries.RiverResult feature = GenerationSemanticQueries.nearestRiver(
|
||||
activeEngine,
|
||||
query,
|
||||
featureType,
|
||||
origin.getBlockX(),
|
||||
origin.getBlockZ(),
|
||||
maximumDistance,
|
||||
(int visited) -> sendStructureMessage(target, commandSender,
|
||||
C.GRAY + "Searched " + visited + " hydrology tiles for " + type + "...")
|
||||
maximumDistance
|
||||
).orElse(null);
|
||||
if (feature == null) {
|
||||
sendStructureMessage(target, commandSender,
|
||||
C.YELLOW + "No accepted " + type + " hydrology feature found within "
|
||||
+ maximumDistance + " blocks.");
|
||||
C.YELLOW + GenerationSemanticQueries.riverNotFoundMessage(featureType, maximumDistance));
|
||||
return;
|
||||
}
|
||||
String label = type + " hydrology feature";
|
||||
if (!teleport) {
|
||||
sendStructureMessage(target, commandSender,
|
||||
C.GREEN + "Found " + label + " at " + feature.x() + ", " + feature.y() + ", " + feature.z() + ".");
|
||||
@@ -259,8 +248,9 @@ public class CommandFind implements DirectorExecutor {
|
||||
target, originWorld, commandSender, label, destination, true);
|
||||
} catch (Throwable error) {
|
||||
sendStructureMessage(target, commandSender,
|
||||
C.RED + "Could not locate " + type + " hydrology: " + error.getClass().getSimpleName());
|
||||
Iris.reportError("Could not locate accepted hydrology feature '" + type + "'.", error);
|
||||
C.RED + "Could not search for " + label + ": " + error.getClass().getSimpleName()
|
||||
+ (error.getMessage() == null ? "" : " - " + error.getMessage()));
|
||||
Iris.reportError("Could not locate hydrology feature '" + featureType.key() + "'.", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import art.arcane.iris.structure.authoring.StructureKey;
|
||||
import art.arcane.iris.structure.authoring.StructureWriteResult;
|
||||
import art.arcane.iris.structure.export.VanillaJigsawExportFormat;
|
||||
import art.arcane.iris.structure.export.VanillaJigsawExportRequest;
|
||||
import art.arcane.iris.structure.export.VanillaJigsawExportSettings;
|
||||
import art.arcane.iris.structure.export.VanillaJigsawExportSource;
|
||||
import art.arcane.iris.structure.graph.PlanarJigsawWorkcellResolver;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
@@ -437,13 +438,14 @@ public class CommandJigsaw implements DirectorExecutor {
|
||||
}
|
||||
VanillaJigsawExportRequest request;
|
||||
try {
|
||||
request = VanillaJigsawExportRequest.builder(
|
||||
VanillaJigsawExportSource.forData(
|
||||
context.request().source(), context.request().structureKey()), destination)
|
||||
VanillaJigsawExportSource source = VanillaJigsawExportSource.forData(
|
||||
context.request().source(), context.request().structureKey());
|
||||
request = VanillaJigsawExportRequest.builder(source, destination)
|
||||
.namespace(namespace)
|
||||
.resourcePath(context.request().structureKey())
|
||||
.format(exportFormat)
|
||||
.replaceExisting(replace)
|
||||
.settings(VanillaJigsawExportSettings.runtimeDefaults(source.loadStructure()))
|
||||
.build();
|
||||
} catch (IllegalArgumentException exception) {
|
||||
sendError(exception.getMessage());
|
||||
|
||||
@@ -21,10 +21,9 @@ package art.arcane.iris.command;
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.studio.map.MapWindow;
|
||||
import art.arcane.iris.studio.map.PaletteExplorerStart;
|
||||
import art.arcane.iris.studio.view.GuiHost;
|
||||
import art.arcane.iris.studio.view.ImageMapStudioGUI;
|
||||
import art.arcane.iris.studio.view.NoiseExplorerGUI;
|
||||
import art.arcane.iris.studio.view.VisionGUI;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.studio.workspace.IrisProject;
|
||||
import art.arcane.iris.studio.workspace.IrisCodeWorkspace;
|
||||
@@ -98,11 +97,14 @@ import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import art.arcane.iris.localization.IrisLanguage;
|
||||
import art.arcane.iris.localization.BukkitCommandMessages;
|
||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
import art.arcane.volmlib.util.localization.LanguageAudience;
|
||||
import art.arcane.iris.localization.BukkitCommandMessagesExtended;
|
||||
@Director(name = "studio", aliases = {"std", "s"}, description = "Studio Commands", descriptionKey = "iris.director.commandstudio.director.studio_commands")
|
||||
public class CommandStudio implements DirectorExecutor {
|
||||
@@ -202,6 +204,22 @@ public class CommandStudio implements DirectorExecutor {
|
||||
}));
|
||||
}
|
||||
|
||||
@Director(description = "Hotload the active Studio pack", descriptionKey = "iris.director.commandstudio.director.hotload_active_studio_pack", aliases = {"refresh"}, origin = DirectorOrigin.BOTH)
|
||||
public void hotload() {
|
||||
VolmitSender commandSender = sender();
|
||||
IrisProject project = Iris.service(StudioSVC.class).getActiveProject();
|
||||
PlatformChunkGenerator generator = project == null ? null : project.getActiveProvider();
|
||||
if (generator == null) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_NO_OPEN_STUDIO_PROJECTS));
|
||||
return;
|
||||
}
|
||||
|
||||
UUID audience = LanguageAudience.current();
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_HOTLOADING));
|
||||
generator.hotloadAsync().whenComplete((ignored, failure) ->
|
||||
LanguageAudience.run(audience, () -> sendHotloadResult(commandSender, failure)));
|
||||
}
|
||||
|
||||
@Director(description = "Toggle your Studio debug scoreboard", descriptionKey = "iris.director.commandstudio.director.toggle_your_studio_debug_scoreboard", aliases = {"board", "sidebar", "sb"}, origin = DirectorOrigin.PLAYER)
|
||||
public void scoreboard() {
|
||||
Player target = player();
|
||||
@@ -255,15 +273,10 @@ public class CommandStudio implements DirectorExecutor {
|
||||
long seed
|
||||
) {
|
||||
if (noGUI()) return;
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_NOISE_EXPLORER));
|
||||
|
||||
if (generator == null) {
|
||||
NoiseExplorerGUI.launch();
|
||||
return;
|
||||
}
|
||||
|
||||
String generatorKey = generator.getLoadKey();
|
||||
NoiseExplorerGUI.launchGeneratorKey(generatorKey, generator, seed);
|
||||
VolmitSender commandSender = sender();
|
||||
UUID openerId = commandSender.isPlayer() ? commandSender.player().getUniqueId() : null;
|
||||
MapWindow.NoiseSelection selection = new MapWindow.NoiseSelection(generator == null ? null : generator.getLoadKey(), generator, seed);
|
||||
withPlayerContext(opener -> openNoiseAtPlayer(commandSender, openerId, opener, selection));
|
||||
}
|
||||
|
||||
@Director(description = "Open the image-map studio (External GUI)", descriptionKey = "iris.director.commandstudio.director.open_image_map_studio_external_gui", aliases = {"imap"})
|
||||
@@ -279,8 +292,7 @@ public class CommandStudio implements DirectorExecutor {
|
||||
return;
|
||||
}
|
||||
|
||||
Engine activeEngine = IrisToolbelt.access(world).getEngine();
|
||||
ImageMapStudioGUI.launch(activeEngine);
|
||||
requestMapStart(world, MapWindow::openImageMaps);
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_IMAGE_MAP_STUDIO));
|
||||
}
|
||||
|
||||
@@ -420,11 +432,61 @@ public class CommandStudio implements DirectorExecutor {
|
||||
return;
|
||||
}
|
||||
|
||||
UUID openerId = sender().isPlayer() ? player().getUniqueId() : null;
|
||||
VisionGUI.launch(IrisToolbelt.access(world).getEngine(), openerId);
|
||||
requestMapStart(world, MapWindow::openMap);
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_MAP));
|
||||
}
|
||||
|
||||
// The map opens where the player stands when they are in that world, otherwise at the world origin.
|
||||
private PaletteExplorerStart mapStartIn(World world, Player opener, UUID openerId) {
|
||||
Location center = opener != null && opener.getWorld().equals(world) ? opener.getLocation() : null;
|
||||
return new PaletteExplorerStart(
|
||||
IrisToolbelt.access(world).getEngine(),
|
||||
world.getUID(),
|
||||
openerId,
|
||||
null,
|
||||
center == null ? 0D : center.getX(),
|
||||
center == null ? 0D : center.getZ()
|
||||
);
|
||||
}
|
||||
|
||||
private void requestMapStart(World world, Consumer<PaletteExplorerStart> launch) {
|
||||
UUID openerId = sender().isPlayer() ? player().getUniqueId() : null;
|
||||
withPlayerContext(opener -> launch.accept(mapStartIn(world, opener, openerId)));
|
||||
}
|
||||
|
||||
private void openNoiseAtPlayer(VolmitSender commandSender, UUID openerId, Player opener, MapWindow.NoiseSelection selection) {
|
||||
World world = opener == null ? null : opener.getWorld();
|
||||
PlatformChunkGenerator worldGenerator = world == null ? null : IrisToolbelt.access(world);
|
||||
Engine engine = worldGenerator == null ? GuiHost.get().findActiveEngine() : worldGenerator.getEngine();
|
||||
if (engine == null) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_YOU_NEED_BE_SPECIFY_IRIS_GENERATED_WORLD));
|
||||
return;
|
||||
}
|
||||
PaletteExplorerStart start = worldGenerator == null
|
||||
? new PaletteExplorerStart(engine, null, openerId, null, 0D, 0D)
|
||||
: mapStartIn(world, opener, openerId);
|
||||
MapWindow.openNoise(start, selection);
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_NOISE_EXPLORER));
|
||||
}
|
||||
|
||||
private void withPlayerContext(Consumer<Player> action) {
|
||||
Player opener = sender().isPlayer() ? player() : null;
|
||||
if (opener == null) {
|
||||
action.accept(null);
|
||||
return;
|
||||
}
|
||||
AtomicBoolean completed = new AtomicBoolean();
|
||||
Consumer<Player> complete = player -> {
|
||||
if (completed.compareAndSet(false, true)) {
|
||||
action.accept(player);
|
||||
}
|
||||
};
|
||||
Runnable retired = () -> complete.accept(null);
|
||||
if (!J.runEntity(opener, () -> complete.accept(opener.isOnline() ? opener : null), 0, retired)) {
|
||||
retired.run();
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Profiles the performance of a dimension", descriptionKey = "iris.director.commandstudio.director.profiles_performance_dimension", origin = DirectorOrigin.PLAYER)
|
||||
public void profile(
|
||||
@Param(description = "The dimension to profile", descriptionKey = "iris.director.commandstudio.param.dimension_profile", contextual = true, contextualOverride = true, defaultValue = "default", customHandler = DimensionHandler.class)
|
||||
@@ -597,6 +659,16 @@ public class CommandStudio implements DirectorExecutor {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_DONE, MessageArgument.untrusted("value", report.getPath())));
|
||||
}
|
||||
|
||||
private void sendHotloadResult(VolmitSender commandSender, Throwable failure) {
|
||||
if (failure != null) {
|
||||
Iris.reportError(failure);
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_HOTLOAD_FAILED,
|
||||
MessageArgument.untrusted("value", String.valueOf(failure.getMessage()))));
|
||||
return;
|
||||
}
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_HOTLOADED));
|
||||
}
|
||||
|
||||
private PlatformChunkGenerator resolveProfileGenerator(IrisDimension dimension) {
|
||||
StudioSVC studioService = Iris.service(StudioSVC.class);
|
||||
if (studioService != null && studioService.isProjectOpen()) {
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package art.arcane.iris.platform.agent;
|
||||
|
||||
import java.lang.foreign.Arena;
|
||||
import java.lang.foreign.FunctionDescriptor;
|
||||
import java.lang.foreign.Linker;
|
||||
import java.lang.foreign.MemorySegment;
|
||||
import java.lang.foreign.ValueLayout;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.util.Locale;
|
||||
|
||||
public final class AttachSignal {
|
||||
private static final int SIGQUIT = 3;
|
||||
private static final int SIGTERM = 15;
|
||||
private static final long ACTION_BYTES = 256;
|
||||
private static final long SIG_IGN = 1L;
|
||||
|
||||
private AttachSignal() {
|
||||
}
|
||||
|
||||
public static boolean restoreJvmHandler() {
|
||||
if (System.getProperty("os.name", "").toLowerCase(Locale.ROOT).contains("win")) {
|
||||
return false;
|
||||
}
|
||||
Linker linker = Linker.nativeLinker();
|
||||
MethodHandle sigaction = linker.downcallHandle(
|
||||
linker.defaultLookup().find("sigaction")
|
||||
.orElseThrow(() -> new IllegalStateException("sigaction is unavailable")),
|
||||
FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS));
|
||||
try (Arena arena = Arena.ofConfined()) {
|
||||
MemorySegment jvm = arena.allocate(ACTION_BYTES);
|
||||
MemorySegment quit = arena.allocate(ACTION_BYTES);
|
||||
if ((int) sigaction.invokeExact(SIGTERM, MemorySegment.NULL, jvm) != 0
|
||||
|| (int) sigaction.invokeExact(SIGQUIT, MemorySegment.NULL, quit) != 0) {
|
||||
throw new IllegalStateException("sigaction could not read the SIGTERM and SIGQUIT handlers");
|
||||
}
|
||||
long handler = jvm.get(ValueLayout.JAVA_LONG, 0);
|
||||
if (handler == 0L || handler == SIG_IGN || quit.get(ValueLayout.JAVA_LONG, 0) == handler) {
|
||||
return false;
|
||||
}
|
||||
if ((int) sigaction.invokeExact(SIGQUIT, jvm, MemorySegment.NULL) != 0) {
|
||||
throw new IllegalStateException("sigaction could not restore the JVM SIGQUIT handler");
|
||||
}
|
||||
return true;
|
||||
} catch (RuntimeException | Error failure) {
|
||||
throw failure;
|
||||
} catch (Throwable failure) {
|
||||
throw new IllegalStateException("Unable to restore the JVM SIGQUIT handler", failure);
|
||||
}
|
||||
}
|
||||
}
|
||||
+31
-34
@@ -17,8 +17,7 @@ import art.arcane.iris.platform.bukkit.terrain.IrisSampleLimits;
|
||||
import art.arcane.iris.platform.bukkit.terrain.IrisSurfaceClassifier;
|
||||
import art.arcane.iris.platform.bukkit.terrain.IrisWorldInfoFactory;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyColumnLayer;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyColumnSample;
|
||||
import art.arcane.iris.generation.hydrology.raster.WaterRole;
|
||||
import art.arcane.iris.generation.terrain.InferredType;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||
@@ -108,15 +107,15 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService {
|
||||
|
||||
try {
|
||||
int surface = engine.getHeight(blockX, blockZ);
|
||||
HydrologyColumnSample hydrology = hydrologySample(engine, blockX, blockZ);
|
||||
WaterRole role = engine.getComplex().getWaterRoleStream().get(blockX, blockZ);
|
||||
int fluid = (int) Math.round(
|
||||
engine.getComplex().getRiverWaterSurfaceStream().get(blockX, blockZ));
|
||||
engine.getComplex().getWaterHeadStream().get(blockX, blockZ));
|
||||
InferredType inferredType = null;
|
||||
if (IrisSurfaceClassifier.requiresSurfaceBiome(surface, fluid)) {
|
||||
IrisBiome biome = engine.getSurfaceBiome(blockX, blockZ);
|
||||
inferredType = biome == null ? null : biome.getInferredType();
|
||||
}
|
||||
return IrisSurfaceClassifier.classify(surface, fluid, inferredType, hydrology);
|
||||
return IrisSurfaceClassifier.classify(surface, fluid, inferredType, role);
|
||||
} catch (Throwable error) {
|
||||
reportQueryFault("surfaceKind", world, error);
|
||||
return IrisSurfaceKind.UNKNOWN;
|
||||
@@ -266,12 +265,12 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService {
|
||||
return false;
|
||||
}
|
||||
|
||||
HydrologyColumnSample hydrology = wantRiver
|
||||
? hydrologySample(engine, blockX, blockZ)
|
||||
: null;
|
||||
WaterRole role = wantRiver
|
||||
? engine.getComplex().getWaterRoleStream().get(blockX, blockZ)
|
||||
: WaterRole.NONE;
|
||||
int surface = wantHeight || wantKind ? engine.getHeight(blockX, blockZ) : 0;
|
||||
int fluid = wantRiver
|
||||
? (int) Math.round(engine.getComplex().getRiverWaterSurfaceStream().get(blockX, blockZ))
|
||||
? (int) Math.round(engine.getComplex().getWaterHeadStream().get(blockX, blockZ))
|
||||
: engine.getDimension().getFluidHeight();
|
||||
boolean needsBiome = wantBiome
|
||||
|| (wantKind && IrisSurfaceClassifier.requiresSurfaceBiome(surface, fluid));
|
||||
@@ -281,29 +280,23 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService {
|
||||
surface,
|
||||
fluid,
|
||||
biome == null ? null : biome.getInferredType(),
|
||||
hydrology
|
||||
role
|
||||
)
|
||||
: IrisSurfaceKind.UNKNOWN;
|
||||
String biomeKey = wantBiome && biome != null ? biome.getLoadKey() : null;
|
||||
int natural = wantNaturalHeight
|
||||
? (int) Math.round(engine.getComplex().getNaturalHeightStream().get(blockX, blockZ)) + minHeight
|
||||
: IrisColumnSample.UNAVAILABLE_HEIGHT;
|
||||
HydrologyColumnLayer surfaceLayer = hydrology == null
|
||||
? null
|
||||
: hydrology.primarySurfaceLayer().orElse(null);
|
||||
boolean riverPresent = surfaceLayer != null;
|
||||
IrisRiverState riverState = wantRiverState
|
||||
? riverState(hydrology)
|
||||
? riverState(role)
|
||||
: IrisRiverState.NONE;
|
||||
double riverDistance = wantRiverDistance && riverPresent
|
||||
? engine.getComplex().getRiverDistanceStream().get(blockX, blockZ)
|
||||
double riverDistance = wantRiverDistance
|
||||
? riverDistance(role)
|
||||
: IrisColumnSample.UNAVAILABLE_RIVER_DISTANCE;
|
||||
int riverFlow = wantRiverFlow && riverPresent
|
||||
? (int) Math.round(engine.getComplex().getRiverFlowStream().get(blockX, blockZ))
|
||||
int riverFlow = wantRiverFlow
|
||||
? riverFlow(role)
|
||||
: IrisColumnSample.UNAVAILABLE_RIVER_FLOW;
|
||||
int riverWaterSurfaceY = wantRiverWaterSurface
|
||||
&& surfaceLayer != null
|
||||
&& surfaceLayer.connectedFluid()
|
||||
int riverWaterSurfaceY = wantRiverWaterSurface && role.wet()
|
||||
? fluid + minHeight
|
||||
: IrisColumnSample.UNAVAILABLE_HEIGHT;
|
||||
sink.accept(new IrisColumnSample(
|
||||
@@ -327,21 +320,25 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService {
|
||||
}
|
||||
}
|
||||
|
||||
static IrisRiverState riverState(HydrologyColumnSample hydrology) {
|
||||
HydrologyColumnLayer layer = hydrology == null
|
||||
? null
|
||||
: hydrology.primarySurfaceLayer().orElse(null);
|
||||
if (layer == null || !layer.channel()) {
|
||||
return IrisRiverState.NONE;
|
||||
}
|
||||
return layer.connectedFluid() ? IrisRiverState.WET : IrisRiverState.DRY;
|
||||
static IrisRiverState riverState(WaterRole role) {
|
||||
return role.wet() ? IrisRiverState.WET : IrisRiverState.NONE;
|
||||
}
|
||||
|
||||
private static HydrologyColumnSample hydrologySample(Engine engine, int blockX, int blockZ) {
|
||||
if (engine.getComplex().getHydrologyRuntime() == null) {
|
||||
return null;
|
||||
static double riverDistance(WaterRole role) {
|
||||
return switch (role) {
|
||||
case NONE -> IrisColumnSample.UNAVAILABLE_RIVER_DISTANCE;
|
||||
case CHANNEL, POND, MOUTH, CASCADE, WATERFALL, INLET -> 0D;
|
||||
case SHORE, POND_SHORE -> 1D;
|
||||
case BANK, DAM -> 2D;
|
||||
};
|
||||
}
|
||||
return engine.getComplex().getHydrologyRuntime().sample(blockX, blockZ).orElse(null);
|
||||
|
||||
static int riverFlow(WaterRole role) {
|
||||
return switch (role) {
|
||||
case NONE, SHORE, POND_SHORE, BANK, DAM -> IrisColumnSample.UNAVAILABLE_RIVER_FLOW;
|
||||
case CHANNEL, MOUTH, CASCADE, WATERFALL -> 1;
|
||||
case POND, INLET -> 0;
|
||||
};
|
||||
}
|
||||
|
||||
private static Optional<String> key(IrisBiome biome) {
|
||||
|
||||
+8
-18
@@ -1,8 +1,7 @@
|
||||
package art.arcane.iris.platform.bukkit.terrain;
|
||||
|
||||
import art.arcane.iris.api.terrain.IrisSurfaceKind;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyColumnLayer;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyColumnSample;
|
||||
import art.arcane.iris.generation.hydrology.raster.WaterRole;
|
||||
import art.arcane.iris.generation.terrain.InferredType;
|
||||
|
||||
public final class IrisSurfaceClassifier {
|
||||
@@ -29,25 +28,16 @@ public final class IrisSurfaceClassifier {
|
||||
int engineSurfaceHeight,
|
||||
int engineFluidHeight,
|
||||
InferredType inferredType,
|
||||
HydrologyColumnSample hydrology
|
||||
WaterRole role
|
||||
) {
|
||||
if (engineSurfaceHeight <= 0) {
|
||||
return IrisSurfaceKind.VOID;
|
||||
}
|
||||
HydrologyColumnLayer layer = hydrology == null
|
||||
? null
|
||||
: hydrology.primarySurfaceLayer().orElse(null);
|
||||
if (layer != null) {
|
||||
if (layer.shore()) {
|
||||
return IrisSurfaceKind.RIVER_SHORE;
|
||||
}
|
||||
if (layer.channel()) {
|
||||
return layer.connectedFluid() ? IrisSurfaceKind.RIVER : IrisSurfaceKind.DRY_CHANNEL;
|
||||
}
|
||||
if (layer.grading()) {
|
||||
return IrisSurfaceKind.LAND;
|
||||
}
|
||||
}
|
||||
return classify(engineSurfaceHeight, engineFluidHeight, inferredType);
|
||||
return switch (role) {
|
||||
case NONE -> classify(engineSurfaceHeight, engineFluidHeight, inferredType);
|
||||
case SHORE, POND_SHORE -> IrisSurfaceKind.RIVER_SHORE;
|
||||
case BANK, DAM -> IrisSurfaceKind.LAND;
|
||||
case CHANNEL, POND, MOUTH, CASCADE, WATERFALL, INLET -> IrisSurfaceKind.RIVER;
|
||||
};
|
||||
}
|
||||
}
|
||||
+39
-3
@@ -23,6 +23,8 @@ import art.arcane.iris.world.event.IrisEngineHotloadEvent;
|
||||
import art.arcane.iris.world.IrisToolbelt;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
|
||||
import art.arcane.iris.studio.StudioSVC;
|
||||
import art.arcane.iris.studio.workspace.IrisProject;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.event.EventHandler;
|
||||
@@ -40,10 +42,47 @@ public final class BukkitGuiHost implements GuiHost.Provider {
|
||||
GuiHost.set(new BukkitGuiHost());
|
||||
}
|
||||
|
||||
/**
|
||||
* The engine of the open studio project, which reads the pack folder being edited, or else the first
|
||||
* Iris world. A normal Iris world runs on its own copy of the pack, made when the world was created.
|
||||
*/
|
||||
@Override
|
||||
public Engine findActiveEngine() {
|
||||
Engine studioEngine = findStudioEngine();
|
||||
if (studioEngine != null) {
|
||||
return studioEngine;
|
||||
}
|
||||
try {
|
||||
for (World world : new ArrayList<>(Bukkit.getWorlds())) {
|
||||
Engine engine = engineOf(world);
|
||||
if (engine != null) {
|
||||
return engine;
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Engine findEngine(UUID worldId) {
|
||||
World world = worldId == null ? null : Bukkit.getWorld(worldId);
|
||||
return world == null ? null : engineOf(world);
|
||||
}
|
||||
|
||||
private static Engine findStudioEngine() {
|
||||
try {
|
||||
StudioSVC studio = Iris.service(StudioSVC.class);
|
||||
IrisProject project = studio == null || !studio.isProjectOpen() ? null : studio.getActiveProject();
|
||||
PlatformChunkGenerator provider = project == null ? null : project.getActiveProvider();
|
||||
Engine engine = provider == null ? null : provider.getEngine();
|
||||
return engine == null || engine.isClosed() ? null : engine;
|
||||
} catch (Throwable ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static Engine engineOf(World world) {
|
||||
try {
|
||||
PlatformChunkGenerator access = IrisToolbelt.access(world);
|
||||
if (access != null && access.getEngine() != null && !access.getEngine().isClosed()) {
|
||||
@@ -51,9 +90,6 @@ public final class BukkitGuiHost implements GuiHost.Provider {
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
+83
-40
@@ -18,23 +18,19 @@
|
||||
|
||||
package art.arcane.iris.studio.view;
|
||||
|
||||
import art.arcane.volmlib.nativelib.view.WorldMarker;
|
||||
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.studio.render.RenderType;
|
||||
import art.arcane.iris.world.IrisWorld;
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.LivingEntity;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
@@ -56,7 +52,7 @@ public final class BukkitVisionOverlay implements GuiOverlay {
|
||||
private final AtomicBoolean playerRefreshQueued = new AtomicBoolean();
|
||||
private final AtomicLong teleportSequence = new AtomicLong();
|
||||
private final AtomicReference<VisionTeleportRequest> latestTeleport = new AtomicReference<>();
|
||||
private volatile List<WorldMarker> playerMarkers = List.of();
|
||||
private volatile List<GuiMarker> playerMarkers = List.of();
|
||||
|
||||
public BukkitVisionOverlay(Engine engine, UUID openerId) {
|
||||
this.engine = engine;
|
||||
@@ -68,7 +64,7 @@ public final class BukkitVisionOverlay implements GuiOverlay {
|
||||
* built by a server thread.
|
||||
*/
|
||||
@Override
|
||||
public List<WorldMarker> players() {
|
||||
public List<GuiMarker> players() {
|
||||
queuePlayerRefresh();
|
||||
return playerMarkers;
|
||||
}
|
||||
@@ -78,26 +74,48 @@ public final class BukkitVisionOverlay implements GuiOverlay {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean scheduled = J.runGlobal(() -> {
|
||||
try {
|
||||
List<WorldMarker> markers = new ArrayList<>();
|
||||
for (Player player : BukkitWorldBinding.players(engine.getWorld())) {
|
||||
Location at = player.getLocation();
|
||||
markers.add(WorldMarker.player(player.getName(), at.getX(), at.getZ()));
|
||||
}
|
||||
playerMarkers = List.copyOf(markers);
|
||||
} finally {
|
||||
playerRefreshQueued.set(false);
|
||||
}
|
||||
});
|
||||
boolean scheduled = J.runGlobal(this::refreshPlayers);
|
||||
|
||||
if (!scheduled) {
|
||||
playerRefreshQueued.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshPlayers() {
|
||||
World world = BukkitWorldBinding.world(engine.getWorld());
|
||||
List<Player> players = BukkitWorldBinding.players(engine.getWorld());
|
||||
if (players.isEmpty() || world == null) {
|
||||
playerMarkers = List.of();
|
||||
playerRefreshQueued.set(false);
|
||||
return;
|
||||
}
|
||||
List<GuiMarker> markers = Collections.synchronizedList(new ArrayList<>(players.size()));
|
||||
AtomicInteger pending = new AtomicInteger(players.size());
|
||||
for (Player player : players) {
|
||||
AtomicBoolean completed = new AtomicBoolean();
|
||||
Runnable complete = () -> {
|
||||
if (completed.compareAndSet(false, true) && pending.decrementAndGet() == 0) {
|
||||
playerMarkers = List.copyOf(markers);
|
||||
playerRefreshQueued.set(false);
|
||||
}
|
||||
};
|
||||
if (!J.runEntity(player, () -> {
|
||||
try {
|
||||
if (player.isOnline() && player.getWorld() == world) {
|
||||
Location at = player.getLocation();
|
||||
markers.add(GuiMarker.player(player.getName(), at.getX(), at.getZ(), at.getYaw()));
|
||||
}
|
||||
} finally {
|
||||
complete.run();
|
||||
}
|
||||
}, 0, complete)) {
|
||||
complete.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestEntities(Consumer<List<WorldMarker>> sink) {
|
||||
public void requestEntities(Consumer<List<GuiMarker>> sink) {
|
||||
J.runGlobal(() -> {
|
||||
IrisWorld target = engine.getWorld();
|
||||
World world = BukkitWorldBinding.world(target);
|
||||
@@ -118,7 +136,7 @@ public final class BukkitVisionOverlay implements GuiOverlay {
|
||||
return;
|
||||
}
|
||||
|
||||
List<WorldMarker> collected = Collections.synchronizedList(new ArrayList<>(living.size()));
|
||||
List<GuiMarker> collected = Collections.synchronizedList(new ArrayList<>(living.size()));
|
||||
AtomicInteger pending = new AtomicInteger(living.size());
|
||||
Runnable complete = () -> {
|
||||
if (pending.decrementAndGet() == 0) {
|
||||
@@ -144,22 +162,22 @@ public final class BukkitVisionOverlay implements GuiOverlay {
|
||||
});
|
||||
}
|
||||
|
||||
private WorldMarker marker(LivingEntity entity, Location at) {
|
||||
private GuiMarker marker(LivingEntity entity, Location at) {
|
||||
String label = Form.capitalizeWords(entity.getType().name().toLowerCase(Locale.ROOT).replaceAll("\\Q_\\E", " "));
|
||||
double maxHealth = 0;
|
||||
try {
|
||||
maxHealth = entity.getAttribute(MAX_HEALTH).getValue();
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
return WorldMarker.entity(label, at.getX(), at.getY(), at.getZ(), entity.getHealth(), maxHealth);
|
||||
return GuiMarker.entity(label, at.getX(), at.getY(), at.getZ(), entity.getHealth(), maxHealth);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void teleport(double worldX, double worldZ) {
|
||||
VisionTeleportRequest request = new VisionTeleportRequest(
|
||||
teleportSequence.incrementAndGet(),
|
||||
VisionGUI.floorWorldCoordinate(worldX),
|
||||
VisionGUI.floorWorldCoordinate(worldZ));
|
||||
(int) StrictMath.floor(worldX),
|
||||
(int) StrictMath.floor(worldZ));
|
||||
latestTeleport.set(request);
|
||||
startTeleport(request);
|
||||
}
|
||||
@@ -212,6 +230,46 @@ public final class BukkitVisionOverlay implements GuiOverlay {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestOpenerPosition(Consumer<OpenerPosition> sink) {
|
||||
AtomicBoolean completed = new AtomicBoolean();
|
||||
Consumer<OpenerPosition> complete = position -> {
|
||||
if (completed.compareAndSet(false, true)) {
|
||||
sink.accept(position);
|
||||
}
|
||||
};
|
||||
if (openerId == null) {
|
||||
complete.accept(OpenerPosition.OFFLINE);
|
||||
return;
|
||||
}
|
||||
boolean scheduled = J.runGlobal(() -> {
|
||||
Player player = Bukkit.getPlayer(openerId);
|
||||
if (player == null) {
|
||||
complete.accept(OpenerPosition.OFFLINE);
|
||||
return;
|
||||
}
|
||||
Runnable retired = () -> complete.accept(OpenerPosition.OFFLINE);
|
||||
if (!J.runEntity(player, () -> complete.accept(positionOf(player,
|
||||
BukkitWorldBinding.world(engine.getWorld()))), 0, retired)) {
|
||||
retired.run();
|
||||
}
|
||||
});
|
||||
if (!scheduled) {
|
||||
complete.accept(OpenerPosition.OFFLINE);
|
||||
}
|
||||
}
|
||||
|
||||
private static OpenerPosition positionOf(Player player, World world) {
|
||||
if (!player.isOnline()) {
|
||||
return OpenerPosition.OFFLINE;
|
||||
}
|
||||
if (world == null || player.getWorld() != world) {
|
||||
return OpenerPosition.IN_OTHER_WORLD;
|
||||
}
|
||||
Location at = player.getLocation();
|
||||
return OpenerPosition.inWorld(at.getX(), at.getY(), at.getZ());
|
||||
}
|
||||
|
||||
private Player selectPlayer(List<Player> players) {
|
||||
if (openerId == null) {
|
||||
return players.isEmpty() ? null : players.get(0);
|
||||
@@ -316,21 +374,6 @@ public final class BukkitVisionOverlay implements GuiOverlay {
|
||||
+ blockX + "," + blockZ + ".", failure);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String openInEditor(double worldX, double worldZ, RenderType type) {
|
||||
IrisComplex complex = engine.getComplex();
|
||||
File file = switch (type) {
|
||||
case BIOME, LAYER_LOAD, DECORATOR_LOAD, OBJECT_LOAD, HEIGHT, RIVER ->
|
||||
complex.getTrueBiomeStream().get(worldX, worldZ).openInVSCode();
|
||||
case BIOME_LAND -> complex.getLandBiomeStream().get(worldX, worldZ).openInVSCode();
|
||||
case BIOME_SEA -> complex.getSeaBiomeStream().get(worldX, worldZ).openInVSCode();
|
||||
case REGION -> complex.getRegionStream().get(worldX, worldZ).openInVSCode();
|
||||
case CAVE_LAND -> complex.getCaveBiomeStream().get(worldX, worldZ).openInVSCode();
|
||||
default -> null;
|
||||
};
|
||||
return file == null ? null : file.getName();
|
||||
}
|
||||
|
||||
private static final class VisionTeleportRequest {
|
||||
private final long sequence;
|
||||
private final int blockX;
|
||||
|
||||
+59
-1
@@ -58,6 +58,7 @@ import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
@@ -78,6 +79,9 @@ public final class PendingWorldReplacementManager implements Listener {
|
||||
private volatile UUID overworldEntryAwaitingVerification;
|
||||
private volatile World overworldEntryWorld;
|
||||
private volatile boolean paperEntryListenerRegistered;
|
||||
private final Object stagingMonitor = new Object();
|
||||
private boolean stagingAdmissionOpen = true;
|
||||
private StagingAdmission activeStaging;
|
||||
|
||||
public PendingWorldReplacementManager(Iris plugin) {
|
||||
this.plugin = Objects.requireNonNull(plugin, "plugin");
|
||||
@@ -135,7 +139,8 @@ public final class PendingWorldReplacementManager implements Listener {
|
||||
LifecycleOperationCoordinator.Domain.WORLD_MUTATION,
|
||||
LifecycleOperationCoordinator.OperationKind.WORLD_REPLACE,
|
||||
requiredWorldKey.toString()
|
||||
); WorldReplacementProgress progress = WorldReplacementProgress.start(
|
||||
); StagingAdmission ignoredAdmission = admitStaging(requiredWorldKey);
|
||||
WorldReplacementProgress progress = WorldReplacementProgress.start(
|
||||
requiredSender, requiredWorldKey.toString())) {
|
||||
synchronized (this) {
|
||||
IrisStartupValidation.requireWorldReplacementStagingReady();
|
||||
@@ -271,6 +276,29 @@ public final class PendingWorldReplacementManager implements Listener {
|
||||
}
|
||||
}
|
||||
|
||||
public void awaitStagingForShutdown() {
|
||||
StagingAdmission staging;
|
||||
synchronized (stagingMonitor) {
|
||||
stagingAdmissionOpen = false;
|
||||
staging = activeStaging;
|
||||
}
|
||||
if (staging == null) {
|
||||
return;
|
||||
}
|
||||
Iris.warn("Waiting for the replacement of " + staging.worldKey + " to finish staging before Iris shuts down.");
|
||||
boolean interrupted = false;
|
||||
while (staging.finished.getCount() > 0L) {
|
||||
try {
|
||||
staging.finished.await();
|
||||
} catch (InterruptedException e) {
|
||||
interrupted = true;
|
||||
}
|
||||
}
|
||||
if (interrupted) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void processPendingStartupReplacements() {
|
||||
ArrayList<String> failures = new ArrayList<>();
|
||||
ArrayList<String> restartBoundaries = new ArrayList<>();
|
||||
@@ -1048,6 +1076,17 @@ public final class PendingWorldReplacementManager implements Listener {
|
||||
return plugin.getDataFolder().toPath().toAbsolutePath().normalize();
|
||||
}
|
||||
|
||||
private StagingAdmission admitStaging(NamespacedKey worldKey) throws IOException {
|
||||
synchronized (stagingMonitor) {
|
||||
if (!stagingAdmissionOpen) {
|
||||
throw new IOException("Iris is shutting down. Run the replacement again after the server starts.");
|
||||
}
|
||||
StagingAdmission staging = new StagingAdmission(worldKey);
|
||||
activeStaging = staging;
|
||||
return staging;
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void loadOverworldEntryGuard() throws IOException {
|
||||
ExactWorldSlotPathPolicy.Target target = resolveTarget(OVERWORLD_KEY);
|
||||
Optional<WorldReplacementEntryGuard.Entry> loaded = WorldReplacementEntryGuard.load(target.worldDirectory());
|
||||
@@ -1218,6 +1257,25 @@ public final class PendingWorldReplacementManager implements Listener {
|
||||
record SavedLocationInspection(boolean safe, Throwable failure) {
|
||||
}
|
||||
|
||||
private final class StagingAdmission implements AutoCloseable {
|
||||
private final NamespacedKey worldKey;
|
||||
private final CountDownLatch finished = new CountDownLatch(1);
|
||||
|
||||
private StagingAdmission(NamespacedKey worldKey) {
|
||||
this.worldKey = worldKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
synchronized (stagingMonitor) {
|
||||
if (activeStaging == this) {
|
||||
activeStaging = null;
|
||||
}
|
||||
}
|
||||
finished.countDown();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class RestartBoundaryRequired extends IOException {
|
||||
private RestartBoundaryRequired(String message) {
|
||||
super(message);
|
||||
|
||||
@@ -27,3 +27,4 @@ permissions:
|
||||
description: Allows survival players to fell Iris-managed trees with an axe.
|
||||
default: op
|
||||
api-version: '${apiVersion}'
|
||||
libraries: [ ${libraries} ]
|
||||
@@ -0,0 +1,72 @@
|
||||
import { readFile, writeFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { setup, openStudio, closeStudio, sampleTerrain, readJson, writeJson,
|
||||
logPosition, logSince } from './studio-support.mjs'
|
||||
|
||||
async function hotload(context, fixture, active, command) {
|
||||
const before = await readJson(fixture.manifestPath(active.key))
|
||||
const response = await context.command(command, /^Studio hotloaded\.$|^Studio hotload failed:/, 90000)
|
||||
context.expect(response === 'Studio hotloaded.', 'The manual Studio command did not complete successfully', { command, response })
|
||||
const after = await readJson(fixture.manifestPath(active.key))
|
||||
context.expect(after.pendingActivationId === null, 'The manual hotload completed with a pending activation')
|
||||
context.report.iris.manualHotloads.push({ command, response,
|
||||
before: before.activeActivationId, after: after.activeActivationId })
|
||||
return after
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'iris-studio-manual-hotload',
|
||||
description: 'Refresh an open Studio through hotload and its refresh alias, verify terrain, and reject commands without a Studio.',
|
||||
async run(context) {
|
||||
const fixture = await setup(context)
|
||||
const biomeFile = path.join(fixture.pack, 'biomes', 'flat.json')
|
||||
const original = await readFile(biomeFile, 'utf8')
|
||||
const logOffset = await logPosition(fixture)
|
||||
const processId = (await context.observe()).processId
|
||||
context.report.iris.manualHotloads = []
|
||||
let active
|
||||
try {
|
||||
await context.step('Reject hotload without an open Studio', async () => {
|
||||
await context.command('/iris studio hotload', /^No open studio projects\.$/, 10000)
|
||||
})
|
||||
await context.step('Open Studio and hotload an unchanged pack', async () => {
|
||||
active = await openStudio(context, fixture)
|
||||
await sampleTerrain(context, fixture, 0, fixture.state.material)
|
||||
await hotload(context, fixture, active, '/iris studio hotload')
|
||||
await sampleTerrain(context, fixture, 4096, fixture.state.material)
|
||||
})
|
||||
await context.step('Refresh an edited pack and inspect newly generated terrain', async () => {
|
||||
const before = await readJson(fixture.manifestPath(active.key))
|
||||
const biome = await readJson(biomeFile)
|
||||
biome.name = 'Manual hotload terrain'
|
||||
biome.layers[0].palette[0].block = 'minecraft:gold_block'
|
||||
await writeJson(biomeFile, biome)
|
||||
await hotload(context, fixture, active, '/iris studio refresh')
|
||||
const after = await fixture.wait(async () => {
|
||||
const manifest = await readJson(fixture.manifestPath(active.key))
|
||||
return manifest.activeActivationId !== before.activeActivationId && manifest.pendingActivationId === null ? manifest : false
|
||||
}, 'The edited Studio pack did not become active', 90000)
|
||||
context.report.iris.activations.push({ before: before.activeActivationId,
|
||||
after: after.activeActivationId, material: 'gold_block' })
|
||||
await sampleTerrain(context, fixture, 16384, 'gold_block')
|
||||
await sampleTerrain(context, fixture, 0, fixture.state.material)
|
||||
})
|
||||
await context.step('Close Studio and reject another refresh', async () => {
|
||||
await closeStudio(context, fixture, active)
|
||||
active = undefined
|
||||
await context.command('/iris studio refresh', /^No open studio projects\.$/, 10000)
|
||||
await context.command('/minecraft:seed', /Seed:/i, 10000)
|
||||
})
|
||||
context.expect((await context.observe()).processId === processId, 'The server restarted during manual Studio hotload')
|
||||
const log = await logSince(fixture, logOffset)
|
||||
context.expect(!/Studio generation update failed|Studio hotload failed|Unable to (capture terrain|save the terrain boundary)|ClosedChannelException|Chunk system error|server has stopped responding|could not acquire its engine runtime|Failed to list files/i.test(log),
|
||||
'Manual Studio hotload emitted a generation, snapshot, storage, or watchdog failure')
|
||||
} finally {
|
||||
try {
|
||||
if (active) await closeStudio(context, fixture, active)
|
||||
} finally {
|
||||
await writeFile(biomeFile, original)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -148,6 +148,9 @@ public class FailedEnableLockTest {
|
||||
try (EnableFixture fixture = new EnableFixture(false)) {
|
||||
IllegalStateException failure = assertThrows(IllegalStateException.class, plugin::onEnable);
|
||||
assertTrue(failure.getMessage(), failure.getMessage().contains("already bound"));
|
||||
Field failed = Iris.class.getDeclaredField("enableFailed");
|
||||
failed.setAccessible(true);
|
||||
assertTrue(failed.getBoolean(plugin));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package art.arcane.iris;
|
||||
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.PrintStream;
|
||||
import java.lang.reflect.Field;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
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 IrisErrorReportingTest {
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
public void debugReportingPreservesStacklessFailures() throws Exception {
|
||||
ArrayIndexOutOfBoundsException failure = new ArrayIndexOutOfBoundsException("raster index");
|
||||
failure.setStackTrace(new StackTraceElement[0]);
|
||||
|
||||
String output = reportWithDebug(failure);
|
||||
|
||||
assertEquals(1L, output.lines().filter(line -> line.contains("raster index")).count());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void debugReportingPreservesFailureWithStacklessCause() throws Exception {
|
||||
ArrayIndexOutOfBoundsException cause = new ArrayIndexOutOfBoundsException("raster index");
|
||||
cause.setStackTrace(new StackTraceElement[0]);
|
||||
IllegalStateException failure = new IllegalStateException("Hydrology failed", cause);
|
||||
|
||||
String output = reportWithDebug(failure);
|
||||
|
||||
assertEquals(1L, output.lines().filter(line -> line.contains("Hydrology failed")).count());
|
||||
assertTrue(output.contains("Caused by: java.lang.ArrayIndexOutOfBoundsException: raster index"));
|
||||
assertTrue(output.contains("IrisErrorReportingTest.debugReportingPreservesFailureWithStacklessCause"));
|
||||
}
|
||||
|
||||
private String reportWithDebug(Throwable failure) throws Exception {
|
||||
Iris previousPlugin = Iris.instance;
|
||||
IrisSettings previousSettings = IrisSettings.settings;
|
||||
PrintStream previousError = System.err;
|
||||
Iris plugin = mock(Iris.class);
|
||||
Field deferred = Iris.class.getDeclaredField("serverStopTeardownDeferred");
|
||||
deferred.setAccessible(true);
|
||||
deferred.set(plugin, new AtomicBoolean());
|
||||
File report = new File(temporaryFolder.getRoot(), "failure.txt");
|
||||
when(plugin.getDataFile(any(String[].class))).thenReturn(report);
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
try (PrintStream errors = new PrintStream(output, true, StandardCharsets.UTF_8);
|
||||
MockedStatic<Iris> iris = mockStatic(Iris.class)) {
|
||||
Iris.instance = plugin;
|
||||
IrisSettings.settings = new IrisSettings();
|
||||
IrisSettings.settings.getGeneral().setDebug(true);
|
||||
System.setErr(errors);
|
||||
iris.when(() -> Iris.reportError(failure)).thenCallRealMethod();
|
||||
|
||||
Iris.reportError(failure);
|
||||
|
||||
assertTrue(Files.readString(report.toPath()).contains(failure.toString()));
|
||||
return output.toString(StandardCharsets.UTF_8);
|
||||
} finally {
|
||||
System.setErr(previousError);
|
||||
IrisSettings.settings = previousSettings;
|
||||
Iris.instance = previousPlugin;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,13 @@ import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
@@ -31,4 +35,19 @@ public class IrisPluginLoaderTest {
|
||||
|
||||
assertEquals(List.of(relocatedCaffeine, relocatedGson), libraries);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readsMavenLibrariesFromTheSpigotDescriptorInThePluginJar() throws Exception {
|
||||
Path jar = temporaryFolder.getRoot().toPath().resolve("Iris.jar");
|
||||
try (OutputStream file = Files.newOutputStream(jar); ZipOutputStream zip = new ZipOutputStream(file)) {
|
||||
zip.putNextEntry(new ZipEntry("plugin.yml"));
|
||||
zip.write("name: Iris\nlibraries: [ 'org.lwjgl:lwjgl:3.4.3', 'org.lwjgl:lwjgl:jar:natives-linux:3.4.3' ]\n"
|
||||
.getBytes(StandardCharsets.UTF_8));
|
||||
zip.closeEntry();
|
||||
}
|
||||
|
||||
List<String> libraries = IrisPluginLoader.mavenLibraries(jar);
|
||||
|
||||
assertEquals(List.of("org.lwjgl:lwjgl:3.4.3", "org.lwjgl:lwjgl:jar:natives-linux:3.4.3"), libraries);
|
||||
}
|
||||
}
|
||||
+269
-2
@@ -1,7 +1,17 @@
|
||||
package art.arcane.iris;
|
||||
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.platform.agent.Agent;
|
||||
import art.arcane.iris.platform.bootstrap.SlimJar;
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.platform.bukkit.plugin.IrisService;
|
||||
import art.arcane.iris.platform.bukkit.plugin.VolmitPlugin;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.world.lifecycle.IrisRuntimeStatics;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.iris.generation.runtime.GenerationSessionLease;
|
||||
import art.arcane.iris.generation.runtime.GenerationSessionManager;
|
||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
@@ -17,6 +27,7 @@ import art.arcane.iris.world.WorldMaintenance;
|
||||
import art.arcane.iris.world.runtime.BukkitEnginePlatformHooks;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import art.arcane.volmlib.nativelib.terrain.ServerShutdownBoundary;
|
||||
import art.arcane.volmlib.integration.ReloadAware;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.World;
|
||||
@@ -26,10 +37,13 @@ import org.mockito.MockedStatic;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
@@ -37,10 +51,16 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doCallRealMethod;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
@@ -109,8 +129,6 @@ public class IrisShutdownMaintenanceTest {
|
||||
}
|
||||
verify(fixture.generator).quiesceForServerShutdown();
|
||||
verify(fixture.generator, never()).closeAsync();
|
||||
fixture.nms.verify(INMS::get);
|
||||
verify(fixture.binding).deferPluginClassLoaderClose();
|
||||
verify(fixture.binding, never()).releasePluginClassLoaderClose();
|
||||
}
|
||||
assertEquals(0, sessions.activeLeases());
|
||||
@@ -151,6 +169,229 @@ public class IrisShutdownMaintenanceTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void startupEnsuresAgentReadinessBeforeLifecycleProtectionWithoutWorldInjection() throws Exception {
|
||||
try (Fixture fixture = new Fixture(); MockedStatic<Agent> agent = mockStatic(Agent.class)) {
|
||||
when(fixture.binding.supportsIrisWorldGeneration()).thenReturn(true);
|
||||
agent.when(Agent::install).thenReturn(true);
|
||||
doAnswer(invocation -> {
|
||||
agent.verify(Agent::install);
|
||||
assertSame(fixture.plugin.getClass().getClassLoader(), invocation.getArgument(0));
|
||||
return null;
|
||||
}).when(fixture.binding).initializeLifecycle(fixture.plugin.getClass().getClassLoader());
|
||||
invoke(fixture.plugin, "initializeShutdownProtection");
|
||||
verify(fixture.binding).initializeLifecycle(fixture.plugin.getClass().getClassLoader());
|
||||
verify(fixture.binding, never()).injectBukkit();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unavailableAgentStopsLifecycleInitialization() throws Exception {
|
||||
try (Fixture fixture = new Fixture(); MockedStatic<Agent> agent = mockStatic(Agent.class)) {
|
||||
when(fixture.binding.supportsIrisWorldGeneration()).thenReturn(true);
|
||||
InvocationTargetException failure = assertThrows(InvocationTargetException.class,
|
||||
() -> invoke(fixture.plugin, "initializeShutdownProtection"));
|
||||
assertTrue(failure.getCause() instanceof IllegalStateException);
|
||||
verify(fixture.binding, never()).initializeLifecycle(fixture.plugin.getClass().getClassLoader());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void worldHookTeardownFailureReleasesProtectionAfterServicesAndWorkersStop() throws Exception {
|
||||
try (Fixture fixture = new Fixture();
|
||||
MockedStatic<Iris> iris = mockStatic(Iris.class);
|
||||
MockedStatic<IrisRuntimeStatics> statics = mockStatic(IrisRuntimeStatics.class);
|
||||
MockedStatic<IrisPlatforms> platforms = mockStatic(IrisPlatforms.class);
|
||||
MockedStatic<BukkitPlatform> bukkitPlatform = mockStatic(BukkitPlatform.class);
|
||||
MockedStatic<SlimJar> providers = mockStatic(SlimJar.class)) {
|
||||
IrisService service = mock(IrisService.class);
|
||||
IllegalStateException failure = new IllegalStateException("world hook teardown failed");
|
||||
fixture.nms.when(INMS::isBound).thenReturn(true);
|
||||
doThrow(failure).when(fixture.binding).uninjectBukkit();
|
||||
setField(fixture.plugin, "services", new KMap<Class<? extends IrisService>, IrisService>());
|
||||
setField(fixture.plugin, "enabledServices", List.of(service));
|
||||
doAnswer(invocation -> {
|
||||
platforms.verify(IrisPlatforms::unbind);
|
||||
bukkitPlatform.verify(BukkitPlatform::releaseHost);
|
||||
assertTrue(MultiBurst.burst.isTerminated());
|
||||
assertTrue(MultiBurst.hydrology.isTerminated());
|
||||
assertTrue(MultiBurst.ioBurst.isTerminated());
|
||||
return null;
|
||||
}).when(fixture.binding).releasePluginClassLoaderClose();
|
||||
fixture.quiesce();
|
||||
try {
|
||||
fixture.finish(5L);
|
||||
fixture.finish(5L);
|
||||
verify(service).onDisable();
|
||||
iris.verify(() -> Iris.reportError("Failed to remove Iris world-generation hooks.", failure));
|
||||
assertTrue(MultiBurst.burst.isTerminated());
|
||||
assertTrue(MultiBurst.hydrology.isTerminated());
|
||||
assertTrue(MultiBurst.ioBurst.isTerminated());
|
||||
verify(fixture.binding).releasePluginClassLoaderClose();
|
||||
platforms.verify(IrisPlatforms::unbind);
|
||||
bukkitPlatform.verify(BukkitPlatform::releaseHost);
|
||||
} finally {
|
||||
MultiBurst.burst.reopen();
|
||||
MultiBurst.hydrology.reopen();
|
||||
MultiBurst.ioBurst.reopen();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failedServiceWithAnOwnedExecutorRetainsTheClassLoader() throws Exception {
|
||||
ExecutorService ownedExecutor = Executors.newSingleThreadExecutor();
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
CountDownLatch finish = new CountDownLatch(1);
|
||||
ownedExecutor.submit(() -> {
|
||||
started.countDown();
|
||||
finish.await();
|
||||
return null;
|
||||
});
|
||||
try (Fixture fixture = new Fixture();
|
||||
MockedStatic<Iris> iris = mockStatic(Iris.class);
|
||||
MockedStatic<IrisRuntimeStatics> statics = mockStatic(IrisRuntimeStatics.class);
|
||||
MockedStatic<IrisPlatforms> platforms = mockStatic(IrisPlatforms.class);
|
||||
MockedStatic<BukkitPlatform> bukkitPlatform = mockStatic(BukkitPlatform.class)) {
|
||||
assertTrue(started.await(5, TimeUnit.SECONDS));
|
||||
IrisService service = mock(IrisService.class);
|
||||
IllegalStateException failure = new IllegalStateException("service teardown failed");
|
||||
doThrow(failure).when(service).onDisable();
|
||||
setField(fixture.plugin, "services", new KMap<Class<? extends IrisService>, IrisService>());
|
||||
setField(fixture.plugin, "enabledServices", List.of(service));
|
||||
fixture.quiesce();
|
||||
fixture.finish(5L);
|
||||
verify(service).onDisable();
|
||||
iris.verify(() -> Iris.reportError("Failed to disable " + service.getClass().getSimpleName() + ".", failure));
|
||||
assertFalse(ownedExecutor.isTerminated());
|
||||
verify(fixture.binding, never()).releasePluginClassLoaderClose();
|
||||
} finally {
|
||||
finish.countDown();
|
||||
ownedExecutor.shutdownNow();
|
||||
assertTrue(ownedExecutor.awaitTermination(5, TimeUnit.SECONDS));
|
||||
MultiBurst.burst.reopen();
|
||||
MultiBurst.hydrology.reopen();
|
||||
MultiBurst.ioBurst.reopen();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failedPostShutdownCallbackRetainsTheClassLoaderForSkippedCleanup() throws Exception {
|
||||
ExecutorService ownedExecutor = Executors.newSingleThreadExecutor();
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
CountDownLatch finish = new CountDownLatch(1);
|
||||
ownedExecutor.submit(() -> {
|
||||
started.countDown();
|
||||
finish.await();
|
||||
return null;
|
||||
});
|
||||
try (Fixture fixture = new Fixture();
|
||||
MockedStatic<Iris> iris = mockStatic(Iris.class);
|
||||
MockedStatic<IrisRuntimeStatics> statics = mockStatic(IrisRuntimeStatics.class);
|
||||
MockedStatic<IrisPlatforms> platforms = mockStatic(IrisPlatforms.class);
|
||||
MockedStatic<BukkitPlatform> bukkitPlatform = mockStatic(BukkitPlatform.class)) {
|
||||
assertTrue(started.await(5, TimeUnit.SECONDS));
|
||||
IllegalStateException failure = new IllegalStateException("post-shutdown callback failed");
|
||||
AtomicBoolean cleanupAttempted = new AtomicBoolean();
|
||||
KList<Runnable> callbacks = new KList<>();
|
||||
callbacks.add(() -> { throw failure; });
|
||||
callbacks.add(() -> {
|
||||
cleanupAttempted.set(true);
|
||||
ownedExecutor.shutdown();
|
||||
});
|
||||
Field callbacksField = VolmitPlugin.class.getDeclaredField("postShutdown");
|
||||
callbacksField.setAccessible(true);
|
||||
callbacksField.set(fixture.plugin, callbacks);
|
||||
Method runCallbacks = VolmitPlugin.class.getDeclaredMethod("runPostShutdown");
|
||||
runCallbacks.setAccessible(true);
|
||||
runCallbacks.invoke(doCallRealMethod().when(fixture.plugin));
|
||||
fixture.quiesce();
|
||||
fixture.finish(5L);
|
||||
iris.verify(() -> Iris.reportError("Failed to run Iris post-shutdown cleanup.", failure));
|
||||
assertFalse(cleanupAttempted.get());
|
||||
assertFalse(ownedExecutor.isShutdown());
|
||||
assertFalse(ownedExecutor.isTerminated());
|
||||
verify(fixture.binding, never()).releasePluginClassLoaderClose();
|
||||
} finally {
|
||||
finish.countDown();
|
||||
ownedExecutor.shutdownNow();
|
||||
assertTrue(ownedExecutor.awaitTermination(5, TimeUnit.SECONDS));
|
||||
MultiBurst.burst.reopen();
|
||||
MultiBurst.hydrology.reopen();
|
||||
MultiBurst.ioBurst.reopen();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void incompleteDrainRetainsServicesWorkersAndClassLoader() throws Exception {
|
||||
try (Fixture fixture = new Fixture(); MockedStatic<Iris> iris = mockStatic(Iris.class)) {
|
||||
IrisService service = mock(IrisService.class);
|
||||
setField(fixture.plugin, "services", new KMap<Class<? extends IrisService>, IrisService>());
|
||||
setField(fixture.plugin, "enabledServices", List.of(service));
|
||||
when(fixture.generator.closeAsync()).thenReturn(new CompletableFuture<>());
|
||||
fixture.quiesce();
|
||||
fixture.finish(0L);
|
||||
verify(service, never()).onDisable();
|
||||
verify(fixture.binding, never()).releasePluginClassLoaderClose();
|
||||
verify(fixture.binding, never()).uninjectBukkit();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void serverStopPreUnloadDefersTeardownWhileTheClassLoaderIsHeld() throws Exception {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
setField(fixture.plugin, "postStopFinisherStarted", new AtomicBoolean(true));
|
||||
try {
|
||||
fixture.commitReloadDuringServerStop();
|
||||
verify(fixture.binding).deferPluginClassLoaderClose();
|
||||
verify(fixture.generator).quiesceForServerShutdown();
|
||||
verify(fixture.generator, never()).closeAsync();
|
||||
assertTrue(fixture.serverStopTeardownDeferred());
|
||||
} finally {
|
||||
WorldMaintenance.endWorldMaintenance(fixture.identity, "pre-unload:HOT_UNLOAD");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void serverStopPreUnloadDrainsInlineWhenTheClassLoaderCannotBeHeld() throws Exception {
|
||||
try (Fixture fixture = new Fixture(); MockedStatic<Iris> iris = mockStatic(Iris.class)) {
|
||||
IllegalStateException failure = new IllegalStateException("class loader hold failed");
|
||||
doThrow(failure).when(fixture.binding).deferPluginClassLoaderClose();
|
||||
try {
|
||||
fixture.commitReloadDuringServerStop();
|
||||
verify(fixture.generator).closeAsync();
|
||||
verify(fixture.generator, never()).quiesceForServerShutdown();
|
||||
assertFalse(fixture.serverStopTeardownDeferred());
|
||||
iris.verify(() -> Iris.reportError("Failed to retain the Iris plugin class loader; draining shutdown inline.", failure));
|
||||
} finally {
|
||||
WorldMaintenance.endWorldMaintenance(fixture.identity, "pre-unload:HOT_UNLOAD", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void serverStopPreUnloadDrainsInlineWithoutInitializedProtection() throws Exception {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
setField(fixture.plugin, "shutdownProtectionInitialized", false);
|
||||
try {
|
||||
fixture.commitReloadDuringServerStop();
|
||||
verify(fixture.binding, never()).deferPluginClassLoaderClose();
|
||||
verify(fixture.generator).closeAsync();
|
||||
verify(fixture.generator, never()).quiesceForServerShutdown();
|
||||
assertFalse(fixture.serverStopTeardownDeferred());
|
||||
} finally {
|
||||
WorldMaintenance.endWorldMaintenance(fixture.identity, "pre-unload:HOT_UNLOAD", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void invoke(Iris plugin, String name) throws ReflectiveOperationException {
|
||||
Method method = Iris.class.getDeclaredMethod(name);
|
||||
method.setAccessible(true);
|
||||
method.invoke(plugin);
|
||||
}
|
||||
|
||||
private static void setField(Iris plugin, String name, Object value) throws ReflectiveOperationException {
|
||||
Field field = Iris.class.getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
@@ -178,6 +419,12 @@ public class IrisShutdownMaintenanceTest {
|
||||
|
||||
private Fixture() throws ReflectiveOperationException {
|
||||
IrisSettings.settings = new IrisSettings();
|
||||
setField(plugin, "shutdownProtectionInitialized", true);
|
||||
setField(plugin, "alreadyDrained", new AtomicBoolean());
|
||||
setField(plugin, "servicesDisabled", new AtomicBoolean());
|
||||
setField(plugin, "runtimeTeardownFailed", new AtomicBoolean());
|
||||
setField(plugin, "sharedRuntimeClosed", new AtomicBoolean());
|
||||
setField(plugin, "terminalCleanupCompleted", new AtomicBoolean());
|
||||
setField(plugin, "serverStopTeardownDeferred", new AtomicBoolean());
|
||||
setField(plugin, "postStopFinisherStarted", new AtomicBoolean());
|
||||
setField(plugin, "deferredShutdownGenerators", new CopyOnWriteArrayList<PlatformChunkGenerator>());
|
||||
@@ -206,6 +453,26 @@ public class IrisShutdownMaintenanceTest {
|
||||
method.invoke(plugin, "manual_disable", 5L);
|
||||
}
|
||||
|
||||
private void commitReloadDuringServerStop() throws ReflectiveOperationException {
|
||||
setField(plugin, "alreadyDrained", new AtomicBoolean());
|
||||
toolbelt.when(IrisToolbelt::isServerStopping).thenReturn(true);
|
||||
doCallRealMethod().when(plugin).commitReload(any());
|
||||
plugin.commitReload(ReloadAware.PreUnloadReason.HOT_UNLOAD);
|
||||
}
|
||||
|
||||
private boolean serverStopTeardownDeferred() throws ReflectiveOperationException {
|
||||
Field field = Iris.class.getDeclaredField("serverStopTeardownDeferred");
|
||||
field.setAccessible(true);
|
||||
return ((AtomicBoolean) field.get(plugin)).get();
|
||||
}
|
||||
|
||||
|
||||
private void finish(long timeoutSeconds) throws ReflectiveOperationException {
|
||||
Method method = Iris.class.getDeclaredMethod("finishDeferredRuntimeTeardown", String.class, long.class);
|
||||
method.setAccessible(true);
|
||||
method.invoke(plugin, "test_shutdown", timeoutSeconds);
|
||||
}
|
||||
|
||||
private void startFinisher() throws ReflectiveOperationException {
|
||||
Method method = Iris.class.getDeclaredMethod("startPostStopFinisher");
|
||||
method.setAccessible(true);
|
||||
|
||||
+77
-10
@@ -1,7 +1,7 @@
|
||||
package art.arcane.iris.command;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyFeatureType;
|
||||
import art.arcane.iris.generation.hydrology.query.HydrologyFeatureType;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.localization.IrisLanguage;
|
||||
@@ -22,6 +22,7 @@ import java.util.OptionalLong;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
@@ -45,7 +46,7 @@ public class CommandFindHydrologyTest {
|
||||
@Test
|
||||
public void worldChangeDuringSearchDoesNotLoadOrTeleportIntoTheNewWorld() {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
fixture.command.river("surface", true);
|
||||
fixture.command.river("river", true);
|
||||
when(fixture.player.getWorld()).thenReturn(mock(World.class));
|
||||
fixture.search.get().run();
|
||||
|
||||
@@ -58,7 +59,7 @@ public class CommandFindHydrologyTest {
|
||||
@Test
|
||||
public void disconnectDuringSearchDoesNotLoadTheDestination() {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
fixture.command.river("surface", true);
|
||||
fixture.command.river("river", true);
|
||||
when(fixture.player.isOnline()).thenReturn(false);
|
||||
fixture.search.get().run();
|
||||
|
||||
@@ -69,7 +70,7 @@ public class CommandFindHydrologyTest {
|
||||
@Test
|
||||
public void worldChangeWhileLoadingTheDestinationCancelsTeleport() {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
fixture.command.river("surface", true);
|
||||
fixture.command.river("river", true);
|
||||
fixture.search.get().run();
|
||||
when(fixture.player.getWorld()).thenReturn(mock(World.class));
|
||||
fixture.chunk.complete(mock(Chunk.class));
|
||||
@@ -82,7 +83,7 @@ public class CommandFindHydrologyTest {
|
||||
@Test
|
||||
public void successIsReportedOnlyAfterTeleportCompletes() {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
fixture.command.river("surface", true);
|
||||
fixture.command.river("river", true);
|
||||
fixture.search.get().run();
|
||||
fixture.chunk.complete(mock(Chunk.class));
|
||||
|
||||
@@ -95,7 +96,7 @@ public class CommandFindHydrologyTest {
|
||||
@Test
|
||||
public void rejectedTeleportReportsFailureWithoutSuccess() {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
fixture.command.river("surface", true);
|
||||
fixture.command.river("river", true);
|
||||
fixture.search.get().run();
|
||||
fixture.chunk.complete(mock(Chunk.class));
|
||||
fixture.teleport.complete(false);
|
||||
@@ -109,7 +110,7 @@ public class CommandFindHydrologyTest {
|
||||
public void failedTeleportReportsTheFullCauseWithoutSuccess() {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
IllegalStateException failure = new IllegalStateException("destination unloaded");
|
||||
fixture.command.river("surface", true);
|
||||
fixture.command.river("river", true);
|
||||
fixture.search.get().run();
|
||||
fixture.chunk.complete(mock(Chunk.class));
|
||||
fixture.teleport.completeExceptionally(failure);
|
||||
@@ -120,6 +121,72 @@ public class CommandFindHydrologyTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void everyFeatureTypeIsSearchedOffTheCommandThreadAtTheRuntimeRadius() {
|
||||
for (HydrologyFeatureType type : HydrologyFeatureType.values()) {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
fixture.command.river(type.key(), false);
|
||||
fixture.queries.verify(() -> GenerationSemanticQueries.nearestRiver(any(), any(), anyInt(), anyInt(), anyInt()), never());
|
||||
fixture.search.get().run();
|
||||
|
||||
fixture.queries.verify(() -> GenerationSemanticQueries.nearestRiver(fixture.engine, type, 0, 0,
|
||||
GenerationSemanticQueries.RIVER_SEARCH_DISTANCE));
|
||||
verify(fixture.sender).sendMessage(contains("Found " + type.key().replace('_', ' ') + " at 32, 63, 32."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unknownTypeIsRejectedWithTheAcceptedTypesBeforeAnySearch() {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
fixture.command.river("surface", true);
|
||||
|
||||
verify(fixture.sender).sendMessage(contains("Use one of: stream, river, major_river, cascade, waterfall, mouth, pond, lake, dam, sea_inlet, cave_pool."));
|
||||
assertNull(fixture.search.get());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missingCavePoolIsReportedAsNotFoundInGeneratedChunks() {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
fixture.queries.when(() -> GenerationSemanticQueries.nearestRiver(eq(fixture.engine), any(), eq(0), eq(0), anyInt()))
|
||||
.thenReturn(Optional.empty());
|
||||
fixture.command.river("cave_pool", true);
|
||||
fixture.search.get().run();
|
||||
|
||||
verify(fixture.sender).sendMessage(contains("Searching generated chunks for the nearest cave pool within 7168 blocks"));
|
||||
verify(fixture.sender).sendMessage(contains("No cave pool found in generated chunks within 7168 blocks."));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missingFeatureIsReportedAsNotFound() {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
fixture.queries.when(() -> GenerationSemanticQueries.nearestRiver(eq(fixture.engine), any(), eq(0), eq(0), anyInt()))
|
||||
.thenReturn(Optional.empty());
|
||||
fixture.command.river("sea_inlet", true);
|
||||
fixture.search.get().run();
|
||||
|
||||
verify(fixture.sender).sendMessage(contains("No sea inlet found within 7168 blocks."));
|
||||
fixture.platform.verify(() -> BukkitPlatform.chunkAtAsync(any(World.class), anyInt(), anyInt(), eq(true)), never());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void plannerFailureIsReportedAsAnErrorNotAsNotFound() {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
IllegalStateException failure = new IllegalStateException("window planner exploded");
|
||||
fixture.queries.when(() -> GenerationSemanticQueries.nearestRiver(eq(fixture.engine), any(), eq(0), eq(0), anyInt()))
|
||||
.thenThrow(failure);
|
||||
fixture.command.river("lake", true);
|
||||
fixture.search.get().run();
|
||||
|
||||
verify(fixture.sender).sendMessage(contains("Could not search for lake: IllegalStateException - window planner exploded"));
|
||||
verify(fixture.sender, never()).sendMessage(contains("No lake found"));
|
||||
fixture.logging.verify(() -> Iris.reportError(any(String.class), eq(failure)));
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Fixture implements AutoCloseable {
|
||||
private final CommandFind command = mock(CommandFind.class, CALLS_REAL_METHODS);
|
||||
private final Engine engine = mock(Engine.class);
|
||||
@@ -132,7 +199,7 @@ public class CommandFindHydrologyTest {
|
||||
private final CompletableFuture<Boolean> teleport = new CompletableFuture<>();
|
||||
private final MockedStatic<J> scheduler = mockStatic(J.class);
|
||||
private final MockedStatic<BukkitPlatform> platform = mockStatic(BukkitPlatform.class);
|
||||
private final MockedStatic<GenerationSemanticQueries> queries = mockStatic(GenerationSemanticQueries.class);
|
||||
private final MockedStatic<GenerationSemanticQueries> queries = mockStatic(GenerationSemanticQueries.class, CALLS_REAL_METHODS);
|
||||
private final MockedStatic<IrisLanguage> language = mockStatic(IrisLanguage.class, invocation -> "teleported");
|
||||
private final MockedStatic<Iris> logging = mockStatic(Iris.class);
|
||||
|
||||
@@ -160,10 +227,10 @@ public class CommandFindHydrologyTest {
|
||||
});
|
||||
platform.when(() -> BukkitPlatform.chunkAtAsync(any(World.class), anyInt(), anyInt(), eq(true))).thenReturn(chunk);
|
||||
platform.when(() -> BukkitPlatform.teleportAsync(eq(player), any(Location.class))).thenReturn(teleport);
|
||||
queries.when(() -> GenerationSemanticQueries.nearestRiver(eq(engine), any(), eq(0), eq(0), anyInt(), any()))
|
||||
queries.when(() -> GenerationSemanticQueries.nearestRiver(eq(engine), any(), eq(0), eq(0), anyInt()))
|
||||
.thenReturn(Optional.of(new GenerationSemanticQueries.RiverResult(
|
||||
GenerationSemanticQueries.RiverSource.ACTIVE_PREDICTION,
|
||||
HydrologyFeatureType.RIFFLE, Optional.empty(), 1L, 32, 63, 32, OptionalLong.empty())));
|
||||
HydrologyFeatureType.RIVER, 1L, 32, 63, 32, OptionalLong.empty())));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+5
-5
@@ -20,26 +20,26 @@ import static org.mockito.Mockito.when;
|
||||
|
||||
public class CommandObjectFluidHeightTest {
|
||||
@Test
|
||||
public void absoluteObjectPlacementShiftsTheColumnRiverHeadFromNegativeMinY() {
|
||||
public void absoluteObjectPlacementShiftsTheColumnWaterHeadFromNegativeMinY() {
|
||||
World world = mock(World.class);
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisDimension dimension = mock(IrisDimension.class);
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
ProceduralStream<Double> riverHead = mock(ProceduralStream.class);
|
||||
ProceduralStream<Double> waterHead = mock(ProceduralStream.class);
|
||||
Map<Block, BlockData> future = new HashMap<>();
|
||||
|
||||
when(engine.getMinHeight()).thenReturn(-64);
|
||||
when(engine.getDimension()).thenReturn(dimension);
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
when(dimension.getFluidHeight()).thenReturn(127);
|
||||
when(complex.getRiverWaterSurfaceStream()).thenReturn(riverHead);
|
||||
when(riverHead.get(12, -7)).thenReturn(131D);
|
||||
when(complex.getWaterHeadStream()).thenReturn(waterHead);
|
||||
when(waterHead.get(12, -7)).thenReturn(131D);
|
||||
|
||||
IObjectPlacer placer = CommandObject.createPlacer(world, future, engine);
|
||||
|
||||
assertEquals(63, placer.getFluidHeight());
|
||||
assertEquals(67, placer.getFluidHeight(12, -7));
|
||||
verify(riverHead).get(12, -7);
|
||||
verify(waterHead).get(12, -7);
|
||||
}
|
||||
}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
package art.arcane.iris.command;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
|
||||
import art.arcane.iris.studio.StudioSVC;
|
||||
import art.arcane.iris.studio.workspace.IrisProject;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.localization.LanguageAudience;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.lang.reflect.Method;
|
||||
import java.io.File;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class CommandStudioHotloadTest {
|
||||
@Test
|
||||
public void hotloadAndRefreshReportWhenNoStudioIsOpen() throws Exception {
|
||||
RecordingSender sender = new RecordingSender();
|
||||
StudioSVC studio = mock(StudioSVC.class);
|
||||
Method hotload = CommandStudio.class.getDeclaredMethod("hotload");
|
||||
assertTrue(List.of(hotload.getAnnotation(Director.class).aliases()).contains("refresh"));
|
||||
|
||||
try (MockedStatic<Iris> iris = mockStatic(Iris.class)) {
|
||||
iris.when(() -> Iris.service(StudioSVC.class)).thenReturn(studio);
|
||||
hotload.invoke(new TestCommand(sender));
|
||||
assertTrue(sender.messages.getLast(), sender.messages.getLast().contains("No open studio"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void completionFeedbackWaitsForTheHotload() {
|
||||
assertCompletionFeedback(null, "Studio hotloaded.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failedHotloadReportsFailureAndLogsTheCause() {
|
||||
assertCompletionFeedback(new IllegalStateException("Invalid pack"), "Studio hotload failed: Invalid pack");
|
||||
}
|
||||
|
||||
private void assertCompletionFeedback(Throwable failure, String expected) {
|
||||
RecordingSender sender = new RecordingSender();
|
||||
StudioSVC studio = mock(StudioSVC.class);
|
||||
PlatformChunkGenerator generator = mock(PlatformChunkGenerator.class);
|
||||
IrisProject project = new IrisProject(new File("tutorial"));
|
||||
project.setActiveProvider(generator);
|
||||
CompletableFuture<Void> pending = new CompletableFuture<>();
|
||||
UUID audience = UUID.randomUUID();
|
||||
when(studio.getActiveProject()).thenReturn(project);
|
||||
when(generator.hotloadAsync()).thenReturn(pending);
|
||||
|
||||
try (MockedStatic<Iris> iris = mockStatic(Iris.class)) {
|
||||
iris.when(() -> Iris.service(StudioSVC.class)).thenReturn(studio);
|
||||
LanguageAudience.run(audience, () -> new TestCommand(sender).hotload());
|
||||
assertNull(LanguageAudience.current());
|
||||
|
||||
verify(generator).hotloadAsync();
|
||||
assertEquals(1, sender.messages.size());
|
||||
assertTrue(sender.messages.getFirst().contains("Hotloading Studio"));
|
||||
if (failure == null) {
|
||||
pending.complete(null);
|
||||
} else {
|
||||
pending.completeExceptionally(failure);
|
||||
iris.verify(() -> Iris.reportError(failure));
|
||||
}
|
||||
assertEquals(2, sender.messages.size());
|
||||
assertTrue(sender.messages.getLast(), sender.messages.getLast().contains(expected));
|
||||
assertEquals(List.of(audience, audience), sender.audiences);
|
||||
assertNull(LanguageAudience.current());
|
||||
}
|
||||
}
|
||||
|
||||
private static final class TestCommand extends CommandStudio {
|
||||
private final VolmitSender sender;
|
||||
|
||||
private TestCommand(VolmitSender sender) {
|
||||
this.sender = sender;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VolmitSender sender() {
|
||||
return sender;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class RecordingSender extends VolmitSender {
|
||||
private final List<String> messages = new ArrayList<>();
|
||||
private final List<UUID> audiences = new ArrayList<>();
|
||||
|
||||
private RecordingSender() {
|
||||
super(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessage(String message) {
|
||||
messages.add(message);
|
||||
audiences.add(LanguageAudience.current());
|
||||
}
|
||||
}
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
package art.arcane.iris.command;
|
||||
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
|
||||
import art.arcane.iris.studio.map.MapWindow;
|
||||
import art.arcane.iris.studio.map.PaletteExplorerStart;
|
||||
import art.arcane.iris.studio.view.GuiHost;
|
||||
import art.arcane.iris.world.IrisToolbelt;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.same;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class StudioMapPositionTest {
|
||||
@Test
|
||||
public void mapCapturesItsStartingPositionOnThePlayerScheduler() {
|
||||
IrisSettings previous = IrisSettings.settings;
|
||||
IrisSettings.settings = new IrisSettings();
|
||||
IrisSettings.settings.getGui().setUseServerLaunchedGuis(true);
|
||||
World world = mock(World.class);
|
||||
Player player = mock(Player.class);
|
||||
VolmitSender sender = mock(VolmitSender.class);
|
||||
PlatformChunkGenerator generator = mock(PlatformChunkGenerator.class);
|
||||
Engine engine = mock(Engine.class);
|
||||
UUID playerId = UUID.randomUUID();
|
||||
UUID worldId = UUID.randomUUID();
|
||||
AtomicBoolean owned = new AtomicBoolean();
|
||||
AtomicReference<Runnable> task = new AtomicReference<>();
|
||||
AtomicReference<PaletteExplorerStart> start = new AtomicReference<>();
|
||||
when(world.getUID()).thenReturn(worldId);
|
||||
when(player.getUniqueId()).thenReturn(playerId);
|
||||
when(player.isOnline()).thenReturn(true);
|
||||
when(player.getWorld()).thenAnswer(invocation -> {
|
||||
assertTrue("world read must use the player scheduler", owned.get());
|
||||
return world;
|
||||
});
|
||||
when(player.getLocation()).thenAnswer(invocation -> {
|
||||
assertTrue("location read must use the player scheduler", owned.get());
|
||||
return new Location(world, 33.25D, 91D, -72.5D);
|
||||
});
|
||||
when(sender.isPlayer()).thenReturn(true);
|
||||
when(sender.player()).thenReturn(player);
|
||||
when(generator.getEngine()).thenReturn(engine);
|
||||
CommandStudio command = new CommandStudio() {
|
||||
@Override
|
||||
public VolmitSender sender() {
|
||||
return sender;
|
||||
}
|
||||
};
|
||||
try (MockedStatic<GuiHost> host = mockStatic(GuiHost.class);
|
||||
MockedStatic<IrisToolbelt> toolbelt = mockStatic(IrisToolbelt.class);
|
||||
MockedStatic<J> scheduling = mockStatic(J.class);
|
||||
MockedStatic<MapWindow> windows = mockStatic(MapWindow.class)) {
|
||||
host.when(GuiHost::isAvailable).thenReturn(true);
|
||||
toolbelt.when(() -> IrisToolbelt.isIrisWorld(world)).thenReturn(true);
|
||||
toolbelt.when(() -> IrisToolbelt.access(world)).thenReturn(generator);
|
||||
scheduling.when(() -> J.runEntity(same(player), any(Runnable.class), eq(0), any(Runnable.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
task.set(invocation.getArgument(1));
|
||||
return true;
|
||||
});
|
||||
windows.when(() -> MapWindow.openMap(any(PaletteExplorerStart.class))).thenAnswer(invocation -> {
|
||||
start.set(invocation.getArgument(0));
|
||||
return null;
|
||||
});
|
||||
|
||||
command.map(world);
|
||||
assertNull(start.get());
|
||||
owned.set(true);
|
||||
task.get().run();
|
||||
|
||||
assertEquals(engine, start.get().engine());
|
||||
assertEquals(playerId, start.get().playerId());
|
||||
assertEquals(worldId, start.get().worldId());
|
||||
assertEquals(33.25D, start.get().centerX(), 0D);
|
||||
assertEquals(-72.5D, start.get().centerZ(), 0D);
|
||||
} finally {
|
||||
IrisSettings.settings = previous;
|
||||
}
|
||||
}
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
package art.arcane.iris.platform.agent;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.foreign.Arena;
|
||||
import java.lang.foreign.FunctionDescriptor;
|
||||
import java.lang.foreign.Linker;
|
||||
import java.lang.foreign.MemorySegment;
|
||||
import java.lang.foreign.ValueLayout;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assume.assumeFalse;
|
||||
|
||||
public class AttachSignalTest {
|
||||
private static final int SIGQUIT = 3;
|
||||
private static final int SIGTERM = 15;
|
||||
private static final long ACTION_BYTES = 256;
|
||||
private static final MethodHandle SIGACTION = Linker.nativeLinker().downcallHandle(
|
||||
Linker.nativeLinker().defaultLookup().find("sigaction").orElseThrow(),
|
||||
FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS));
|
||||
|
||||
@Test
|
||||
public void aConsoleThatResetsQuitGetsTheJvmHandlerBack() throws Throwable {
|
||||
assumeFalse(System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("win"));
|
||||
long jvmHandler = handler(SIGTERM);
|
||||
assertNotEquals(0L, jvmHandler);
|
||||
try (Arena arena = Arena.ofConfined()) {
|
||||
MemorySegment original = arena.allocate(ACTION_BYTES);
|
||||
assertEquals(0, (int) SIGACTION.invokeExact(SIGQUIT, MemorySegment.NULL, original));
|
||||
try {
|
||||
MemorySegment reset = arena.allocate(ACTION_BYTES);
|
||||
assertEquals(0, (int) SIGACTION.invokeExact(SIGQUIT, reset, MemorySegment.NULL));
|
||||
assertEquals(0L, handler(SIGQUIT));
|
||||
|
||||
assertTrue(AttachSignal.restoreJvmHandler());
|
||||
|
||||
assertEquals(jvmHandler, handler(SIGQUIT));
|
||||
assertFalse(AttachSignal.restoreJvmHandler());
|
||||
} finally {
|
||||
assertEquals(0, (int) SIGACTION.invokeExact(SIGQUIT, original, MemorySegment.NULL));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static long handler(int signal) throws Throwable {
|
||||
try (Arena arena = Arena.ofConfined()) {
|
||||
MemorySegment action = arena.allocate(ACTION_BYTES);
|
||||
assertEquals(0, (int) SIGACTION.invokeExact(signal, MemorySegment.NULL, action));
|
||||
return action.get(ValueLayout.JAVA_LONG, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
+60
-45
@@ -9,16 +9,18 @@ import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.runtime.BiomeEnvironment;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.generation.terrain.InferredType;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.world.history.SavedBiomeUnavailableException;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyColumnLayer;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyColumnSample;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyFeatureRef;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyFeatureType;
|
||||
import art.arcane.volmlib.util.stream.ProceduralStream;
|
||||
import art.arcane.volmlib.util.stream.interpolation.Interpolated;
|
||||
import art.arcane.iris.api.terrain.IrisColumnSample;
|
||||
import art.arcane.iris.generation.hydrology.raster.WaterRole;
|
||||
import art.arcane.iris.platform.bukkit.plugin.IrisService;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.generator.ChunkGenerator;
|
||||
@@ -26,6 +28,7 @@ import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
@@ -112,10 +115,32 @@ public class IrisTerrainSVCTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptedHydrologyMapsToThePublicDiagnosticStates() {
|
||||
assertEquals(IrisRiverState.NONE, IrisTerrainSVC.riverState(null));
|
||||
assertEquals(IrisRiverState.WET, IrisTerrainSVC.riverState(river(true)));
|
||||
assertEquals(IrisRiverState.DRY, IrisTerrainSVC.riverState(river(false)));
|
||||
public void waterRolesMapToThePublicRiverFields() {
|
||||
for (WaterRole role : WaterRole.values()) {
|
||||
assertEquals(role.name(), role.wet() ? IrisRiverState.WET : IrisRiverState.NONE, IrisTerrainSVC.riverState(role));
|
||||
double distance = switch (role) {
|
||||
case NONE -> IrisColumnSample.UNAVAILABLE_RIVER_DISTANCE;
|
||||
case CHANNEL, POND, MOUTH, CASCADE, WATERFALL, INLET -> 0D;
|
||||
case SHORE, POND_SHORE -> 1D;
|
||||
case BANK, DAM -> 2D;
|
||||
};
|
||||
assertEquals(role.name(), distance, IrisTerrainSVC.riverDistance(role), 0D);
|
||||
int flow = switch (role) {
|
||||
case NONE, SHORE, POND_SHORE, BANK, DAM -> IrisColumnSample.UNAVAILABLE_RIVER_FLOW;
|
||||
case CHANNEL, MOUTH, CASCADE, WATERFALL -> 1;
|
||||
case POND, INLET -> 0;
|
||||
};
|
||||
assertEquals(role.name(), flow, IrisTerrainSVC.riverFlow(role));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void columnSamplesReportTheWaterRoleOfEachColumn() throws Exception {
|
||||
assertEquals(List.of("RIVER", "WET", "0.0", "1", "6"), sampled(WaterRole.CHANNEL, 70));
|
||||
assertEquals(List.of("RIVER", "WET", "0.0", "0", "6"), sampled(WaterRole.POND, 70));
|
||||
assertEquals(List.of("RIVER_SHORE", "NONE", "1.0", "-1", "unavailable"), sampled(WaterRole.SHORE, 50));
|
||||
assertEquals(List.of("LAND", "NONE", "2.0", "-1", "unavailable"), sampled(WaterRole.BANK, 50));
|
||||
assertEquals(List.of("LAND", "NONE", "NaN", "-1", "unavailable"), sampled(WaterRole.NONE, 50));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -219,6 +244,33 @@ public class IrisTerrainSVCTest {
|
||||
}
|
||||
}
|
||||
|
||||
private static List<String> sampled(WaterRole role, int head) throws Exception {
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
IrisBiome biome = mock(IrisBiome.class);
|
||||
when(biome.getInferredType()).thenReturn(InferredType.LAND);
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
when(engine.getDimension()).thenReturn(mock(IrisDimension.class));
|
||||
when(engine.getHeight(0, 0)).thenReturn(60);
|
||||
when(engine.getMinHeight()).thenReturn(-64);
|
||||
when(engine.getSurfaceBiome(0, 0)).thenReturn(biome);
|
||||
when(complex.getWaterRoleStream()).thenReturn(ProceduralStream.of((x, z) -> role, Interpolated.of(value -> 0D, value -> role)));
|
||||
when(complex.getWaterHeadStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> (double) head));
|
||||
IrisColumnQuery query = IrisColumnQuery.rect(0, 0, 0, 0, 1, EnumSet.of(IrisColumnField.SURFACE_KIND,
|
||||
IrisColumnField.RIVER_STATE, IrisColumnField.RIVER_DISTANCE, IrisColumnField.RIVER_FLOW,
|
||||
IrisColumnField.RIVER_WATER_SURFACE_Y));
|
||||
List<IrisColumnSample> samples = new ArrayList<>();
|
||||
try (MockedStatic<IrisSettings> settings = mockStatic(IrisSettings.class)) {
|
||||
settings.when(IrisSettings::get).thenReturn(new IrisSettings());
|
||||
assertTrue(enabledService().sampleColumns(world(engine), query, samples::add));
|
||||
}
|
||||
assertEquals(1, samples.size());
|
||||
IrisColumnSample sample = samples.get(0);
|
||||
return List.of(sample.surfaceKind().name(), sample.riverState().name(), String.valueOf(sample.riverDistance()),
|
||||
String.valueOf(sample.riverFlow()),
|
||||
sample.riverWaterSurfaceY() == IrisColumnSample.UNAVAILABLE_HEIGHT ? "unavailable" : String.valueOf(sample.riverWaterSurfaceY()));
|
||||
}
|
||||
|
||||
private static World world(Engine engine) {
|
||||
World world = mock(World.class);
|
||||
ChunkGenerator generator = mock(ChunkGenerator.class, withSettings().extraInterfaces(PlatformChunkGenerator.class));
|
||||
@@ -235,41 +287,4 @@ public class IrisTerrainSVCTest {
|
||||
((AtomicBoolean) enabled.get(service)).set(true);
|
||||
return service;
|
||||
}
|
||||
|
||||
private static HydrologyColumnSample river(boolean connectedFluid) {
|
||||
HydrologyFeatureRef feature = new HydrologyFeatureRef(
|
||||
1L,
|
||||
HydrologyFeatureType.SURFACE_POOL,
|
||||
2L,
|
||||
3L,
|
||||
0,
|
||||
63,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
false
|
||||
);
|
||||
HydrologyColumnLayer layer = new HydrologyColumnLayer(
|
||||
feature,
|
||||
60,
|
||||
63,
|
||||
63,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
connectedFluid,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
connectedFluid,
|
||||
false,
|
||||
"water",
|
||||
"river",
|
||||
"mouth",
|
||||
"shore",
|
||||
"dry",
|
||||
"cave"
|
||||
);
|
||||
return new HydrologyColumnSample(0, 0, 70, 63, false, "parent", List.of(layer));
|
||||
}
|
||||
}
|
||||
+18
-93
@@ -1,15 +1,10 @@
|
||||
package art.arcane.iris.platform.bukkit.terrain;
|
||||
|
||||
import art.arcane.iris.api.terrain.IrisSurfaceKind;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyColumnLayer;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyColumnSample;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyFeatureRef;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyFeatureType;
|
||||
import art.arcane.iris.generation.hydrology.raster.WaterRole;
|
||||
import art.arcane.iris.generation.terrain.InferredType;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
@@ -69,98 +64,28 @@ public class IrisSurfaceClassifierTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptedSurfaceRolesOverrideGenericOceanAndLandKinds() {
|
||||
assertEquals(IrisSurfaceKind.RIVER, IrisSurfaceClassifier.classify(
|
||||
60,
|
||||
63,
|
||||
InferredType.SEA,
|
||||
hydrology(HydrologyFeatureType.SURFACE_POOL, true, false, true)
|
||||
));
|
||||
assertEquals(IrisSurfaceKind.RIVER, IrisSurfaceClassifier.classify(
|
||||
60,
|
||||
63,
|
||||
InferredType.SEA,
|
||||
hydrology(HydrologyFeatureType.MOUTH, true, false, true)
|
||||
));
|
||||
assertEquals(IrisSurfaceKind.RIVER_SHORE, IrisSurfaceClassifier.classify(
|
||||
64,
|
||||
63,
|
||||
InferredType.SHORE,
|
||||
hydrology(HydrologyFeatureType.SURFACE_POOL, false, true, false)
|
||||
));
|
||||
assertEquals(IrisSurfaceKind.DRY_CHANNEL, IrisSurfaceClassifier.classify(
|
||||
60,
|
||||
60,
|
||||
InferredType.LAND,
|
||||
hydrology(HydrologyFeatureType.SURFACE_POOL, true, false, false)
|
||||
));
|
||||
assertEquals(IrisSurfaceKind.LAND, IrisSurfaceClassifier.classify(
|
||||
60,
|
||||
60,
|
||||
InferredType.LAND,
|
||||
hydrology(HydrologyFeatureType.SURFACE_POOL, false, false, false)
|
||||
));
|
||||
public void waterRolesOverrideGenericOceanAndLandKinds() {
|
||||
for (WaterRole role : WaterRole.values()) {
|
||||
IrisSurfaceKind expected = switch (role) {
|
||||
case NONE -> IrisSurfaceKind.OCEAN;
|
||||
case SHORE, POND_SHORE -> IrisSurfaceKind.RIVER_SHORE;
|
||||
case BANK, DAM -> IrisSurfaceKind.LAND;
|
||||
case CHANNEL, POND, MOUTH, CASCADE, WATERFALL, INLET -> IrisSurfaceKind.RIVER;
|
||||
};
|
||||
assertEquals(role.name(), expected, IrisSurfaceClassifier.classify(60, 63, InferredType.SEA, role));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void voidClassificationWinsOverAcceptedRiverGeometry() {
|
||||
assertEquals(IrisSurfaceKind.VOID, IrisSurfaceClassifier.classify(
|
||||
0,
|
||||
63,
|
||||
InferredType.LAND,
|
||||
hydrology(HydrologyFeatureType.SURFACE_POOL, true, false, true)
|
||||
));
|
||||
public void voidClassificationWinsOverWaterRoles() {
|
||||
for (WaterRole role : WaterRole.values()) {
|
||||
assertEquals(role.name(), IrisSurfaceKind.VOID, IrisSurfaceClassifier.classify(0, 63, InferredType.LAND, role));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void absentRejectedCandidatesDoNotCreatePublicRiverKinds() {
|
||||
assertEquals(IrisSurfaceKind.LAND, IrisSurfaceClassifier.classify(
|
||||
64,
|
||||
63,
|
||||
InferredType.LAND,
|
||||
null
|
||||
));
|
||||
}
|
||||
|
||||
private static HydrologyColumnSample hydrology(
|
||||
HydrologyFeatureType type,
|
||||
boolean channel,
|
||||
boolean shore,
|
||||
boolean connectedFluid
|
||||
) {
|
||||
HydrologyFeatureRef feature = new HydrologyFeatureRef(
|
||||
1L,
|
||||
type,
|
||||
2L,
|
||||
3L,
|
||||
0,
|
||||
63,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
false
|
||||
);
|
||||
HydrologyColumnLayer layer = new HydrologyColumnLayer(
|
||||
feature,
|
||||
60,
|
||||
63,
|
||||
63,
|
||||
channel,
|
||||
shore,
|
||||
!channel && !shore,
|
||||
connectedFluid,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
connectedFluid && channel,
|
||||
false,
|
||||
"water",
|
||||
"river",
|
||||
"mouth",
|
||||
"shore",
|
||||
"dry",
|
||||
"cave"
|
||||
);
|
||||
return new HydrologyColumnSample(0, 0, 70, 63, false, "parent", List.of(layer));
|
||||
public void columnsWithoutAWaterRoleKeepTheGenericKinds() {
|
||||
assertEquals(IrisSurfaceKind.LAND, IrisSurfaceClassifier.classify(64, 63, InferredType.LAND, WaterRole.NONE));
|
||||
assertEquals(IrisSurfaceKind.SHORE, IrisSurfaceClassifier.classify(64, 63, InferredType.SHORE, WaterRole.NONE));
|
||||
}
|
||||
}
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
package art.arcane.iris.studio.view;
|
||||
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
import art.arcane.iris.world.IrisWorld;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.same;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class BukkitOpenerPositionTest {
|
||||
@Test
|
||||
public void playerMarkersAreCapturedOnTheirEntitySchedulers() {
|
||||
try (Harness harness = new Harness()) {
|
||||
harness.binding.when(() -> BukkitWorldBinding.players(harness.target)).thenReturn(List.of(harness.player));
|
||||
when(harness.player.getName()).thenReturn("Explorer");
|
||||
assertTrue(harness.overlay.players().isEmpty());
|
||||
|
||||
harness.runEntity();
|
||||
|
||||
List<GuiMarker> markers = harness.overlay.players();
|
||||
assertEquals(1, markers.size());
|
||||
assertEquals("Explorer", markers.getFirst().label());
|
||||
assertEquals(18.5D, markers.getFirst().worldX(), 0D);
|
||||
assertEquals(-4.25D, markers.getFirst().worldZ(), 0D);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void positionIsCapturedOnlyOnTheOwningEntityTask() {
|
||||
try (Harness harness = new Harness()) {
|
||||
harness.overlay.requestOpenerPosition(harness.results::add);
|
||||
assertTrue(harness.results.isEmpty());
|
||||
|
||||
harness.runEntity();
|
||||
|
||||
assertEquals(List.of(OpenerPosition.inWorld(18.5D, 72D, -4.25D)), harness.results);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void retiredEntityCompletesTheRequestOnce() {
|
||||
try (Harness harness = new Harness()) {
|
||||
harness.overlay.requestOpenerPosition(harness.results::add);
|
||||
assertTrue("retirement callback must be registered", harness.retired != null);
|
||||
|
||||
harness.retired.run();
|
||||
harness.retired.run();
|
||||
|
||||
assertEquals(List.of(OpenerPosition.OFFLINE), harness.results);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void disconnectBeforeCaptureCompletesAsOfflineAndReconnectCanRefresh() {
|
||||
try (Harness harness = new Harness()) {
|
||||
harness.overlay.requestOpenerPosition(harness.results::add);
|
||||
when(harness.player.isOnline()).thenReturn(false);
|
||||
harness.runEntity();
|
||||
assertEquals(List.of(OpenerPosition.OFFLINE), harness.results);
|
||||
|
||||
when(harness.player.isOnline()).thenReturn(true);
|
||||
harness.overlay.requestOpenerPosition(harness.results::add);
|
||||
harness.runEntity();
|
||||
assertEquals(List.of(OpenerPosition.OFFLINE, OpenerPosition.inWorld(18.5D, 72D, -4.25D)), harness.results);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectedEntityTaskCompletesOnceEvenIfRetirementAlreadyRan() {
|
||||
try (Harness harness = new Harness()) {
|
||||
harness.reject = true;
|
||||
harness.overlay.requestOpenerPosition(harness.results::add);
|
||||
assertEquals(List.of(OpenerPosition.OFFLINE), harness.results);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void changingWorldBeforeCaptureReportsOtherWorld() {
|
||||
try (Harness harness = new Harness()) {
|
||||
harness.overlay.requestOpenerPosition(harness.results::add);
|
||||
when(harness.player.getWorld()).thenReturn(mock(World.class));
|
||||
harness.runEntity();
|
||||
assertEquals(List.of(OpenerPosition.IN_OTHER_WORLD), harness.results);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Harness implements AutoCloseable {
|
||||
private final Player player = mock(Player.class);
|
||||
private final World world = mock(World.class);
|
||||
private final Engine engine = mock(Engine.class);
|
||||
private final IrisWorld target = mock(IrisWorld.class);
|
||||
private final UUID opener = UUID.randomUUID();
|
||||
private final List<OpenerPosition> results = new ArrayList<>();
|
||||
private final MockedStatic<J> scheduling = mockStatic(J.class);
|
||||
private final MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
|
||||
private final MockedStatic<BukkitWorldBinding> binding = mockStatic(BukkitWorldBinding.class);
|
||||
private final BukkitVisionOverlay overlay = new BukkitVisionOverlay(engine, opener);
|
||||
private Runnable task;
|
||||
private Runnable retired;
|
||||
private boolean entityOwned;
|
||||
private boolean reject;
|
||||
|
||||
private Harness() {
|
||||
when(engine.getWorld()).thenReturn(target);
|
||||
when(player.isOnline()).thenReturn(true);
|
||||
when(player.getWorld()).thenReturn(world);
|
||||
when(player.getLocation()).thenAnswer(invocation -> {
|
||||
assertTrue("location read outside owning entity task", entityOwned);
|
||||
return new Location(world, 18.5D, 72D, -4.25D);
|
||||
});
|
||||
bukkit.when(() -> Bukkit.getPlayer(opener)).thenReturn(player);
|
||||
binding.when(() -> BukkitWorldBinding.world(target)).thenReturn(world);
|
||||
scheduling.when(() -> J.runGlobal(any(Runnable.class))).thenAnswer(invocation -> {
|
||||
invocation.getArgument(0, Runnable.class).run();
|
||||
return true;
|
||||
});
|
||||
scheduling.when(() -> J.runEntity(same(player), any(Runnable.class))).thenAnswer(invocation -> {
|
||||
task = invocation.getArgument(1);
|
||||
return !reject;
|
||||
});
|
||||
scheduling.when(() -> J.runEntity(same(player), any(Runnable.class), eq(0), any(Runnable.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
task = invocation.getArgument(1);
|
||||
retired = invocation.getArgument(3);
|
||||
if (reject) {
|
||||
retired.run();
|
||||
}
|
||||
return !reject;
|
||||
});
|
||||
}
|
||||
|
||||
private void runEntity() {
|
||||
entityOwned = true;
|
||||
try {
|
||||
task.run();
|
||||
} finally {
|
||||
entityOwned = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
binding.close();
|
||||
bukkit.close();
|
||||
scheduling.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
+55
@@ -11,12 +11,16 @@ import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
@@ -42,4 +46,55 @@ public class PendingWorldReplacementAdmissionTest {
|
||||
executor.awaitTermination(5L, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shutdownRejectsReplacementsThatStartAfterIt() {
|
||||
PendingWorldReplacementManager manager = new PendingWorldReplacementManager(mock(Iris.class));
|
||||
manager.awaitStagingForShutdown();
|
||||
|
||||
IOException rejected = assertThrows(IOException.class, () -> manager.stageReplacement(
|
||||
mock(VolmitSender.class), NamespacedKey.minecraft("overworld"), mock(IrisDimension.class), null));
|
||||
|
||||
assertTrue(rejected.getMessage().contains("shutting down"));
|
||||
assertTrue(LifecycleOperationCoordinator.get().isIdle());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shutdownWaitsForTheReplacementBeingStaged() throws Exception {
|
||||
PendingWorldReplacementManager manager = new PendingWorldReplacementManager(mock(Iris.class));
|
||||
AtomicReference<Throwable> stagingFailure = new AtomicReference<>();
|
||||
Thread staging = new Thread(() -> {
|
||||
try {
|
||||
manager.stageReplacement(mock(VolmitSender.class), NamespacedKey.minecraft("overworld"),
|
||||
mock(IrisDimension.class), null);
|
||||
} catch (Throwable failure) {
|
||||
stagingFailure.set(failure);
|
||||
}
|
||||
});
|
||||
Thread shutdown = new Thread(manager::awaitStagingForShutdown);
|
||||
synchronized (manager) {
|
||||
staging.start();
|
||||
awaitState(staging, Thread.State.BLOCKED);
|
||||
shutdown.start();
|
||||
awaitState(shutdown, Thread.State.WAITING);
|
||||
assertTrue(staging.isAlive());
|
||||
}
|
||||
shutdown.join(TimeUnit.SECONDS.toMillis(10L));
|
||||
staging.join(TimeUnit.SECONDS.toMillis(10L));
|
||||
|
||||
assertFalse(shutdown.isAlive());
|
||||
assertFalse(staging.isAlive());
|
||||
assertTrue(stagingFailure.get() instanceof Exception);
|
||||
assertTrue(LifecycleOperationCoordinator.get().isIdle());
|
||||
}
|
||||
|
||||
private static void awaitState(Thread thread, Thread.State state) throws InterruptedException {
|
||||
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5L);
|
||||
while (thread.getState() != state) {
|
||||
if (System.nanoTime() > deadline) {
|
||||
throw new AssertionError(thread.getName() + " never reached " + state + "; it is " + thread.getState());
|
||||
}
|
||||
Thread.sleep(5L);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,7 +62,7 @@ apply from: file('../../gradle/java21-runtime-exclusions.gradle')
|
||||
|
||||
Properties rootProperties = new Properties()
|
||||
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
|
||||
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.3.5'))
|
||||
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.3.6'))
|
||||
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.3'))
|
||||
String fabricLoaderVersion = providers.gradleProperty('fabricLoaderVersion').getOrElse(irisModTarget.fabricLoader.toString())
|
||||
Closure<String> irisArtifactName = { String platform, String targetVersion ->
|
||||
@@ -131,6 +131,8 @@ configurations.named('bundle').configure {
|
||||
exclude(group: 'de.crazydev22.slimjar.helper')
|
||||
exclude(group: 'de.crazydev22.slimjar')
|
||||
exclude(group: 'io.github.slimjar')
|
||||
// The game ships its own LWJGL; without the modules it leaves out, the map's 3D views draw in software.
|
||||
exclude(group: 'org.lwjgl')
|
||||
}
|
||||
|
||||
configurations.compileClasspath.extendsFrom(configurations.devBundle)
|
||||
@@ -153,7 +155,7 @@ dependencies {
|
||||
String modConfiguration = irisModTarget.fabricRemap ? 'modImplementation' : 'implementation'
|
||||
add(modConfiguration, "net.fabricmc:fabric-loader:${fabricLoaderVersion}")
|
||||
testImplementation('junit:junit:4.13.2')
|
||||
testImplementation('org.mockito:mockito-core:5.23.0')
|
||||
testImplementation(libs.mockito.core)
|
||||
testImplementation(testFixtures('art.arcane:core'))
|
||||
testImplementation(libs.spigot) {
|
||||
transitive = false
|
||||
@@ -216,7 +218,9 @@ dependencies {
|
||||
libs.caffeine,
|
||||
'com.fulcrumgenomics:jlibdeflate:0.2.0',
|
||||
libs.dom4j,
|
||||
libs.jaxen
|
||||
libs.jaxen,
|
||||
libs.flatlaf,
|
||||
libs.flatlaf.extras
|
||||
]
|
||||
shared.each { Object notation ->
|
||||
['bundle', 'devBundle'].each { String configuration ->
|
||||
@@ -236,6 +240,8 @@ tasks.named('compileJava', JavaCompile).configure {
|
||||
options.release.set((int) irisModTarget.java)
|
||||
}
|
||||
|
||||
apply from: file('../../gradle/mockito-agent.gradle')
|
||||
|
||||
tasks.named('test').configure {
|
||||
}
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ apply from: file('../../gradle/java21-runtime-exclusions.gradle')
|
||||
|
||||
Properties rootProperties = new Properties()
|
||||
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
|
||||
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.3.5'))
|
||||
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.3.6'))
|
||||
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.3'))
|
||||
String forgeVersion = providers.gradleProperty('forgeVersion').getOrElse(irisModTarget.forge.toString())
|
||||
String buildLane = gradle.extensions.extraProperties.has('irisBuildLane')
|
||||
@@ -152,12 +152,16 @@ configurations.named('bundle').configure {
|
||||
exclude(group: 'de.crazydev22.slimjar.helper')
|
||||
exclude(group: 'de.crazydev22.slimjar')
|
||||
exclude(group: 'io.github.slimjar')
|
||||
// The game ships its own LWJGL; without the modules it leaves out, the map's 3D views draw in software.
|
||||
exclude(group: 'org.lwjgl')
|
||||
}
|
||||
|
||||
configurations.named('runBundle').configure {
|
||||
exclude(group: 'de.crazydev22.slimjar.helper')
|
||||
exclude(group: 'de.crazydev22.slimjar')
|
||||
exclude(group: 'io.github.slimjar')
|
||||
// The game ships its own LWJGL; without the modules it leaves out, the map's 3D views draw in software.
|
||||
exclude(group: 'org.lwjgl')
|
||||
}
|
||||
|
||||
configurations.compileClasspath.extendsFrom(configurations.devBundle)
|
||||
@@ -178,7 +182,7 @@ dependencies {
|
||||
? minecraft.dependency("net.minecraftforge:forge:${forgeVersion}")
|
||||
: minecraft.dependency("iris-${buildLane}", "net.minecraftforge:forge:${forgeVersion}"))
|
||||
testImplementation('junit:junit:4.13.2')
|
||||
testImplementation('org.mockito:mockito-core:5.23.0')
|
||||
testImplementation(libs.mockito.core)
|
||||
testImplementation(testFixtures('art.arcane:core'))
|
||||
testImplementation(libs.spigot) {
|
||||
transitive = false
|
||||
@@ -215,7 +219,9 @@ dependencies {
|
||||
libs.caffeine,
|
||||
'com.fulcrumgenomics:jlibdeflate:0.2.0',
|
||||
libs.dom4j,
|
||||
libs.jaxen
|
||||
libs.jaxen,
|
||||
libs.flatlaf,
|
||||
libs.flatlaf.extras
|
||||
]
|
||||
shared.each { Object notation ->
|
||||
['bundle', 'devBundle', 'runBundle'].each { String configuration ->
|
||||
@@ -272,6 +278,8 @@ tasks.named('compileJava', JavaCompile).configure {
|
||||
options.release.set((int) irisModTarget.java)
|
||||
}
|
||||
|
||||
apply from: file('../../gradle/mockito-agent.gradle')
|
||||
|
||||
tasks.named('test').configure {
|
||||
}
|
||||
|
||||
|
||||
@@ -227,6 +227,7 @@ public final class ModdedEngineBootstrap {
|
||||
failure = runStopStage(failure, "wand service", ModdedWandService::clearAll);
|
||||
failure = runStopStage(failure, "block break handler", ModdedBlockBreakHandler::clear);
|
||||
failure = runStopStage(failure, "studio commands", ModdedStudioCommands::clear);
|
||||
failure = runStopStage(failure, "gui windows", GuiHost::shutdown);
|
||||
failure = runStopStage(failure, "gui host", ModdedGuiHost::clear);
|
||||
failure = runStopStage(failure, "services", () -> services().disableAll());
|
||||
reportStopFailure("stopping", failure);
|
||||
|
||||
+1
@@ -150,6 +150,7 @@ final class ModdedCommandHelp {
|
||||
Entry.command("close", "", ModdedHelpMessages.COMMAND_CLOSE_CLOSE_THE_OPEN_STUDIO_DIMENSION_AND_DISCARD_ITS_WORLD, "x"),
|
||||
Entry.command("tpstudio", "", ModdedHelpMessages.COMMAND_TPSTUDIO_TELEPORT_INTO_THE_OPEN_STUDIO_DIMENSION, "stp"),
|
||||
Entry.command("status", "", ModdedHelpMessages.COMMAND_STATUS_SHOW_THE_OPEN_STUDIO_DIMENSION_AND_ITS_PACK),
|
||||
Entry.command("hotload", "", ModdedHelpMessages.COMMAND_HOTLOAD_REFRESH_THE_OPEN_STUDIO, "refresh"),
|
||||
Entry.command("noise", "[generator] [seed]", ModdedHelpMessages.COMMAND_NOISE_OPEN_THE_NOISE_EXPLORER_GUI_ON_THE_SERVER_DISPLAY, "nmap"),
|
||||
Entry.command("map", "", ModdedHelpMessages.COMMAND_MAP_OPEN_THE_VISION_MAP_GUI_ON_THE_SERVER_DISPLAY, "render"),
|
||||
Entry.command("vscode", "[pack]", ModdedHelpMessages.COMMAND_VSCODE_REGENERATE_THE_CODE_WORKSPACE_FOR_A_PACK_AND_OPEN_IT_IN_YOUR, "vsc"),
|
||||
|
||||
+2
-20
@@ -25,7 +25,7 @@ import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.structure.placement.IrisStructureLocator;
|
||||
import art.arcane.iris.structure.nativegen.NativeStructureGenerationPolicy;
|
||||
import art.arcane.iris.structure.placement.StructureReachability;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyFeatureQuery;
|
||||
import art.arcane.iris.generation.hydrology.query.HydrologyFeatureType;
|
||||
import art.arcane.iris.world.history.GenerationFindCatalog;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.structure.nativegen.IrisNativeStructureDecision;
|
||||
@@ -61,7 +61,7 @@ final class ModdedCommandSuggestions {
|
||||
static final SuggestionProvider<NativeCommandSource> OBJECT_KEYS = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> suggestObjectKeys(context, builder);
|
||||
static final SuggestionProvider<NativeCommandSource> STRUCTURE_KEYS = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> suggestStructureKeys(context, builder);
|
||||
static final SuggestionProvider<NativeCommandSource> POI_TYPES = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> NativeCommandRegistration.suggest(List.of("buried_treasure"), builder);
|
||||
static final SuggestionProvider<NativeCommandSource> HYDROLOGY_TYPES = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> suggestHydrologyTypes(context, builder);
|
||||
static final SuggestionProvider<NativeCommandSource> HYDROLOGY_TYPES = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> NativeCommandRegistration.suggest(HydrologyFeatureType.keys(), builder);
|
||||
static final SuggestionProvider<NativeCommandSource> PACK_NAMES = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> suggestPackNames(context, builder);
|
||||
static final SuggestionProvider<NativeCommandSource> DIMENSION_NAMES = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> suggestDimensionNames(context, builder);
|
||||
|
||||
@@ -87,24 +87,6 @@ final class ModdedCommandSuggestions {
|
||||
return builder.buildFuture();
|
||||
}
|
||||
|
||||
private static CompletableFuture<Suggestions> suggestHydrologyTypes(
|
||||
CommandContext<NativeCommandSource> context,
|
||||
SuggestionsBuilder builder
|
||||
) {
|
||||
ModdedCommandFeedback.tab(context.getSource());
|
||||
try {
|
||||
Engine engine = IrisModdedCommands.engineFor(context.getSource().world());
|
||||
if (engine != null && engine.getComplex().getHydrologyRuntime() != null) {
|
||||
return NativeCommandRegistration.suggest(
|
||||
engine.getComplex().getHydrologyRuntime().featureQueryKeys(), builder);
|
||||
}
|
||||
} catch (Throwable error) {
|
||||
warnTabFailure("hydrology types", context.getSource(), error);
|
||||
}
|
||||
return NativeCommandRegistration.suggest(
|
||||
HydrologyFeatureQuery.suggestions(List.of()), builder);
|
||||
}
|
||||
|
||||
static Set<String> reachableBiomeKeys(Engine engine) {
|
||||
return reachableBiomeKeys(GenerationFindCatalog.biomes(engine));
|
||||
}
|
||||
|
||||
+13
-22
@@ -39,8 +39,7 @@ import art.arcane.iris.structure.placement.StructureReachability;
|
||||
import art.arcane.iris.generation.runtime.WrongEngineBroException;
|
||||
import art.arcane.iris.world.history.GenerationSemanticQueries;
|
||||
import art.arcane.iris.world.history.GenerationFindCatalog;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyFeatureQuery;
|
||||
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
|
||||
import art.arcane.iris.generation.hydrology.query.HydrologyFeatureType;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.structure.nativegen.IrisNativeStructureDecision;
|
||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||
@@ -143,39 +142,31 @@ final class ModdedLocateCommands {
|
||||
IrisModdedCommands.fail(source, "This dimension is not generated by Iris.");
|
||||
return 0;
|
||||
}
|
||||
IrisHydrologyRuntime runtime = engine.getComplex().getHydrologyRuntime();
|
||||
HydrologyFeatureQuery query;
|
||||
HydrologyFeatureType featureType;
|
||||
try {
|
||||
query = HydrologyFeatureQuery.parse(type);
|
||||
featureType = HydrologyFeatureType.parse(type);
|
||||
} catch (IllegalArgumentException error) {
|
||||
IrisModdedCommands.fail(source, error.getMessage());
|
||||
return 0;
|
||||
}
|
||||
String label = featureType.key().replace('_', ' ');
|
||||
int originX = player.blockX();
|
||||
int originZ = player.blockZ();
|
||||
int requestedDistance = runtime == null
|
||||
? 8192
|
||||
: Math.min(8192, runtime.settings().routing().tileSize() * 15);
|
||||
int maximumDistance = runtime == null
|
||||
? requestedDistance
|
||||
: runtime.maximumFeatureSearchDistance(originX, originZ, requestedDistance);
|
||||
int maximumDistance = GenerationSemanticQueries.RIVER_SEARCH_DISTANCE;
|
||||
NativeModdedServer server = source.server();
|
||||
IrisModdedCommands.ok(source, "Searching accepted hydrology plans for " + type + "...");
|
||||
IrisModdedCommands.ok(source, GenerationSemanticQueries.riverSearchMessage(featureType, maximumDistance));
|
||||
MultiBurst.burst.completeValueAsync(() -> GenerationSemanticQueries.nearestRiver(
|
||||
engine, query, originX, originZ, maximumDistance,
|
||||
(int visited) -> server.execute(() -> IrisModdedCommands.ok(source,
|
||||
"Searched " + visited + " hydrology tiles for " + type + "..."))).orElse(null))
|
||||
engine, featureType, originX, originZ, maximumDistance).orElse(null))
|
||||
.whenComplete((GenerationSemanticQueries.RiverResult feature, Throwable error) -> server.execute(() -> {
|
||||
if (error != null) {
|
||||
ModdedIrisLog.error("Hydrology locate failed for {}", type, error);
|
||||
IrisModdedCommands.fail(source,
|
||||
"Could not locate " + type + " hydrology: " + error.getClass().getSimpleName());
|
||||
Throwable cause = error instanceof CompletionException && error.getCause() != null ? error.getCause() : error;
|
||||
ModdedIrisLog.error("Hydrology locate failed for {}", featureType.key(), cause);
|
||||
IrisModdedCommands.fail(source, "Could not search for " + label + ": " + cause.getClass().getSimpleName()
|
||||
+ (cause.getMessage() == null ? "" : " - " + cause.getMessage()));
|
||||
return;
|
||||
}
|
||||
if (feature == null) {
|
||||
IrisModdedCommands.fail(source,
|
||||
"No accepted " + type + " hydrology feature found within "
|
||||
+ maximumDistance + " blocks.");
|
||||
IrisModdedCommands.fail(source, GenerationSemanticQueries.riverNotFoundMessage(featureType, maximumDistance));
|
||||
return;
|
||||
}
|
||||
teleportToStructure(
|
||||
@@ -185,7 +176,7 @@ final class ModdedLocateCommands {
|
||||
feature.x(),
|
||||
feature.y() + 2,
|
||||
feature.z(),
|
||||
type + " hydrology feature"
|
||||
label
|
||||
);
|
||||
}));
|
||||
return 1;
|
||||
|
||||
+14
@@ -23,6 +23,7 @@ import art.arcane.volmlib.nativelib.terrain.NativePregenRuntime;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativePregenRuntime.ChunkLoad;
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.world.pregen.MantleHeapPressure;
|
||||
import art.arcane.iris.world.pregen.PregenHydrology;
|
||||
import art.arcane.iris.world.pregen.PregenListener;
|
||||
import art.arcane.iris.world.pregen.PregenMantleBackpressure;
|
||||
import art.arcane.iris.world.pregen.PregeneratorMethod;
|
||||
@@ -30,6 +31,7 @@ import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.modded.ModdedGenPool;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
@@ -71,6 +73,8 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
||||
private final PregenMantleBackpressure backpressure;
|
||||
private final PauseWhenEmptyGuard pauseGuard;
|
||||
private final LongSupplier clock;
|
||||
private final AtomicBoolean closing = new AtomicBoolean();
|
||||
private final PregenHydrology hydrology;
|
||||
|
||||
public ModdedPregenMethod(Configuration configuration) {
|
||||
this(configuration, System::nanoTime);
|
||||
@@ -80,6 +84,7 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
||||
this.clock = Objects.requireNonNull(clock);
|
||||
runtime = configuration.runtime();
|
||||
engine = configuration.engine();
|
||||
hydrology = new PregenHydrology(() -> engine);
|
||||
sync = configuration.sync();
|
||||
pauseGuard = new PauseWhenEmptyGuard(runtime);
|
||||
IrisSettings.IrisSettingsPregen pregen = IrisSettings.get().getPregen();
|
||||
@@ -121,6 +126,8 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
closing.set(true);
|
||||
hydrology.close();
|
||||
try {
|
||||
awaitPendingChunks();
|
||||
ModdedIrisLog.info("Iris modded pregen closing: dim={} completed={} pending={} peakInFlight={} finalLimit={}",
|
||||
@@ -341,6 +348,13 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRegionsAhead(List<long[]> regions) {
|
||||
if (!closing.get()) {
|
||||
hydrology.prepare(regions);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMethod(int x, int z) {
|
||||
return "Modded";
|
||||
|
||||
+92
-9
@@ -23,10 +23,9 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldView;
|
||||
|
||||
import art.arcane.iris.modded.ModdedIrisLog;
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.studio.map.MapWindow;
|
||||
import art.arcane.iris.studio.map.PaletteExplorerStart;
|
||||
import art.arcane.iris.studio.view.GuiHost;
|
||||
import art.arcane.iris.studio.view.ImageMapStudioGUI;
|
||||
import art.arcane.iris.studio.view.NoiseExplorerGUI;
|
||||
import art.arcane.iris.studio.view.VisionGUI;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.pack.StructurePackageClosure;
|
||||
import art.arcane.iris.pack.ImageMapPackageClosure;
|
||||
@@ -38,6 +37,7 @@ import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.biome.IrisBiomeGeneratorLink;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.world.entity.IrisEntitySpawn;
|
||||
import art.arcane.iris.world.IrisWorld;
|
||||
import art.arcane.iris.generation.noise.IrisGenerator;
|
||||
import art.arcane.iris.pack.PackExportClosure;
|
||||
import art.arcane.iris.world.entity.IrisEntity;
|
||||
@@ -49,6 +49,7 @@ import art.arcane.iris.modded.ModdedDimensionManager;
|
||||
import art.arcane.iris.modded.ModdedEngineBootstrap;
|
||||
import art.arcane.iris.modded.ModdedScheduler;
|
||||
import art.arcane.iris.modded.ModdedWorkspaceGenerator;
|
||||
import art.arcane.iris.modded.service.ModdedStudioHotloadService;
|
||||
import art.arcane.iris.generation.concurrent.BurstExecutor;
|
||||
import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
@@ -85,6 +86,7 @@ import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.regex.Pattern;
|
||||
@@ -92,6 +94,7 @@ import java.util.stream.Stream;
|
||||
|
||||
import art.arcane.iris.localization.IrisLanguage;
|
||||
import art.arcane.iris.modded.localization.ModdedCommandMessages;
|
||||
import art.arcane.volmlib.util.localization.LanguageAudience;
|
||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
public final class ModdedStudioCommands {
|
||||
private static final Predicate<NativeCommandSource> GATE = NativeCommandRegistration.GAMEMASTERS;
|
||||
@@ -152,6 +155,10 @@ public final class ModdedStudioCommands {
|
||||
.executes(ModdedCommandTree.localized((CommandContext<NativeCommandSource> context) -> tpStudio(context.getSource()))));
|
||||
root.then(NativeCommandRegistration.literal("status")
|
||||
.executes(ModdedCommandTree.localized((CommandContext<NativeCommandSource> context) -> status(context.getSource()))));
|
||||
root.then(NativeCommandRegistration.literal("hotload")
|
||||
.executes(ModdedCommandTree.localized((CommandContext<NativeCommandSource> context) -> hotload(context.getSource()))));
|
||||
root.then(NativeCommandRegistration.literal("refresh")
|
||||
.executes(ModdedCommandTree.localized((CommandContext<NativeCommandSource> context) -> hotload(context.getSource()))));
|
||||
root.then(workspaceTree("vscode", true));
|
||||
root.then(workspaceTree("vsc", true));
|
||||
root.then(workspaceTree("update", false));
|
||||
@@ -247,8 +254,9 @@ public final class ModdedStudioCommands {
|
||||
NativeProtocolPlayer player = source.player();
|
||||
ModdedGuiHost.bindContext(worldView(source.server(), level), engine, player == null ? null : player.id());
|
||||
}
|
||||
if (generatorKey == null || generatorKey.isBlank()) {
|
||||
NoiseExplorerGUI.launch();
|
||||
Engine mapEngine = engine != null ? engine : GuiHost.get().findActiveEngine();
|
||||
if ((generatorKey == null || generatorKey.isBlank()) && mapEngine != null) {
|
||||
MapWindow.openNoise(mapStart(mapEngine, source.player()), new MapWindow.NoiseSelection(null, null, seed));
|
||||
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_OPENING_NOISE_EXPLORER_ON_SERVER_DISPLAY));
|
||||
return 1;
|
||||
}
|
||||
@@ -262,7 +270,7 @@ public final class ModdedStudioCommands {
|
||||
return 0;
|
||||
}
|
||||
String selectedGeneratorKey = generatorKey.trim();
|
||||
NoiseExplorerGUI.launchGeneratorKey(selectedGeneratorKey, generator, seed);
|
||||
MapWindow.openNoise(mapStart(engine, source.player()), new MapWindow.NoiseSelection(selectedGeneratorKey, generator, seed));
|
||||
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_OPENING_NOISE_EXPLORER_GENERATOR_SEED, MessageArgument.untrusted("value", generatorKey.trim()), MessageArgument.untrusted("seed", seed)));
|
||||
return 1;
|
||||
}
|
||||
@@ -280,11 +288,20 @@ public final class ModdedStudioCommands {
|
||||
}
|
||||
NativeProtocolPlayer player = source.player();
|
||||
ModdedGuiHost.bindContext(worldView(source.server(), level), engine, player == null ? null : player.id());
|
||||
VisionGUI.launch(engine, player == null ? null : player.id());
|
||||
MapWindow.openMap(mapStart(engine, player));
|
||||
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_OPENING_VISION_MAP_ON_SERVER_DISPLAY, MessageArgument.untrusted("value", level.name())));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Modded worlds have no Bukkit world id; the map finds the engine again through the bound GUI context.
|
||||
private static PaletteExplorerStart mapStart(Engine engine, NativeProtocolPlayer player) {
|
||||
IrisWorld world = engine.getWorld();
|
||||
NativeWorld level = world == null ? null : world.platformWorld();
|
||||
boolean inWorld = player != null && player.connected() && level != null && player.inWorld(level);
|
||||
return new PaletteExplorerStart(engine, null, player == null ? null : player.id(), null,
|
||||
inWorld ? player.x() : 0D, inWorld ? player.z() : 0D);
|
||||
}
|
||||
|
||||
private static int imageMap(NativeCommandSource source) {
|
||||
NativeWorld level = source.world();
|
||||
Engine engine = IrisModdedCommands.engineFor(level);
|
||||
@@ -294,13 +311,13 @@ public final class ModdedStudioCommands {
|
||||
));
|
||||
return 0;
|
||||
}
|
||||
if (!ImageMapStudioGUI.isAvailable()) {
|
||||
if (!GuiHost.isAvailable() || !IrisSettings.get().getGui().isUseServerLaunchedGuis()) {
|
||||
IrisModdedCommands.fail(source, guiUnavailableMessage());
|
||||
return 0;
|
||||
}
|
||||
NativeProtocolPlayer player = source.player();
|
||||
ModdedGuiHost.bindContext(worldView(source.server(), level), engine, player == null ? null : player.id());
|
||||
ImageMapStudioGUI.launch(engine);
|
||||
MapWindow.openImageMaps(mapStart(engine, player));
|
||||
IrisModdedCommands.ok(source, IrisLanguage.plain(
|
||||
ModdedCommandMessages.MODDED_STUDIO_COMMANDS_OPENING_IMAGE_MAP_STUDIO_ON_SERVER_DISPLAY,
|
||||
MessageArgument.untrusted("value", level.name())
|
||||
@@ -643,6 +660,72 @@ public final class ModdedStudioCommands {
|
||||
return transition;
|
||||
}
|
||||
|
||||
private static int hotload(NativeCommandSource source) {
|
||||
NativeProtocolPlayer player = source.player();
|
||||
NativeModdedServer server = source.server();
|
||||
UUID owner = player == null ? CONSOLE_OWNER : player.id();
|
||||
UUID audience = source.playerId();
|
||||
Executor feedback = task -> server.execute(() -> LanguageAudience.run(audience, task));
|
||||
CompletableFuture<Void> transition = TRANSITIONS.submit(owner, () -> hotloadTransition(source, owner, feedback));
|
||||
transition.whenComplete((ignored, failure) -> {
|
||||
if (failure == null) {
|
||||
return;
|
||||
}
|
||||
Throwable cause = unwrapFailure(failure);
|
||||
ModdedIrisLog.error("Iris Studio hotload command failed", cause);
|
||||
feedback.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(
|
||||
ModdedCommandMessages.MODDED_STUDIO_COMMANDS_STUDIO_HOTLOAD_FAILED,
|
||||
MessageArgument.trusted("errorMessage", IrisLanguage.errorDetail(cause)))));
|
||||
});
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static CompletableFuture<Void> hotloadTransition(
|
||||
NativeCommandSource source,
|
||||
UUID owner,
|
||||
Executor feedback
|
||||
) {
|
||||
return CompletableFuture.supplyAsync(() -> hotloadStudio(source, owner, feedback), feedback)
|
||||
.thenCompose(request -> request);
|
||||
}
|
||||
|
||||
private static CompletableFuture<Void> hotloadStudio(
|
||||
NativeCommandSource source,
|
||||
UUID owner,
|
||||
Executor feedback
|
||||
) {
|
||||
String dimensionId = STUDIOS.get(owner);
|
||||
if (dimensionId == null) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(
|
||||
ModdedCommandMessages.MODDED_STUDIO_COMMANDS_YOU_DO_NOT_HAVE_OPEN_STUDIO_USE_IRIS_STUDIO_OPEN));
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
NativeWorld studio = ModdedDimensionManager.level(source.server(), dimensionId);
|
||||
if (studio == null) {
|
||||
STUDIOS.remove(owner, dimensionId);
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(
|
||||
ModdedCommandMessages.MODDED_STUDIO_COMMANDS_YOUR_STUDIO_DIMENSION_IS_NO_LONGER_LOADED_USE_IRIS_STUDIO));
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
Engine engine = IrisModdedCommands.engineFor(studio);
|
||||
ModdedStudioHotloadService service = ModdedEngineBootstrap.services().service(ModdedStudioHotloadService.class);
|
||||
if (engine == null || service == null) {
|
||||
throw new IllegalStateException("The Studio engine or hotload service is unavailable.");
|
||||
}
|
||||
return service.requestHotload(dimensionId, engine)
|
||||
.thenAcceptAsync(completed -> sendHotloadResult(source, completed), feedback);
|
||||
}
|
||||
|
||||
private static void sendHotloadResult(NativeCommandSource source, boolean completed) {
|
||||
if (completed) {
|
||||
IrisModdedCommands.ok(source, IrisLanguage.plain(
|
||||
ModdedCommandMessages.MODDED_STUDIO_COMMANDS_STUDIO_HOTLOAD_COMPLETE));
|
||||
} else {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(
|
||||
ModdedCommandMessages.MODDED_STUDIO_COMMANDS_STUDIO_HOTLOAD_BUSY));
|
||||
}
|
||||
}
|
||||
|
||||
private static int status(NativeCommandSource source) {
|
||||
NativeModdedServer server = source.server();
|
||||
List<ModdedDimensionManager.Handle> handles = ModdedDimensionManager.handles();
|
||||
|
||||
+48
-26
@@ -18,21 +18,22 @@
|
||||
|
||||
package art.arcane.iris.modded.command;
|
||||
|
||||
import art.arcane.iris.studio.view.GuiHost;
|
||||
import art.arcane.volmlib.nativelib.view.WorldMarker;
|
||||
import art.arcane.iris.studio.view.GuiMarker;
|
||||
import art.arcane.iris.studio.view.GuiOverlay;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.studio.view.OpenerPosition;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.studio.render.RenderType;
|
||||
import art.arcane.iris.modded.ModdedIrisLog;
|
||||
import art.arcane.iris.modded.ModdedTeleportDeadline;
|
||||
import art.arcane.iris.world.IrisWorld;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeProtocolPlayer;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
||||
import art.arcane.volmlib.nativelib.view.WorldView;
|
||||
|
||||
import java.awt.Desktop;
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import art.arcane.volmlib.nativelib.view.WorldView;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public final class ModdedVisionOverlay implements GuiOverlay {
|
||||
@@ -46,14 +47,33 @@ public final class ModdedVisionOverlay implements GuiOverlay {
|
||||
opener = context.opener();
|
||||
}
|
||||
|
||||
// WorldView doesn't report which way a player faces, so their markers all face south.
|
||||
@Override
|
||||
public List<WorldMarker> players() {
|
||||
return world.players();
|
||||
public List<GuiMarker> players() {
|
||||
return world.players().stream()
|
||||
.map(player -> GuiMarker.player(player.label(), player.worldX(), player.worldZ(), 0F))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestEntities(Consumer<List<WorldMarker>> sink) {
|
||||
world.requestEntities(sink);
|
||||
public void requestEntities(Consumer<List<GuiMarker>> sink) {
|
||||
world.requestEntities(entities -> sink.accept(entities.stream()
|
||||
.map(entity -> GuiMarker.entity(entity.label(), entity.worldX(), entity.worldY(), entity.worldZ(),
|
||||
entity.health(), entity.maxHealth()))
|
||||
.toList()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestOpenerPosition(Consumer<OpenerPosition> sink) {
|
||||
if (opener == null) {
|
||||
sink.accept(OpenerPosition.OFFLINE);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
world.execute(() -> sink.accept(openerPosition()));
|
||||
} catch (RejectedExecutionException failure) {
|
||||
sink.accept(OpenerPosition.OFFLINE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -80,23 +100,25 @@ public final class ModdedVisionOverlay implements GuiOverlay {
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public String openInEditor(double worldX, double worldZ, RenderType type) {
|
||||
if (!GuiHost.isAvailable() || !Desktop.isDesktopSupported()) {
|
||||
return null;
|
||||
private OpenerPosition openerPosition() {
|
||||
IrisWorld target = engine.getWorld();
|
||||
if (target == null || engine.isClosing() || engine.isClosed()) {
|
||||
return OpenerPosition.OFFLINE;
|
||||
}
|
||||
IrisComplex complex = engine.getComplex();
|
||||
File file = switch (type) {
|
||||
case BIOME, LAYER_LOAD, DECORATOR_LOAD, OBJECT_LOAD, HEIGHT, RIVER ->
|
||||
complex.getTrueBiomeStream().get(worldX, worldZ).openInVSCode();
|
||||
case BIOME_LAND -> complex.getLandBiomeStream().get(worldX, worldZ).openInVSCode();
|
||||
case BIOME_SEA -> complex.getSeaBiomeStream().get(worldX, worldZ).openInVSCode();
|
||||
case REGION -> complex.getRegionStream().get(worldX, worldZ).openInVSCode();
|
||||
case CAVE_LAND -> complex.getCaveBiomeStream().get(worldX, worldZ).openInVSCode();
|
||||
default -> null;
|
||||
};
|
||||
return file == null ? null : file.getName();
|
||||
NativeWorld level = target.platformWorld();
|
||||
if (level == null) {
|
||||
return OpenerPosition.OFFLINE;
|
||||
}
|
||||
NativeProtocolPlayer player = NativeModdedServer.forWorld(level).player(opener);
|
||||
if (player == null || !player.connected()) {
|
||||
return OpenerPosition.OFFLINE;
|
||||
}
|
||||
if (!player.inWorld(level)) {
|
||||
return OpenerPosition.IN_OTHER_WORLD;
|
||||
}
|
||||
return OpenerPosition.inWorld(player.x(), player.y(), player.z());
|
||||
}
|
||||
|
||||
public record Context(WorldView world, Engine engine, UUID opener) {
|
||||
}
|
||||
|
||||
|
||||
+15
@@ -1016,6 +1016,18 @@ public final class ModdedCommandMessages {
|
||||
"iris.modded.moddedstudiocommands.studio_close_failed",
|
||||
"Studio close failed: " + "{value}" + "{errorMessage}"
|
||||
);
|
||||
public static final TextKey MODDED_STUDIO_COMMANDS_STUDIO_HOTLOAD_COMPLETE = TextKey.of(
|
||||
"iris.modded.moddedstudiocommands.studio_hotload_complete",
|
||||
"Studio hotloaded."
|
||||
);
|
||||
public static final TextKey MODDED_STUDIO_COMMANDS_STUDIO_HOTLOAD_BUSY = TextKey.of(
|
||||
"iris.modded.moddedstudiocommands.studio_hotload_busy",
|
||||
"Studio is already checking or hotloading its pack. Try again after it finishes."
|
||||
);
|
||||
public static final TextKey MODDED_STUDIO_COMMANDS_STUDIO_HOTLOAD_FAILED = TextKey.of(
|
||||
"iris.modded.moddedstudiocommands.studio_hotload_failed",
|
||||
"Studio hotload failed{errorMessage}"
|
||||
);
|
||||
public static final TextKey MODDED_STUDIO_COMMANDS_STUDIO_CLOSED_WAS_EVACUATED_UNLOADED_ITS_REGION_DATA_DELETED = TextKey.of(
|
||||
"iris.modded.moddedstudiocommands.studio_closed_was_evacuated_unloaded_its_region_data_deleted",
|
||||
"Studio closed: " + "{dimensionId}" + " was evacuated, unloaded and its region data deleted."
|
||||
@@ -1530,6 +1542,9 @@ public final class ModdedCommandMessages {
|
||||
MODDED_STUDIO_COMMANDS_STUDIO_OPEN_NOW_RUNS_SEED_USE_IRIS_STUDIO_CLOSE_WHEN,
|
||||
MODDED_STUDIO_COMMANDS_YOU_DO_NOT_HAVE_OPEN_STUDIO_USE_IRIS_STUDIO_OPEN,
|
||||
MODDED_STUDIO_COMMANDS_STUDIO_CLOSE_FAILED,
|
||||
MODDED_STUDIO_COMMANDS_STUDIO_HOTLOAD_COMPLETE,
|
||||
MODDED_STUDIO_COMMANDS_STUDIO_HOTLOAD_BUSY,
|
||||
MODDED_STUDIO_COMMANDS_STUDIO_HOTLOAD_FAILED,
|
||||
MODDED_STUDIO_COMMANDS_STUDIO_CLOSED_WAS_EVACUATED_UNLOADED_ITS_REGION_DATA_DELETED,
|
||||
MODDED_STUDIO_COMMANDS_NO_STUDIO_DIMENSIONS_ARE_CURRENTLY_OPEN,
|
||||
MODDED_STUDIO_COMMANDS_ACTIVE_STUDIO_DIMENSION_S,
|
||||
|
||||
+5
@@ -242,6 +242,10 @@ public final class ModdedHelpMessages {
|
||||
"iris.modded.help.entry.command.status_studio",
|
||||
"Show the open studio dimension and its pack"
|
||||
);
|
||||
public static final TextKey COMMAND_HOTLOAD_REFRESH_THE_OPEN_STUDIO = TextKey.of(
|
||||
"iris.modded.help.entry.command.hotload_studio",
|
||||
"Reload the open Studio pack for newly generated terrain"
|
||||
);
|
||||
public static final TextKey COMMAND_NOISE_OPEN_THE_NOISE_EXPLORER_GUI_ON_THE_SERVER_DISPLAY = TextKey.of(
|
||||
"iris.modded.help.entry.command.noise",
|
||||
"Open the Noise Explorer GUI on the server display"
|
||||
@@ -479,6 +483,7 @@ public final class ModdedHelpMessages {
|
||||
COMMAND_CLOSE_CLOSE_THE_OPEN_STUDIO_DIMENSION_AND_DISCARD_ITS_WORLD,
|
||||
COMMAND_TPSTUDIO_TELEPORT_INTO_THE_OPEN_STUDIO_DIMENSION,
|
||||
COMMAND_STATUS_SHOW_THE_OPEN_STUDIO_DIMENSION_AND_ITS_PACK,
|
||||
COMMAND_HOTLOAD_REFRESH_THE_OPEN_STUDIO,
|
||||
COMMAND_NOISE_OPEN_THE_NOISE_EXPLORER_GUI_ON_THE_SERVER_DISPLAY,
|
||||
COMMAND_MAP_OPEN_THE_VISION_MAP_GUI_ON_THE_SERVER_DISPLAY,
|
||||
COMMAND_VSCODE_REGENERATE_THE_CODE_WORKSPACE_FOR_A_PACK_AND_OPEN_IT_IN_YOUR,
|
||||
|
||||
+34
-7
@@ -123,6 +123,33 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
|
||||
watches.clear();
|
||||
}
|
||||
|
||||
public CompletableFuture<Boolean> requestHotload(String dimensionId, Engine engine) {
|
||||
if (!dimensionId.startsWith(STUDIO_DIMENSION_PREFIX) || !engine.isStudio()) {
|
||||
return CompletableFuture.failedFuture(new IllegalArgumentException("Only Studio worlds can be hotloaded."));
|
||||
}
|
||||
ExecutorService active = executor;
|
||||
if (active == null || active.isShutdown()) {
|
||||
return CompletableFuture.failedFuture(new IllegalStateException("Iris Studio hotload is unavailable."));
|
||||
}
|
||||
Watch watch = watches.computeIfAbsent(dimensionId, (String key) -> new Watch());
|
||||
if (!watch.busy.compareAndSet(false, true)) {
|
||||
return CompletableFuture.completedFuture(false);
|
||||
}
|
||||
try {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
hotload(dimensionId, engine);
|
||||
return true;
|
||||
} finally {
|
||||
watch.busy.set(false);
|
||||
}
|
||||
}, active);
|
||||
} catch (RejectedExecutionException rejected) {
|
||||
watch.busy.set(false);
|
||||
return CompletableFuture.failedFuture(rejected);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refreshWorkspace(Engine engine) {
|
||||
writeWorkspace(engine, "workspace refresh");
|
||||
@@ -254,7 +281,7 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
|
||||
try {
|
||||
active.execute(() -> {
|
||||
try {
|
||||
poll(dimensionId, watch, generator, engine);
|
||||
poll(dimensionId, watch, engine);
|
||||
} finally {
|
||||
watch.busy.set(false);
|
||||
}
|
||||
@@ -332,14 +359,14 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
|
||||
return isPregeneratorActive(engine);
|
||||
}
|
||||
|
||||
private void poll(String dimensionId, Watch watch, IrisModdedChunkGenerator generator, Engine engine) {
|
||||
private void poll(String dimensionId, Watch watch, Engine engine) {
|
||||
try {
|
||||
if (watch.engine != engine) {
|
||||
watch.close();
|
||||
watch.engine = engine;
|
||||
watch.folder = new ReactiveFolder(
|
||||
engine.getData().getDataFolder(),
|
||||
(KList<File> created, KList<File> changed, KList<File> deleted) -> hotload(dimensionId, generator, engine),
|
||||
(KList<File> created, KList<File> changed, KList<File> deleted) -> hotload(dimensionId, engine),
|
||||
new KList<>(".iob", ".json"),
|
||||
new KList<>(".iris"),
|
||||
new KList<>());
|
||||
@@ -354,12 +381,12 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
|
||||
}
|
||||
}
|
||||
|
||||
private void hotload(String dimensionId, IrisModdedChunkGenerator generator, Engine engine) {
|
||||
if (engine.isClosed()) {
|
||||
return;
|
||||
}
|
||||
private void hotload(String dimensionId, Engine engine) {
|
||||
long start = System.currentTimeMillis();
|
||||
try {
|
||||
if (engine.isClosed() || engine.isClosing()) {
|
||||
throw new IllegalStateException("The Studio world is closing or closed.");
|
||||
}
|
||||
engine.hotloadSilently();
|
||||
ModdedIrisLog.info("Iris studio hotload {} pack={} {}ms", dimensionId, engine.getDimension().getLoadKey(), System.currentTimeMillis() - start);
|
||||
} catch (Throwable e) {
|
||||
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
package art.arcane.iris.modded.command;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.engine.HydrologyRuntime;
|
||||
import art.arcane.iris.generation.hydrology.query.HydrologyFeatureQuery;
|
||||
import art.arcane.iris.generation.hydrology.query.HydrologyFeatureRef;
|
||||
import art.arcane.iris.generation.hydrology.query.HydrologyFeatureType;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.world.history.GenerationSemanticQueries;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeCommandSource;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeProtocolPlayer;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeStructureQueries;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.MockedConstruction;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyDouble;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockConstruction;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class ModdedLocateRiverTest {
|
||||
private static final int DISTANCE = GenerationSemanticQueries.RIVER_SEARCH_DISTANCE;
|
||||
|
||||
@Test
|
||||
public void unknownTypeFailsBeforeSearching() {
|
||||
Fixture fixture = new Fixture();
|
||||
try (MockedStatic<IrisModdedCommands> commands = fixture.commands()) {
|
||||
assertEquals(0, ModdedLocateCommands.gotoRiver(fixture.source, "ocean_trench"));
|
||||
|
||||
commands.verify(() -> IrisModdedCommands.fail(eq(fixture.source), anyString()));
|
||||
commands.verify(() -> IrisModdedCommands.ok(eq(fixture.source), anyString()), never());
|
||||
verify(fixture.server, never()).execute(any());
|
||||
verify(fixture.runtime, never()).nearestFeature(anyInt(), anyInt(), any(), anyInt(), any());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missingFeatureReportsNotFound() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
when(fixture.runtime.nearestFeature(anyInt(), anyInt(), any(), anyInt(), any())).thenReturn(Optional.empty());
|
||||
try (MockedStatic<IrisModdedCommands> commands = fixture.commands()) {
|
||||
assertEquals(1, ModdedLocateCommands.gotoRiver(fixture.source, "river"));
|
||||
fixture.finishOnServerThread();
|
||||
|
||||
commands.verify(() -> IrisModdedCommands.fail(fixture.source, "No river found within " + DISTANCE + " blocks."));
|
||||
verify(fixture.player, never()).teleport(any(), anyDouble(), anyDouble(), anyDouble());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void plannerFailureIsReportedAsAnErrorNotAsNotFound() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
when(fixture.runtime.nearestFeature(anyInt(), anyInt(), any(), anyInt(), any()))
|
||||
.thenThrow(new IllegalStateException("window planner exploded"));
|
||||
try (MockedStatic<IrisModdedCommands> commands = fixture.commands()) {
|
||||
assertEquals(1, ModdedLocateCommands.gotoRiver(fixture.source, "major_river"));
|
||||
fixture.finishOnServerThread();
|
||||
|
||||
commands.verify(() -> IrisModdedCommands.fail(fixture.source,
|
||||
"Could not search for major river: IllegalStateException - window planner exploded"));
|
||||
commands.verify(() -> IrisModdedCommands.fail(eq(fixture.source), eq("No major river found within " + DISTANCE + " blocks.")),
|
||||
never());
|
||||
verify(fixture.player, never()).teleport(any(), anyDouble(), anyDouble(), anyDouble());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void foundFeatureTeleportsAboveItFromThePlayersPosition() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
when(fixture.runtime.nearestFeature(anyInt(), anyInt(), any(), anyInt(), any()))
|
||||
.thenReturn(Optional.of(new HydrologyFeatureRef(9L, HydrologyFeatureType.WATERFALL, 400, 130, -250)));
|
||||
when(fixture.player.teleport(fixture.world, 400.5D, 68D, -249.5D)).thenReturn(true);
|
||||
try (MockedStatic<IrisModdedCommands> commands = fixture.commands();
|
||||
MockedConstruction<NativeStructureQueries> queries = mockConstruction(NativeStructureQueries.class)) {
|
||||
assertEquals(1, ModdedLocateCommands.gotoRiver(fixture.source, "waterfall"));
|
||||
fixture.finishOnServerThread();
|
||||
|
||||
ArgumentCaptor<HydrologyFeatureQuery> query = ArgumentCaptor.forClass(HydrologyFeatureQuery.class);
|
||||
verify(fixture.runtime).nearestFeature(eq(120), eq(-75), query.capture(), eq(DISTANCE), any());
|
||||
assertEquals(HydrologyFeatureQuery.of(HydrologyFeatureType.WATERFALL), query.getValue());
|
||||
verify(queries.constructed().getFirst()).loadChunk(400 >> 4, -250 >> 4);
|
||||
verify(fixture.player).teleport(fixture.world, 400.5D, 68D, -249.5D);
|
||||
commands.verify(() -> IrisModdedCommands.ok(eq(fixture.source), anyString()), times(2));
|
||||
commands.verify(() -> IrisModdedCommands.fail(eq(fixture.source), anyString()), never());
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Fixture {
|
||||
private final NativeCommandSource source = mock(NativeCommandSource.class);
|
||||
private final NativeWorld world = mock(NativeWorld.class);
|
||||
private final NativeProtocolPlayer player = mock(NativeProtocolPlayer.class);
|
||||
private final NativeModdedServer server = mock(NativeModdedServer.class);
|
||||
private final Engine engine = mock(Engine.class);
|
||||
private final HydrologyRuntime runtime = mock(HydrologyRuntime.class);
|
||||
private final CompletableFuture<Runnable> completion = new CompletableFuture<>();
|
||||
|
||||
private Fixture() {
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
IrisDimension dimension = mock(IrisDimension.class);
|
||||
when(source.player()).thenReturn(player);
|
||||
when(source.world()).thenReturn(world);
|
||||
when(source.server()).thenReturn(server);
|
||||
when(player.blockX()).thenReturn(120);
|
||||
when(player.blockZ()).thenReturn(-75);
|
||||
when(player.connected()).thenReturn(true);
|
||||
when(player.inWorld(world)).thenReturn(true);
|
||||
when(world.minHeight()).thenReturn(-64);
|
||||
when(world.maxHeight()).thenReturn(320);
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
when(engine.getDimension()).thenReturn(dimension);
|
||||
when(dimension.getMinHeight()).thenReturn(-64);
|
||||
when(complex.getHydrologyRuntime()).thenReturn(runtime);
|
||||
when(runtime.enabled()).thenReturn(true);
|
||||
doAnswer(invocation -> {
|
||||
completion.complete(invocation.getArgument(0));
|
||||
return null;
|
||||
}).when(server).execute(any());
|
||||
}
|
||||
|
||||
private MockedStatic<IrisModdedCommands> commands() {
|
||||
MockedStatic<IrisModdedCommands> commands = mockStatic(IrisModdedCommands.class);
|
||||
commands.when(() -> IrisModdedCommands.engineFor(world)).thenReturn(engine);
|
||||
return commands;
|
||||
}
|
||||
|
||||
private void finishOnServerThread() throws Exception {
|
||||
completion.get(10L, TimeUnit.SECONDS).run();
|
||||
}
|
||||
}
|
||||
}
|
||||
+24
@@ -1,6 +1,7 @@
|
||||
package art.arcane.iris.modded.command;
|
||||
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.generation.hydrology.engine.HydrologyPregeneration;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.modded.ModdedGenPool;
|
||||
import art.arcane.iris.world.pregen.MantleHeapPressure;
|
||||
@@ -11,8 +12,10 @@ import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InOrder;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
@@ -31,9 +34,11 @@ import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyList;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
@@ -89,6 +94,25 @@ public class ModdedPregenMethodLifecycleTest {
|
||||
verify(runtime, never()).execute(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void closeWithdrawsQueuedHydrologyPrefetchAndRefusesLaterAnnouncements() {
|
||||
HydrologyPregeneration epoch = mock(HydrologyPregeneration.class);
|
||||
when(engine.getComplex().getHydrologyRuntime().openPregeneration()).thenReturn(epoch);
|
||||
when(runtime.isServerThread()).thenReturn(true);
|
||||
assertTrue(method.deferFinalSaveToServerThread());
|
||||
List<long[]> ahead = List.of(new long[]{0L, 0L}, new long[]{1L, 0L});
|
||||
|
||||
method.onRegionsAhead(ahead);
|
||||
method.close();
|
||||
method.onRegionsAhead(ahead);
|
||||
|
||||
InOrder order = inOrder(epoch);
|
||||
order.verify(epoch).prepare(ahead);
|
||||
order.verify(epoch).close();
|
||||
verify(epoch, times(1)).prepare(anyList());
|
||||
method.completeDeferredFinalSave();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void asynchronousCallerCannotClaimTheShutdownSave() {
|
||||
assertFalse(method.deferFinalSaveToServerThread());
|
||||
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
package art.arcane.iris.modded.command;
|
||||
|
||||
import art.arcane.iris.localization.IrisLanguage;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeCommandSource;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeCommandText;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeProtocolPlayer;
|
||||
import art.arcane.volmlib.util.localization.LanguageAudience;
|
||||
import com.mojang.brigadier.CommandDispatcher;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class ModdedStudioHotloadLanguageTest {
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
private final UUID playerId = UUID.randomUUID();
|
||||
private File dataFolder;
|
||||
|
||||
@BeforeClass
|
||||
public static void bootstrap() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
dataFolder = temporaryFolder.newFolder();
|
||||
assertTrue(IrisLanguage.reload(dataFolder, "en_US"));
|
||||
Files.writeString(dataFolder.toPath().resolve("languages/de_DE.toml"),
|
||||
"\"iris.modded.moddedstudiocommands.you_do_not_have_open_studio_use_iris_studio_open\" = \"Kein Studio geöffnet.\"\n");
|
||||
IrisLanguage.start();
|
||||
IrisLanguage.selections().selectPlayer(playerId, "de_DE").get(5L, TimeUnit.SECONDS);
|
||||
ModdedStudioCommands.clear();
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
ModdedStudioCommands.clear();
|
||||
IrisLanguage.shutdown();
|
||||
assertTrue(IrisLanguage.reload(dataFolder, "en_US"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void delayedHotloadFeedbackKeepsThePlayersLanguage() throws Exception {
|
||||
assertEquals(List.of("Kein Studio geöffnet."), runCommand("studio hotload", true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void delayedConsoleRefreshUsesTheServerLanguage() throws Exception {
|
||||
assertEquals(List.of("You do not have an open studio. Use /iris studio open <pack> first."),
|
||||
runCommand("studio refresh", false));
|
||||
}
|
||||
|
||||
private List<String> runCommand(String command, boolean playerCommand) throws Exception {
|
||||
NativeModdedServer server = mock(NativeModdedServer.class);
|
||||
List<Runnable> tasks = new ArrayList<>();
|
||||
doAnswer(invocation -> {
|
||||
tasks.add(invocation.getArgument(0));
|
||||
return null;
|
||||
}).when(server).execute(any(Runnable.class));
|
||||
NativeCommandSource source = mock(NativeCommandSource.class);
|
||||
when(source.server()).thenReturn(server);
|
||||
when(source.isGameMaster()).thenReturn(true);
|
||||
if (playerCommand) {
|
||||
NativeProtocolPlayer player = mock(NativeProtocolPlayer.class);
|
||||
when(player.id()).thenReturn(playerId);
|
||||
when(source.player()).thenReturn(player);
|
||||
when(source.playerId()).thenReturn(playerId);
|
||||
}
|
||||
List<String> messages = new ArrayList<>();
|
||||
doAnswer(invocation -> {
|
||||
NativeCommandText text = invocation.getArgument(0);
|
||||
messages.add(text.getString());
|
||||
return null;
|
||||
}).when(source).sendFailure(any());
|
||||
CommandDispatcher<NativeCommandSource> dispatcher = new CommandDispatcher<>();
|
||||
dispatcher.register(ModdedStudioCommands.tree("studio"));
|
||||
|
||||
assertEquals(1, dispatcher.execute(command, source));
|
||||
assertTrue(messages.isEmpty());
|
||||
assertNull(LanguageAudience.current());
|
||||
UUID ambientAudience = playerCommand ? UUID.randomUUID() : playerId;
|
||||
try (LanguageAudience.Scope audience = LanguageAudience.open(ambientAudience)) {
|
||||
while (!tasks.isEmpty()) {
|
||||
tasks.removeFirst().run();
|
||||
assertEquals(ambientAudience, LanguageAudience.current());
|
||||
}
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
}
|
||||
+73
@@ -1,25 +1,98 @@
|
||||
package art.arcane.iris.modded.command;
|
||||
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.studio.view.OpenerPosition;
|
||||
import art.arcane.iris.world.IrisWorld;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeProtocolPlayer;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
||||
import art.arcane.volmlib.nativelib.view.WorldView;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class ModdedVisionOverlayTest {
|
||||
@Test
|
||||
public void rejectedServerTaskCompletesThePositionRequestAsOffline() {
|
||||
WorldView world = mock(WorldView.class);
|
||||
doThrow(new RejectedExecutionException("Server stopped")).when(world).execute(any(Runnable.class));
|
||||
ModdedVisionOverlay overlay = new ModdedVisionOverlay(new ModdedVisionOverlay.Context(world,
|
||||
mock(Engine.class), UUID.randomUUID()));
|
||||
AtomicReference<OpenerPosition> position = new AtomicReference<>();
|
||||
|
||||
overlay.requestOpenerPosition(position::set);
|
||||
|
||||
assertEquals(OpenerPosition.OFFLINE, position.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void openerPositionRunsOnTheServerAndTracksDisconnectAndWorldChanges() {
|
||||
WorldView world = mock(WorldView.class);
|
||||
AtomicReference<Runnable> scheduled = new AtomicReference<>();
|
||||
doAnswer(invocation -> {
|
||||
scheduled.set(invocation.getArgument(0));
|
||||
return null;
|
||||
}).when(world).execute(any(Runnable.class));
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisWorld irisWorld = mock(IrisWorld.class);
|
||||
NativeWorld level = mock(NativeWorld.class);
|
||||
NativeModdedServer server = mock(NativeModdedServer.class);
|
||||
NativeProtocolPlayer player = mock(NativeProtocolPlayer.class);
|
||||
UUID opener = UUID.randomUUID();
|
||||
when(engine.getWorld()).thenReturn(irisWorld);
|
||||
when(irisWorld.platformWorld()).thenReturn(level);
|
||||
when(server.player(opener)).thenReturn(player);
|
||||
when(player.connected()).thenReturn(true);
|
||||
when(player.inWorld(level)).thenReturn(true);
|
||||
when(player.x()).thenReturn(21.5D);
|
||||
when(player.y()).thenReturn(85.25D);
|
||||
when(player.z()).thenReturn(-39.5D);
|
||||
ModdedVisionOverlay overlay = new ModdedVisionOverlay(new ModdedVisionOverlay.Context(world, engine, opener));
|
||||
AtomicReference<OpenerPosition> position = new AtomicReference<>();
|
||||
try (MockedStatic<NativeModdedServer> nativeServer = mockStatic(NativeModdedServer.class)) {
|
||||
nativeServer.when(() -> NativeModdedServer.forWorld(level)).thenReturn(server);
|
||||
|
||||
overlay.requestOpenerPosition(position::set);
|
||||
assertNull("position reads must wait for the server task", position.get());
|
||||
scheduled.get().run();
|
||||
assertEquals(OpenerPosition.inWorld(21.5D, 85.25D, -39.5D), position.get());
|
||||
|
||||
overlay.requestOpenerPosition(position::set);
|
||||
when(player.connected()).thenReturn(false);
|
||||
scheduled.get().run();
|
||||
assertEquals(OpenerPosition.OFFLINE, position.get());
|
||||
|
||||
when(player.connected()).thenReturn(true);
|
||||
when(player.inWorld(level)).thenReturn(false);
|
||||
overlay.requestOpenerPosition(position::set);
|
||||
scheduled.get().run();
|
||||
assertEquals(OpenerPosition.IN_OTHER_WORLD, position.get());
|
||||
|
||||
when(player.inWorld(level)).thenReturn(true);
|
||||
overlay.requestOpenerPosition(position::set);
|
||||
scheduled.get().run();
|
||||
assertEquals(OpenerPosition.inWorld(21.5D, 85.25D, -39.5D), position.get());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A Vision click can land anywhere in the world, so the destination chunk is usually cold and gets the same
|
||||
* bounded deadline as /iris tp.
|
||||
|
||||
+119
@@ -1,5 +1,6 @@
|
||||
package art.arcane.iris.modded.service;
|
||||
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.modded.ModdedScheduler;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
@@ -8,19 +9,130 @@ import org.mockito.InOrder;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class ModdedStudioHotloadServiceTest {
|
||||
@Test
|
||||
public void manualHotloadRunsWithoutAFileChangeOnTheWorker() throws Exception {
|
||||
ModdedStudioHotloadService service = new ModdedStudioHotloadService();
|
||||
Engine engine = studioEngine();
|
||||
Thread caller = Thread.currentThread();
|
||||
doAnswer(invocation -> {
|
||||
assertNotSame(caller, Thread.currentThread());
|
||||
return null;
|
||||
}).when(engine).hotloadSilently();
|
||||
service.onEnable();
|
||||
try {
|
||||
assertTrue(service.requestHotload("irisworldgen:studio_test", engine).get(5L, TimeUnit.SECONDS));
|
||||
verify(engine).hotloadSilently();
|
||||
} finally {
|
||||
service.onDisable();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void overlappingManualHotloadIsRejected() throws Exception {
|
||||
ModdedStudioHotloadService service = new ModdedStudioHotloadService();
|
||||
Engine engine = studioEngine();
|
||||
CountDownLatch entered = new CountDownLatch(1);
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
doAnswer(invocation -> {
|
||||
entered.countDown();
|
||||
assertTrue(release.await(5L, TimeUnit.SECONDS));
|
||||
return null;
|
||||
}).when(engine).hotloadSilently();
|
||||
service.onEnable();
|
||||
try {
|
||||
CompletableFuture<Boolean> first = service.requestHotload("irisworldgen:studio_test", engine);
|
||||
assertTrue(entered.await(5L, TimeUnit.SECONDS));
|
||||
assertFalse(service.requestHotload("irisworldgen:studio_test", engine).get(5L, TimeUnit.SECONDS));
|
||||
release.countDown();
|
||||
assertTrue(first.get(5L, TimeUnit.SECONDS));
|
||||
verify(engine).hotloadSilently();
|
||||
} finally {
|
||||
release.countDown();
|
||||
service.onDisable();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void closingStudioIsNotHotloadedAfterWaitingInTheQueue() throws Exception {
|
||||
ModdedStudioHotloadService service = new ModdedStudioHotloadService();
|
||||
Engine engine = studioEngine();
|
||||
ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
executor.submit(() -> {
|
||||
try {
|
||||
assertTrue(release.await(5L, TimeUnit.SECONDS));
|
||||
} catch (InterruptedException failure) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException(failure);
|
||||
}
|
||||
});
|
||||
setExecutor(service, executor);
|
||||
try {
|
||||
CompletableFuture<Boolean> requested = service.requestHotload("irisworldgen:studio_test", engine);
|
||||
when(engine.isClosing()).thenReturn(true);
|
||||
release.countDown();
|
||||
assertThrows(ExecutionException.class, () -> requested.get(5L, TimeUnit.SECONDS));
|
||||
verify(engine, never()).hotloadSilently();
|
||||
} finally {
|
||||
release.countDown();
|
||||
service.onDisable();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void manualHotloadRejectsProductionWorlds() throws Exception {
|
||||
ModdedStudioHotloadService service = new ModdedStudioHotloadService();
|
||||
Engine engine = studioEngine();
|
||||
service.onEnable();
|
||||
try {
|
||||
assertThrows(ExecutionException.class, () -> service.requestHotload("irisworldgen:production", engine)
|
||||
.get(5L, TimeUnit.SECONDS));
|
||||
when(engine.isStudio()).thenReturn(false);
|
||||
assertThrows(ExecutionException.class, () -> service.requestHotload("irisworldgen:studio_test", engine)
|
||||
.get(5L, TimeUnit.SECONDS));
|
||||
verify(engine, never()).hotloadSilently();
|
||||
} finally {
|
||||
service.onDisable();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failedManualHotloadReleasesTheWatchForRetry() throws Exception {
|
||||
ModdedStudioHotloadService service = new ModdedStudioHotloadService();
|
||||
Engine engine = studioEngine();
|
||||
doThrow(new IllegalStateException("Invalid pack")).doNothing().when(engine).hotloadSilently();
|
||||
service.onEnable();
|
||||
try {
|
||||
assertThrows(ExecutionException.class, () -> service.requestHotload("irisworldgen:studio_test", engine)
|
||||
.get(5L, TimeUnit.SECONDS));
|
||||
assertTrue(service.requestHotload("irisworldgen:studio_test", engine).get(5L, TimeUnit.SECONDS));
|
||||
} finally {
|
||||
service.onDisable();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shutdownDrainsWorkerBeforeAllowingRestart() throws Exception {
|
||||
ModdedStudioHotloadService service = new ModdedStudioHotloadService();
|
||||
@@ -102,4 +214,11 @@ public class ModdedStudioHotloadServiceTest {
|
||||
field.set(service, executor);
|
||||
}
|
||||
|
||||
private static Engine studioEngine() {
|
||||
Engine engine = mock(Engine.class);
|
||||
when(engine.isStudio()).thenReturn(true);
|
||||
when(engine.getDimension()).thenReturn(new IrisDimension());
|
||||
return engine;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -60,7 +60,7 @@ apply from: file('../../gradle/java21-runtime-exclusions.gradle')
|
||||
|
||||
Properties rootProperties = new Properties()
|
||||
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
|
||||
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.3.5'))
|
||||
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.3.6'))
|
||||
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.3'))
|
||||
String neoForgeVersion = providers.gradleProperty('neoForgeVersion').getOrElse(irisModTarget.neoForge.toString())
|
||||
Closure<String> irisArtifactName = { String platform, String targetVersion ->
|
||||
@@ -132,6 +132,8 @@ configurations.named('bundle').configure {
|
||||
exclude(group: 'de.crazydev22.slimjar.helper')
|
||||
exclude(group: 'de.crazydev22.slimjar')
|
||||
exclude(group: 'io.github.slimjar')
|
||||
// The game ships its own LWJGL; without the modules it leaves out, the map's 3D views draw in software.
|
||||
exclude(group: 'org.lwjgl')
|
||||
}
|
||||
|
||||
configurations.compileClasspath.extendsFrom(configurations.devBundle)
|
||||
@@ -141,7 +143,7 @@ configurations.testRuntimeClasspath.extendsFrom(configurations.runtimeClasspath,
|
||||
|
||||
dependencies {
|
||||
testImplementation('junit:junit:4.13.2')
|
||||
testImplementation('org.mockito:mockito-core:5.23.0')
|
||||
testImplementation(libs.mockito.core)
|
||||
testImplementation(testFixtures('art.arcane:core'))
|
||||
testImplementation(libs.spigot) {
|
||||
transitive = false
|
||||
@@ -173,7 +175,9 @@ dependencies {
|
||||
libs.caffeine,
|
||||
'com.fulcrumgenomics:jlibdeflate:0.2.0',
|
||||
libs.dom4j,
|
||||
libs.jaxen
|
||||
libs.jaxen,
|
||||
libs.flatlaf,
|
||||
libs.flatlaf.extras
|
||||
]
|
||||
add('bundle', libs.asm)
|
||||
add('devBundle', 'org.ow2.asm:asm') {
|
||||
@@ -261,6 +265,8 @@ tasks.named('compileJava', JavaCompile).configure {
|
||||
options.release.set((int) irisModTarget.java)
|
||||
}
|
||||
|
||||
apply from: file('../../gradle/mockito-agent.gradle')
|
||||
|
||||
tasks.named('test').configure {
|
||||
useJUnitPlatform()
|
||||
classpath = classpath.minus(developmentModLibraries).plus(sourceSets.modLibraries.output)
|
||||
|
||||
+12
-7
@@ -55,7 +55,7 @@ apply from: file('gradle/runtime-relocations.gradle')
|
||||
apply from: file('gradle/volmlib-stamp.gradle')
|
||||
|
||||
group = 'art.arcane'
|
||||
version = providers.gradleProperty('irisVersion').getOrElse('4.3.5')
|
||||
version = providers.gradleProperty('irisVersion').getOrElse('4.3.6')
|
||||
|
||||
project(':core') {
|
||||
group = 'art.arcane'
|
||||
@@ -89,7 +89,6 @@ Closure<String> irisArtifactName = { String platform, String targetVersion ->
|
||||
String bukkitArtifactName = irisArtifactName('CraftBukkit', bukkitMinecraftRange)
|
||||
Map<String, String> runtimeDownloadRelocations = (Map<String, String>) ext.runtimeDownloadRelocations
|
||||
List<String> runtimeDownloadedPrefixes = (List<String>) ext.runtimeDownloadedPrefixes
|
||||
File bukkitArtifact = layout.buildDirectory.file("libs/${bukkitArtifactName}").get().asFile
|
||||
apply plugin: ApiGenerator
|
||||
|
||||
List<String> nmsBindings = ['v26_2_R1', 'v26_3_R1']
|
||||
@@ -127,7 +126,7 @@ nmsBindings.each { String key ->
|
||||
compileOnly(rootProject.libs.annotations)
|
||||
compileOnly(rootProject.libs.byteBuddy.core)
|
||||
testImplementation('junit:junit:4.13.2')
|
||||
testImplementation('org.mockito:mockito-core:5.23.0')
|
||||
testImplementation(rootProject.libs.mockito.core)
|
||||
if (key == 'v26_3_R1') {
|
||||
testImplementation(project(':probe'))
|
||||
}
|
||||
@@ -194,6 +193,7 @@ tasks.named('jar', Jar).configure {
|
||||
includeEmptyDirs = false
|
||||
from(jarJar)
|
||||
from(provider { included.resolve().collect { zipTree(it) } }) {
|
||||
exclude('META-INF/INDEX.LIST')
|
||||
exclude('languages/**')
|
||||
exclude('com/fren_gor/**')
|
||||
|
||||
@@ -534,19 +534,20 @@ List<String> requiredBukkitArtifactEntries = [
|
||||
tasks.register('verifyBukkitArtifact') {
|
||||
group = 'verification'
|
||||
dependsOn('jar')
|
||||
File artifact = bukkitArtifact
|
||||
Provider<RegularFile> artifact = tasks.named('jar', Jar).flatMap { Jar task -> task.archiveFile }
|
||||
inputs.file(artifact)
|
||||
inputs.files(nativeRuntime)
|
||||
inputs.files(commandHandlerSources)
|
||||
inputs.files(mantleComponentSources)
|
||||
doLast {
|
||||
File archive = artifact.get().asFile
|
||||
BukkitArtifactVerifier.verify(
|
||||
artifact,
|
||||
archive,
|
||||
requiredBukkitArtifactEntries,
|
||||
16,
|
||||
runtimeDownloadedPrefixes)
|
||||
NativeRuntimeArtifacts.verify(artifact, nativeRuntimeModules, embeddedNatives())
|
||||
logger.lifecycle("Verified ${artifact.name} packaging and class reference graph")
|
||||
NativeRuntimeArtifacts.verify(archive, nativeRuntimeModules, embeddedNatives())
|
||||
logger.lifecycle("Verified ${archive.name} packaging and class reference graph")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -586,11 +587,14 @@ allprojects {
|
||||
layout.buildDirectory.set(layout.projectDirectory.dir("build/${runtimeOutputVariant}"))
|
||||
}
|
||||
|
||||
apply from: rootProject.file('gradle/mockito-agent.gradle')
|
||||
|
||||
tasks.withType(Test).configureEach {
|
||||
int moduleForks = project.path.startsWith(':adapters:bukkit:') ? Math.min(2, testForks)
|
||||
: project.path == ':probe' ? Math.min(4, testForks) : testForks
|
||||
maxParallelForks = Math.min(gradle.startParameter.maxWorkerCount, moduleForks)
|
||||
jvmArgs('-XX:ParallelGCThreads=2', '-XX:ConcGCThreads=1', '-XX:CICompilerCount=2')
|
||||
timeout = java.time.Duration.ofMinutes(60)
|
||||
}
|
||||
|
||||
java {
|
||||
@@ -731,6 +735,7 @@ pluginPackaging {
|
||||
policyName = 'Iris Bukkit'
|
||||
packed = true
|
||||
packedJarAccessors = ['art/arcane/iris/platform/bukkit/plugin/VolmitPlugin#getJarFile']
|
||||
packedPayloadOnlyPrefixes = ['iris-language-guides/']
|
||||
stripDirectories = true
|
||||
stripLocalVariables = true
|
||||
releaseCompression = providers.gradleProperty('compactRelease')
|
||||
|
||||
+16
-3
@@ -108,6 +108,10 @@ dependencies {
|
||||
compileOnly(libs.log4j.api)
|
||||
compileOnly(libs.log4j.core)
|
||||
compileOnly(libs.guava)
|
||||
compileOnly(libs.lwjgl)
|
||||
compileOnly(libs.lwjgl.opengl)
|
||||
compileOnly(libs.lwjgl.egl)
|
||||
compileOnly(libs.lwjgl.jawt)
|
||||
|
||||
// Third Party Integrations
|
||||
compileOnly(libs.nexo)
|
||||
@@ -161,23 +165,32 @@ dependencies {
|
||||
slim(libs.byteBuddy.agent)
|
||||
slim(libs.dom4j)
|
||||
slim(libs.jaxen)
|
||||
slim(libs.flatlaf)
|
||||
slim(libs.flatlaf.extras)
|
||||
|
||||
testFixturesImplementation('junit:junit:4.13.2')
|
||||
testFixturesImplementation('org.mockito:mockito-core:5.23.0')
|
||||
testFixturesImplementation(libs.mockito.core)
|
||||
testFixturesCompileOnly(libs.paper.api)
|
||||
|
||||
testImplementation('junit:junit:4.13.2')
|
||||
testImplementation('org.mockito:mockito-core:5.23.0')
|
||||
testImplementation(libs.mockito.core)
|
||||
testImplementation(libs.paper.api)
|
||||
testImplementation(libs.log4j.core)
|
||||
testRuntimeOnly(libs.paper.api)
|
||||
testImplementation(libs.multiverseCore)
|
||||
testImplementation(libs.annotations)
|
||||
testImplementation(libs.mythic)
|
||||
testRuntimeOnly(libs.lwjgl)
|
||||
testRuntimeOnly(libs.lwjgl.opengl)
|
||||
testRuntimeOnly(libs.lwjgl.egl)
|
||||
testRuntimeOnly(libs.lwjgl.jawt)
|
||||
testRuntimeOnly(variantOf(libs.lwjgl) { classifier('natives-linux') })
|
||||
testRuntimeOnly(variantOf(libs.lwjgl.opengl) { classifier('natives-linux') })
|
||||
}
|
||||
|
||||
tasks.named('test').configure {
|
||||
maxHeapSize = '1g'
|
||||
timeout = java.time.Duration.ofMinutes(60)
|
||||
maxHeapSize = '2g'
|
||||
jvmArgs('-XX:+ExitOnOutOfMemoryError')
|
||||
systemProperty('iris.generation.counters', 'true')
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ art/arcane/iris/generation/decoration/IrisSpeleothems.java
|
||||
art/arcane/iris/generation/decoration/IrisSurface.java
|
||||
art/arcane/iris/generation/decoration/tree/IrisTree.java
|
||||
art/arcane/iris/generation/geometry/AxisAlignedBB.java
|
||||
art/arcane/iris/generation/mantle/MantleWriter.java
|
||||
art/arcane/iris/generation/runtime/BukkitEngineWorldManager.java
|
||||
art/arcane/iris/generation/runtime/EngineAssignedWorldManager.java
|
||||
art/arcane/iris/generation/runtime/EnginePlayer.java
|
||||
@@ -57,7 +56,6 @@ art/arcane/iris/integration/data/NexoDataProvider.java
|
||||
art/arcane/iris/integration/data/OraxenDataProvider.java
|
||||
art/arcane/iris/localization/C.java
|
||||
art/arcane/iris/pack/IrisConverter.java
|
||||
art/arcane/iris/pack/IrisPack.java
|
||||
art/arcane/iris/pack/IrisPackBenchmarking.java
|
||||
art/arcane/iris/pack/datapack/DatapackIngestService.java
|
||||
art/arcane/iris/pack/datapack/ServerConfigurator.java
|
||||
@@ -126,8 +124,10 @@ art/arcane/iris/studio/jigsaw/JigsawStudioAutosaveScheduler.java
|
||||
art/arcane/iris/studio/jigsaw/JigsawStudioCapture.java
|
||||
art/arcane/iris/studio/jigsaw/JigsawStudioCaptureScheduler.java
|
||||
art/arcane/iris/studio/jigsaw/JigsawStudioChunkWriter.java
|
||||
art/arcane/iris/studio/jigsaw/JigsawStudioConnectorTool.java
|
||||
art/arcane/iris/studio/jigsaw/JigsawStudioDetailsMenu.java
|
||||
art/arcane/iris/studio/jigsaw/JigsawStudioDirtyTracker.java
|
||||
art/arcane/iris/studio/jigsaw/JigsawStudioFloorPainter.java
|
||||
art/arcane/iris/studio/jigsaw/JigsawStudioGraphMutations.java
|
||||
art/arcane/iris/studio/jigsaw/JigsawStudioMainMenu.java
|
||||
art/arcane/iris/studio/jigsaw/JigsawStudioMarkerParser.java
|
||||
|
||||
+7
-23
@@ -1,25 +1,14 @@
|
||||
package art.arcane.iris.command.specialhandlers;
|
||||
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyFeatureQuery;
|
||||
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
|
||||
import art.arcane.iris.command.DirectorParameterHandler;
|
||||
import art.arcane.iris.generation.hydrology.query.HydrologyFeatureType;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.director.exceptions.DirectorParsingException;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public final class HydrologyTypeHandler implements DirectorParameterHandler<String> {
|
||||
@Override
|
||||
public KList<String> getPossibilities() {
|
||||
Engine activeEngine = engine();
|
||||
if (activeEngine == null) {
|
||||
return new KList<>(HydrologyFeatureQuery.suggestions(List.of()));
|
||||
}
|
||||
IrisHydrologyRuntime runtime = activeEngine.getComplex().getHydrologyRuntime();
|
||||
return new KList<>(runtime == null
|
||||
? HydrologyFeatureQuery.suggestions(List.of())
|
||||
: runtime.featureQueryKeys());
|
||||
return new KList<>(HydrologyFeatureType.keys());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -29,16 +18,11 @@ public final class HydrologyTypeHandler implements DirectorParameterHandler<Stri
|
||||
|
||||
@Override
|
||||
public String parse(String input, boolean force) throws DirectorParsingException {
|
||||
String normalized = input == null ? "" : input.trim();
|
||||
if (normalized.isEmpty()) {
|
||||
throw new DirectorParsingException("Hydrology feature type must not be blank.");
|
||||
try {
|
||||
return HydrologyFeatureType.parse(input).key();
|
||||
} catch (IllegalArgumentException error) {
|
||||
throw new DirectorParsingException(error.getMessage());
|
||||
}
|
||||
for (String possibility : getPossibilities(normalized)) {
|
||||
if (possibility.equalsIgnoreCase(normalized)) {
|
||||
return possibility;
|
||||
}
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -48,6 +32,6 @@ public final class HydrologyTypeHandler implements DirectorParameterHandler<Stri
|
||||
|
||||
@Override
|
||||
public String getRandomDefault() {
|
||||
return "surface";
|
||||
return HydrologyFeatureType.RIVER.key();
|
||||
}
|
||||
}
|
||||
@@ -373,6 +373,7 @@ public class IrisSettings {
|
||||
public boolean useServerLaunchedGuis = true;
|
||||
public boolean maximumPregenGuiFPS = false;
|
||||
public boolean colorMode = true;
|
||||
public String theme = "auto";
|
||||
}
|
||||
|
||||
@Data
|
||||
|
||||
@@ -26,7 +26,7 @@ import art.arcane.iris.generation.decoration.IrisDepositVariant;
|
||||
import art.arcane.iris.generation.decoration.IrisOreGenerator;
|
||||
import art.arcane.iris.generation.decoration.IrisOreBands;
|
||||
import art.arcane.iris.generation.decoration.IrisProceduralObjects;
|
||||
import art.arcane.iris.generation.hydrology.IrisRiverPolicy;
|
||||
import art.arcane.iris.generation.hydrology.config.IrisHydrologyPolicy;
|
||||
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
|
||||
import art.arcane.iris.generation.noise.NoiseStyle;
|
||||
import art.arcane.iris.generation.terrain.InferredType;
|
||||
@@ -161,8 +161,8 @@ public class IrisBiome extends IrisRegistrant implements Rarity {
|
||||
private int lockLayersMax = 7;
|
||||
@Description("Profile-driven 3D cave configuration")
|
||||
private IrisCaveProfile caveProfile = new IrisCaveProfile();
|
||||
@Description("Biome-level river placement, routing, profile, biome, and geometry policy. Omit to inherit region and dimension settings.")
|
||||
private IrisRiverPolicy riverPolicy = null;
|
||||
@Description("Water settings for this biome: rainfall, pond density, where streams and ponds may form, and the fluid that fills them. A value left unset comes from the region, then from the dimension.")
|
||||
private IrisHydrologyPolicy hydrologyPolicy = null;
|
||||
@MinNumber(1)
|
||||
@MaxNumber(512)
|
||||
@Description("The rarity of this biome (integer)")
|
||||
@@ -580,6 +580,10 @@ public class IrisBiome extends IrisRegistrant implements Rarity {
|
||||
return IrisBiomeLayerGenerator.getLayerSeaHeightGenerators(this, rng, data);
|
||||
}
|
||||
|
||||
public KList<CNG> getCaveCeilingLayerHeightGenerators(RNG rng, IrisData data) {
|
||||
return IrisBiomeLayerGenerator.getCaveCeilingLayerHeightGenerators(this, rng, data);
|
||||
}
|
||||
|
||||
public NativeBlockState getSurfaceBlock(int x, int z, RNG rng, IrisData idm) {
|
||||
return IrisBiomeLayerGenerator.getSurfaceBlock(this, x, z, rng, idm);
|
||||
}
|
||||
|
||||
@@ -148,8 +148,7 @@ final class IrisBiomeLayerGenerator {
|
||||
return data;
|
||||
}
|
||||
|
||||
KList<CNG> heightGenerators = getHeightGenerators(
|
||||
biome.getLayerCeilingHeightGenerators(), ceilingLayers, 7235, random, rdata);
|
||||
KList<CNG> heightGenerators = getCaveCeilingLayerHeightGenerators(biome, random, rdata);
|
||||
|
||||
for (int i = 0; i < layerCount; i++) {
|
||||
IrisBiomePaletteLayer layer = ceilingLayers.get(i);
|
||||
@@ -317,6 +316,10 @@ final class IrisBiomeLayerGenerator {
|
||||
return getHeightGenerators(biome.getLayerSeaHeightGenerators(), biome.getSeaLayers(), 7735, rng, data);
|
||||
}
|
||||
|
||||
static KList<CNG> getCaveCeilingLayerHeightGenerators(IrisBiome biome, RNG rng, IrisData data) {
|
||||
return getHeightGenerators(biome.getLayerCeilingHeightGenerators(), biome.getCaveCeilingLayers(), 7235, rng, data);
|
||||
}
|
||||
|
||||
private static KList<CNG> getHeightGenerators(IrisBiomeLayerHeightCache cache,
|
||||
KList<IrisBiomePaletteLayer> layers,
|
||||
int seedOffset, RNG rng, IrisData data) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import art.arcane.iris.generation.runtime.DimensionStackLayout;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.generation.runtime.EngineMetrics;
|
||||
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
|
||||
import art.arcane.iris.generation.hydrology.raster.ChunkWaterRaster;
|
||||
import art.arcane.iris.generation.biome.FloatingIslandBoundarySampler;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.mantle.CaveTerrainSnapshot;
|
||||
@@ -25,6 +25,7 @@ public class ChunkContext {
|
||||
private FloatingBiomeOverlay floatingBiomes;
|
||||
private CaveTerrainSnapshot caveTerrain;
|
||||
private TransitionTerrainColumn[] transitionTerrain;
|
||||
private volatile ChunkWaterRaster waterRaster;
|
||||
private final int x;
|
||||
private final int z;
|
||||
private final IrisComplex complex;
|
||||
@@ -89,10 +90,6 @@ public class ChunkContext {
|
||||
PrefillPlan resolvedPlan = prefillPlan == null ? PrefillPlan.NO_CAVE : prefillPlan;
|
||||
boolean capturePrefillMetric = metrics != null;
|
||||
long totalStartNanos = capturePrefillMetric ? System.nanoTime() : 0L;
|
||||
IrisHydrologyRuntime hydrologyRuntime = complex.getHydrologyRuntime();
|
||||
if (hydrologyRuntime != null && (resolvedPlan.height || resolvedPlan.biome)) {
|
||||
hydrologyRuntime.prepareChunkColumns(x, z);
|
||||
}
|
||||
if (shouldPrefillAsync(resolvedPlan, complex.isPlatformMainThread())) {
|
||||
prefillAsync(resolvedPlan);
|
||||
} else {
|
||||
@@ -115,6 +112,9 @@ public class ChunkContext {
|
||||
}
|
||||
|
||||
private void prefillSurface(PrefillPlan prefillPlan) {
|
||||
if (prefillPlan.height || prefillPlan.biome) {
|
||||
waterRaster();
|
||||
}
|
||||
if (prefillPlan.height) {
|
||||
height.fillRounded(roundedHeight);
|
||||
}
|
||||
@@ -209,6 +209,18 @@ public class ChunkContext {
|
||||
return complex;
|
||||
}
|
||||
|
||||
public ChunkWaterRaster waterRaster() {
|
||||
ChunkWaterRaster known = waterRaster;
|
||||
if (known != null) {
|
||||
return known;
|
||||
}
|
||||
ChunkWaterRaster raster = complex.getHydrologyRuntime().raster(x >> 4, z >> 4);
|
||||
if (!raster.provisional()) {
|
||||
waterRaster = raster;
|
||||
}
|
||||
return raster;
|
||||
}
|
||||
|
||||
public FloatingIslandBoundarySampler getFloatingIslandBoundarySampler() {
|
||||
return floatingIslandBoundarySampler;
|
||||
}
|
||||
|
||||
@@ -542,7 +542,7 @@ final class DecoratorCore {
|
||||
return stack;
|
||||
}
|
||||
|
||||
private static boolean allowsSurface(IrisDecorator decorator, NativeBlockState surface, IrisData data) {
|
||||
static boolean allowsSurface(IrisDecorator decorator, NativeBlockState surface, IrisData data) {
|
||||
return decorator.isForcePlace()
|
||||
|| (decorator.getWhitelist() == null || matchesPalette(decorator.getWhitelistArray(data), surface))
|
||||
&& (decorator.getBlacklist() == null || !matchesPalette(decorator.getBlacklistArray(data), surface));
|
||||
|
||||
@@ -0,0 +1,390 @@
|
||||
package art.arcane.iris.generation.decoration;
|
||||
|
||||
import art.arcane.iris.generation.block.B;
|
||||
import art.arcane.iris.generation.block.IrisBlockData;
|
||||
import art.arcane.iris.generation.hydrology.plan.HydrologySettings;
|
||||
import art.arcane.iris.generation.hydrology.raster.WaterRole;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.hunk.Hunk;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public final class HydrologyVegetation {
|
||||
private static final long SALT = 0x4859445256454745L;
|
||||
private static final long GOLDEN = 0x9e3779b97f4a7c15L;
|
||||
private static final int[][] SIDES = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||
|
||||
private final long seed;
|
||||
private final Engine engine;
|
||||
private final RNG floorRng;
|
||||
private final RNG surfaceRng;
|
||||
private final RNG shoreRng;
|
||||
|
||||
public HydrologyVegetation(long seed, Engine engine) {
|
||||
this.seed = seed ^ SALT;
|
||||
this.engine = engine;
|
||||
this.floorRng = new RNG(DecoratorCore.partSeed(this.seed, IrisDecorationPart.SEA_FLOOR));
|
||||
this.surfaceRng = new RNG(DecoratorCore.partSeed(this.seed, IrisDecorationPart.SEA_SURFACE));
|
||||
this.shoreRng = new RNG(DecoratorCore.partSeed(this.seed, IrisDecorationPart.SHORE_LINE));
|
||||
}
|
||||
|
||||
public void decorate(Hunk<NativeBlockState> output, int localX, int localZ, int x, int z, WaterRole role, Water water, IrisData data) {
|
||||
Placement placement = placement(role);
|
||||
if (placement == Placement.NONE) {
|
||||
return;
|
||||
}
|
||||
Decorators decorators = water.decorators(x, z);
|
||||
if (decorators.empty()) {
|
||||
return;
|
||||
}
|
||||
int ground = water.ground(x, z);
|
||||
if (ground < 0 || ground + 1 >= output.getHeight()) {
|
||||
return;
|
||||
}
|
||||
Column column = new Column(output, localX, localZ, x, z, ground, water, data);
|
||||
if (placement == Placement.SHORE) {
|
||||
decorateShore(column, decorators.shore);
|
||||
return;
|
||||
}
|
||||
int head = water.head(x, z);
|
||||
if (head <= ground || head + 1 >= output.getHeight()) {
|
||||
return;
|
||||
}
|
||||
decorateFloor(column, head, decorators.floor);
|
||||
if (placement == Placement.STILL && decorators.surface.length > 0 && still(water, x, z, head)) {
|
||||
decorateSurface(column, head, decorators.surface);
|
||||
}
|
||||
}
|
||||
|
||||
private static Placement placement(WaterRole role) {
|
||||
return switch (role) {
|
||||
case CHANNEL, POND -> Placement.STILL;
|
||||
case CASCADE, WATERFALL -> Placement.FLOOR;
|
||||
case BANK, SHORE, POND_SHORE -> Placement.SHORE;
|
||||
case NONE, DAM, MOUTH, INLET -> Placement.NONE;
|
||||
};
|
||||
}
|
||||
|
||||
private void decorateFloor(Column column, int head, IrisDecorator[] bucket) {
|
||||
int room = head - column.ground - 1;
|
||||
if (room < 1 || bucket.length == 0) {
|
||||
return;
|
||||
}
|
||||
IrisDecorator decorator = pick(bucket, floorRng, column, IrisDecorationPart.SEA_FLOOR);
|
||||
NativeBlockState support = column.output.get(column.localX, column.ground, column.localZ);
|
||||
if (decorator == null || !B.isSolid(support) || !column.accepts(decorator, support)) {
|
||||
return;
|
||||
}
|
||||
column.place(decorator, floorRng, column.ground + 1, room, Slot.FLOOR, null);
|
||||
}
|
||||
|
||||
private void decorateSurface(Column column, int head, IrisDecorator[] bucket) {
|
||||
NativeBlockState support = column.output.get(column.localX, head, column.localZ);
|
||||
if (!isWaterSource(support)) {
|
||||
return;
|
||||
}
|
||||
IrisDecorator decorator = pick(bucket, surfaceRng, column, IrisDecorationPart.SEA_SURFACE);
|
||||
if (decorator == null || !column.accepts(decorator, support)) {
|
||||
return;
|
||||
}
|
||||
column.place(decorator, surfaceRng, head + 1, column.output.getHeight() - head - 1, Slot.SURFACE, null);
|
||||
}
|
||||
|
||||
private void decorateShore(Column column, IrisDecorator[] bucket) {
|
||||
NativeBlockState support = column.output.get(column.localX, column.ground, column.localZ);
|
||||
if (bucket.length == 0 || support == null || !support.isSolid()) {
|
||||
return;
|
||||
}
|
||||
IrisDecorator decorator = pick(bucket, shoreRng, column, IrisDecorationPart.SHORE_LINE);
|
||||
if (decorator == null || !column.accepts(decorator, support)) {
|
||||
return;
|
||||
}
|
||||
column.place(decorator, shoreRng, column.ground + 1, column.output.getHeight() - column.ground - 1, Slot.SHORE, support);
|
||||
}
|
||||
|
||||
private IrisDecorator pick(IrisDecorator[] bucket, RNG partRng, Column column, IrisDecorationPart part) {
|
||||
IrisDecorator picked = null;
|
||||
long choice = seed + part.ordinal();
|
||||
int count = 0;
|
||||
for (IrisDecorator decorator : bucket) {
|
||||
if (!decorator.passesChanceGate(partRng, column.x, column.z, column.data)) {
|
||||
continue;
|
||||
}
|
||||
count++;
|
||||
choice = mix(choice ^ mix(column.x * 0x632be59bd9b4e019L + column.z * 0x8cb92ba72f3d8dd7L + count));
|
||||
if (count == 1 || Long.remainderUnsigned(choice, count) == 0L) {
|
||||
picked = decorator;
|
||||
}
|
||||
}
|
||||
if (picked == null || picked.isForcePlace() || picked.getSlopeCondition().isDefault()
|
||||
|| picked.getSlopeCondition().isValid(column.water.slope(column.x, column.z))) {
|
||||
return picked;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static boolean still(Water water, int x, int z, int head) {
|
||||
for (int[] side : SIDES) {
|
||||
int neighbourX = x + side[0];
|
||||
int neighbourZ = z + side[1];
|
||||
int neighbourHead = water.head(neighbourX, neighbourZ);
|
||||
if (neighbourHead != head && water.ground(neighbourX, neighbourZ) < neighbourHead) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static boolean isWaterSource(NativeBlockState state) {
|
||||
if (state == null || !state.isWater()) {
|
||||
return false;
|
||||
}
|
||||
String level = IrisProceduralBlocks.propertyValue(state, "level");
|
||||
return level == null || level.equals("0");
|
||||
}
|
||||
|
||||
static boolean holdsWater(NativeBlockState state) {
|
||||
return B.isFluid(state) || B.isWaterLogged(state) || IrisSurfaceDecorator.isAquaticPlacement(state);
|
||||
}
|
||||
|
||||
static boolean plannedWater(Water water, int x, int z, int y) {
|
||||
int ground = water.ground(x, z);
|
||||
int top = water.head(x, z);
|
||||
if (y <= ground || top <= ground) {
|
||||
return false;
|
||||
}
|
||||
for (int[] side : SIDES) {
|
||||
int neighbourHead = water.head(x + side[0], z + side[1]);
|
||||
if (neighbourHead > top && water.ground(x + side[0], z + side[1]) < neighbourHead) {
|
||||
top = neighbourHead;
|
||||
}
|
||||
}
|
||||
return y <= top;
|
||||
}
|
||||
|
||||
private static boolean twoBlock(NativeBlockState state) {
|
||||
String half = IrisProceduralBlocks.propertyValue(state, "half");
|
||||
return "lower".equals(half) || "upper".equals(half);
|
||||
}
|
||||
|
||||
private static long mix(long value) {
|
||||
long mixed = (value + GOLDEN ^ (value + GOLDEN >>> 30)) * 0xbf58476d1ce4e5b9L;
|
||||
mixed = (mixed ^ (mixed >>> 27)) * 0x94d049bb133111ebL;
|
||||
return mixed ^ (mixed >>> 31);
|
||||
}
|
||||
|
||||
public interface Water {
|
||||
int ground(int x, int z);
|
||||
|
||||
int head(int x, int z);
|
||||
|
||||
double slope(int x, int z);
|
||||
|
||||
Decorators decorators(int x, int z);
|
||||
}
|
||||
|
||||
private enum Placement {
|
||||
NONE,
|
||||
SHORE,
|
||||
FLOOR,
|
||||
STILL
|
||||
}
|
||||
|
||||
private enum Slot {
|
||||
FLOOR,
|
||||
SURFACE,
|
||||
SHORE
|
||||
}
|
||||
|
||||
private final class Column {
|
||||
private final Hunk<NativeBlockState> output;
|
||||
private final int localX;
|
||||
private final int localZ;
|
||||
private final int x;
|
||||
private final int z;
|
||||
private final int ground;
|
||||
private final Water water;
|
||||
private final IrisData data;
|
||||
|
||||
private Column(Hunk<NativeBlockState> output, int localX, int localZ, int x, int z, int ground, Water water, IrisData data) {
|
||||
this.output = output;
|
||||
this.localX = localX;
|
||||
this.localZ = localZ;
|
||||
this.x = x;
|
||||
this.z = z;
|
||||
this.ground = ground;
|
||||
this.water = water;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
private boolean accepts(IrisDecorator decorator, NativeBlockState support) {
|
||||
return decorator.isForcePlace() || DecoratorCore.allowsSurface(decorator, support, data);
|
||||
}
|
||||
|
||||
private int stack(IrisDecorator decorator, RNG rng, int room) {
|
||||
return decorator.isStacking() ? Math.min(room, DecoratorCore.computeStack(decorator, rng, x, z, data, room)) : 1;
|
||||
}
|
||||
|
||||
private void place(IrisDecorator decorator, RNG rng, int baseY, int room, Slot slot, NativeBlockState support) {
|
||||
NativeBlockState first = decorator.pickBlockData(rng, data, x, z);
|
||||
if (first == null) {
|
||||
return;
|
||||
}
|
||||
if (twoBlock(first)) {
|
||||
if (room >= 2) {
|
||||
placePair(decorator, first, baseY, slot, support);
|
||||
}
|
||||
return;
|
||||
}
|
||||
int stack = stack(decorator, rng, room);
|
||||
NativeBlockState sampled = first;
|
||||
boolean sampledTop = false;
|
||||
for (int index = 0; index < stack; index++) {
|
||||
int y = baseY + index;
|
||||
boolean top = (stack == 1 ? 1D : (double) index / (stack - 1)) >= decorator.getTopThreshold();
|
||||
if (top != sampledTop) {
|
||||
sampled = top ? decorator.pickBlockDataTop(rng, data, x, z) : decorator.pickBlockData(rng, data, x, z);
|
||||
sampledTop = top;
|
||||
}
|
||||
NativeBlockState block = sampled == null ? null : fit(sampled, y, slot, decorator, index == 0 ? support : null);
|
||||
if (block == null) {
|
||||
return;
|
||||
}
|
||||
output.set(localX, y, localZ, block);
|
||||
}
|
||||
}
|
||||
|
||||
private void placePair(IrisDecorator decorator, NativeBlockState block, int y, Slot slot, NativeBlockState support) {
|
||||
NativeBlockState lower = fit(block.withProperty("half", "lower"), y, slot, decorator, support);
|
||||
NativeBlockState upper = lower == null ? null : fit(block.withProperty("half", "upper"), y + 1, slot, decorator, null);
|
||||
if (upper != null) {
|
||||
output.set(localX, y, localZ, lower);
|
||||
output.set(localX, y + 1, localZ, upper);
|
||||
}
|
||||
}
|
||||
|
||||
private NativeBlockState fit(NativeBlockState block, int y, Slot slot, IrisDecorator decorator, NativeBlockState support) {
|
||||
NativeBlockState existing = output.get(localX, y, localZ);
|
||||
if (slot == Slot.FLOOR) {
|
||||
NativeBlockState placed = IrisProceduralBlocks.normalizeWaterlogging(block, existing, true);
|
||||
return isWaterSource(existing) && IrisSurfaceDecorator.isAquaticPlacement(placed) ? placed : null;
|
||||
}
|
||||
if (!B.isAir(existing) || holdsWater(block)) {
|
||||
return null;
|
||||
}
|
||||
if (slot == Slot.SURFACE) {
|
||||
return block;
|
||||
}
|
||||
if (besideWater(y) || support != null && !DecoratorCore.isValidShorelineSupport(decorator, block, support)) {
|
||||
return null;
|
||||
}
|
||||
return IrisSugarCane.canPlace(block, output, localX, y, localZ, x, z, engine) ? block : null;
|
||||
}
|
||||
|
||||
private boolean besideWater(int y) {
|
||||
for (int[] side : SIDES) {
|
||||
int neighbourX = localX + side[0];
|
||||
int neighbourZ = localZ + side[1];
|
||||
if (neighbourX >= 0 && neighbourX < output.getWidth() && neighbourZ >= 0 && neighbourZ < output.getDepth()) {
|
||||
if (holdsWater(output.get(neighbourX, y, neighbourZ))) {
|
||||
return true;
|
||||
}
|
||||
} else if (plannedWater(water, x + side[0], z + side[1], y)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Decorators {
|
||||
public static final Decorators EMPTY = new Decorators(new IrisDecorator[0], new IrisDecorator[0], new IrisDecorator[0]);
|
||||
|
||||
private final IrisDecorator[] floor;
|
||||
private final IrisDecorator[] surface;
|
||||
private final IrisDecorator[] shore;
|
||||
|
||||
private Decorators(IrisDecorator[] floor, IrisDecorator[] surface, IrisDecorator[] shore) {
|
||||
this.floor = floor;
|
||||
this.surface = surface;
|
||||
this.shore = shore;
|
||||
}
|
||||
|
||||
public static Decorators compile(List<IrisDecorator> decorators, IrisData data, String path) {
|
||||
HydrologySettings.requireDecorators(decorators, path);
|
||||
if (decorators == null || decorators.isEmpty()) {
|
||||
return EMPTY;
|
||||
}
|
||||
List<IrisDecorator> floor = new ArrayList<>();
|
||||
List<IrisDecorator> surface = new ArrayList<>();
|
||||
List<IrisDecorator> shore = new ArrayList<>();
|
||||
for (int index = 0; index < decorators.size(); index++) {
|
||||
IrisDecorator decorator = decorators.get(index);
|
||||
String field = path + "[" + index + "]";
|
||||
IrisDecorationPart part = decorator.getPartOf();
|
||||
requirePlaced(decorator.getPalette(), data, field + ".palette", part);
|
||||
requirePlaced(decorator.getTopPalette(), data, field + ".topPalette", part);
|
||||
requireKnown(decorator.getWhitelist(), data, field + ".whitelist");
|
||||
requireKnown(decorator.getBlacklist(), data, field + ".blacklist");
|
||||
switch (part) {
|
||||
case SEA_FLOOR -> floor.add(decorator);
|
||||
case SEA_SURFACE -> surface.add(decorator);
|
||||
default -> shore.add(decorator);
|
||||
}
|
||||
}
|
||||
return new Decorators(floor.toArray(new IrisDecorator[0]), surface.toArray(new IrisDecorator[0]), shore.toArray(new IrisDecorator[0]));
|
||||
}
|
||||
|
||||
public boolean empty() {
|
||||
return floor.length == 0 && surface.length == 0 && shore.length == 0;
|
||||
}
|
||||
|
||||
private static void requirePlaced(List<IrisBlockData> blocks, IrisData data, String path, IrisDecorationPart part) {
|
||||
if (blocks == null) {
|
||||
return;
|
||||
}
|
||||
for (int index = 0; index < blocks.size(); index++) {
|
||||
String field = path + "[" + index + "]";
|
||||
String name = blocks.get(index).getBlock();
|
||||
NativeBlockState state = requireKnown(blocks.get(index), data, field);
|
||||
if (state.isFluid()) {
|
||||
throw new IllegalArgumentException(field + " names the fluid '" + name + "'; water decorators never place fluids");
|
||||
}
|
||||
if (part == IrisDecorationPart.SEA_FLOOR && !waterCompatible(state)) {
|
||||
throw new IllegalArgumentException(field + " names '" + name + "', which cannot stand in water; SEA_FLOOR decorators place"
|
||||
+ " only waterloggable or water plants such as seagrass, kelp, coral and sea pickles");
|
||||
}
|
||||
if (part != IrisDecorationPart.SEA_FLOOR && holdsWater(state)) {
|
||||
throw new IllegalArgumentException(field + " names '" + name + "', which holds water; " + part
|
||||
+ " decorators grow in air and place only blocks without water, never waterlogged blocks, seagrass, kelp, sea pickles or coral");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void requireKnown(List<IrisBlockData> blocks, IrisData data, String path) {
|
||||
if (blocks == null) {
|
||||
return;
|
||||
}
|
||||
for (int index = 0; index < blocks.size(); index++) {
|
||||
requireKnown(blocks.get(index), data, path + "[" + index + "]");
|
||||
}
|
||||
}
|
||||
|
||||
private static NativeBlockState requireKnown(IrisBlockData block, IrisData data, String path) {
|
||||
NativeBlockState state = block.getBlockData(data);
|
||||
if (state == null || state.isAir()) {
|
||||
throw new IllegalArgumentException(path + " names the block '" + block.getBlock()
|
||||
+ "', which does not exist on this server; water decorators never place air");
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
private static boolean waterCompatible(NativeBlockState state) {
|
||||
return IrisSurfaceDecorator.isAquaticPlacement(state) || IrisProceduralBlocks.propertyValue(state, "waterlogged") != null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,6 @@ package art.arcane.iris.generation.decoration;
|
||||
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyColumnLayer;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyColumnSample;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.hunk.Hunk;
|
||||
@@ -71,21 +69,18 @@ final class IrisSugarCane {
|
||||
|
||||
private static boolean plannedWater(Engine engine, int x, int y, int z) {
|
||||
IrisComplex complex = engine.getComplex();
|
||||
HydrologyColumnSample sample = complex.sampleHydrologyColumn(x, z);
|
||||
HydrologyColumnLayer fluid = sample == null ? null : sample.primarySurfaceFluidLayerOrNull();
|
||||
if (fluid != null) {
|
||||
return y > fluid.bedY() && y <= fluid.fluidHeadY()
|
||||
&& supportsAdjacent(complex.resolveHydrologyFluid(fluid.profileKey(), x, z));
|
||||
}
|
||||
int terrainHeight = (int) Math.round(complex.getHeightStream().getDouble(x, z));
|
||||
int fluidHeight = (int) Math.round(complex.getRiverWaterSurfaceStream().getDouble(x, z));
|
||||
int fluidHeight = (int) Math.round(complex.getWaterHeadStream().getDouble(x, z));
|
||||
if (y <= terrainHeight || y > fluidHeight) {
|
||||
return false;
|
||||
}
|
||||
if (complex.getWaterRoleStream().get(x, z).wet()) {
|
||||
return supportsAdjacent(complex.resolveWaterFluid(x, z));
|
||||
}
|
||||
IrisBiome biome = complex.getTrueBiomeStream().get(x, z);
|
||||
KList<NativeBlockState> layers = biome.generateSeaLayers(x, z,
|
||||
new RNG(engine.getSeedManager().getTerrain()), fluidHeight - terrainHeight, engine.getData());
|
||||
int depth = fluidHeight - y;
|
||||
return supportsAdjacent(layers.hasIndex(depth) ? layers.get(depth) : complex.resolveSurfaceFluid(x, z));
|
||||
return supportsAdjacent(layers.hasIndex(depth) ? layers.get(depth) : complex.resolveSeaFluid(x, z));
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.cave.CavePosition;
|
||||
|
||||
record CaveCandidateBounds(
|
||||
int entryX,
|
||||
int entryY,
|
||||
int entryZ,
|
||||
int waterHeadY,
|
||||
int horizontalRadius,
|
||||
int maximumDepth,
|
||||
int dryHeadroom
|
||||
) {
|
||||
boolean containsColumn(int x, int z) {
|
||||
long deltaX = (long) x - entryX;
|
||||
long deltaZ = (long) z - entryZ;
|
||||
long radiusSquared = (long) horizontalRadius * horizontalRadius;
|
||||
return deltaX * deltaX + deltaZ * deltaZ <= radiusSquared;
|
||||
}
|
||||
|
||||
int minimumY() {
|
||||
return Math.toIntExact((long) entryY - maximumDepth);
|
||||
}
|
||||
|
||||
int maximumY() {
|
||||
return Math.max(entryY, Math.addExact(waterHeadY, dryHeadroom + 1));
|
||||
}
|
||||
|
||||
boolean contains(CavePosition position) {
|
||||
if (!containsColumn(position.x(), position.z())) {
|
||||
return false;
|
||||
}
|
||||
return position.y() >= minimumY() && position.y() <= maximumY();
|
||||
}
|
||||
}
|
||||
@@ -1,202 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.cave.CavePosition;
|
||||
import art.arcane.iris.generation.hydrology.cave.CavePositionIndex;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveAction;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCandidate;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveFluidPolicy;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveMode;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCavePlannerSettings;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveSource;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
|
||||
final class CaveCandidateBuilder {
|
||||
private final RiverCourse course;
|
||||
private final HydraulicSegment representative;
|
||||
private final LinkedHashMap<CavePosition, HydrologyCaveAction> actions;
|
||||
private final LinkedHashSet<CavePosition> openings;
|
||||
private final CavePositionIndex openingIndex;
|
||||
private final List<CaveSurfaceOpening> surfaceOpenings;
|
||||
private int minimumX;
|
||||
private int maximumX;
|
||||
private int minimumY;
|
||||
private int maximumY;
|
||||
private int minimumZ;
|
||||
private int maximumZ;
|
||||
|
||||
CaveCandidateBuilder(RiverCourse course, HydraulicSegment representative) {
|
||||
this.course = course;
|
||||
this.representative = representative;
|
||||
this.actions = new LinkedHashMap<>();
|
||||
this.openings = new LinkedHashSet<>();
|
||||
this.openingIndex = new CavePositionIndex();
|
||||
this.surfaceOpenings = HydrologyCaveCourseFilter.surfaceOpenings(course);
|
||||
this.minimumX = Integer.MAX_VALUE;
|
||||
this.maximumX = Integer.MIN_VALUE;
|
||||
this.minimumY = Integer.MAX_VALUE;
|
||||
this.maximumY = Integer.MIN_VALUE;
|
||||
this.minimumZ = Integer.MAX_VALUE;
|
||||
this.maximumZ = Integer.MIN_VALUE;
|
||||
}
|
||||
|
||||
RiverCourse course() {
|
||||
return course;
|
||||
}
|
||||
|
||||
private HydraulicSegment representative() {
|
||||
return representative;
|
||||
}
|
||||
|
||||
boolean isEmpty() {
|
||||
return actions.isEmpty();
|
||||
}
|
||||
|
||||
void addAction(CavePosition position, HydrologyCaveAction action) {
|
||||
HydrologyCaveAction existing = actions.get(position);
|
||||
if (existing == null || actionPriority(action) < actionPriority(existing)) {
|
||||
actions.put(position, action);
|
||||
}
|
||||
include(position);
|
||||
}
|
||||
|
||||
void addOpening(CavePosition position) {
|
||||
if (openingIndex.add(position.x(), position.y(), position.z())) {
|
||||
openings.add(position);
|
||||
}
|
||||
include(position);
|
||||
}
|
||||
|
||||
void addOpening(int x, int y, int z) {
|
||||
if (openingIndex.add(x, y, z)) {
|
||||
openings.add(new CavePosition(x, y, z));
|
||||
}
|
||||
include(x, y, z);
|
||||
}
|
||||
|
||||
void addOpeningNeighborhood(CavePosition position) {
|
||||
addOpening(position);
|
||||
for (int[] offset : HydrologyCaveCourseFilter.NEIGHBORS) {
|
||||
addOpening(
|
||||
position.x() + offset[0],
|
||||
position.y() + offset[1],
|
||||
position.z() + offset[2]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void addAdjacentSurfaceOpenings(
|
||||
HydrologyFootprintCompiler.ValidationRaster validation
|
||||
) {
|
||||
ArrayList<CavePosition> actionPositions = new ArrayList<>(actions.keySet());
|
||||
for (CavePosition position : actionPositions) {
|
||||
for (int[] offset : HydrologyCaveCourseFilter.HORIZONTAL_NEIGHBORS) {
|
||||
int neighborX = position.x() + offset[0];
|
||||
int neighborZ = position.z() + offset[1];
|
||||
if (actions.containsKey(new CavePosition(neighborX, position.y(), neighborZ))) {
|
||||
continue;
|
||||
}
|
||||
HydrologyColumnSample neighbor = validation.surfaceColumnAt(neighborX, neighborZ);
|
||||
if (neighbor == null) {
|
||||
continue;
|
||||
}
|
||||
for (HydrologyColumnLayer layer : neighbor.layers()) {
|
||||
if (layer.feature().courseId() == course.id() && layer.channel() && layer.fluidOwned()
|
||||
&& layer.bedY() < position.y() && position.y() <= layer.fluidHeadY()) {
|
||||
addOpening(neighborX, position.y(), neighborZ);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void addSurfaceOpening(HydrologyColumnLayer layer, CavePosition position) {
|
||||
for (CaveSurfaceOpening opening : surfaceOpenings) {
|
||||
if (!opening.matches(layer, position)) {
|
||||
continue;
|
||||
}
|
||||
if (opening.includeNeighborhood()) {
|
||||
addOpening(position);
|
||||
for (int[] offset : HydrologyCaveCourseFilter.NEIGHBORS) {
|
||||
int y = position.y() + offset[1];
|
||||
if (y >= opening.minimumY()) {
|
||||
addOpening(position.x() + offset[0], y, position.z() + offset[2]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
addOpening(position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void include(CavePosition position) {
|
||||
include(position.x(), position.y(), position.z());
|
||||
}
|
||||
|
||||
private void include(int x, int y, int z) {
|
||||
minimumX = Math.min(minimumX, x);
|
||||
maximumX = Math.max(maximumX, x);
|
||||
minimumY = Math.min(minimumY, y);
|
||||
maximumY = Math.max(maximumY, y);
|
||||
minimumZ = Math.min(minimumZ, z);
|
||||
maximumZ = Math.max(maximumZ, z);
|
||||
}
|
||||
|
||||
HydrologyCaveCandidate build(HydrologyCaveCourseFilter.Options options) {
|
||||
HydrologyPoint start = representative.start();
|
||||
int waterHead = representative.upstreamHeadY();
|
||||
int entryY = Math.max(waterHead, maximumY);
|
||||
CavePosition entry = new CavePosition(start.x(), entryY, start.z());
|
||||
CavePosition target = new CavePosition(start.x(), waterHead, start.z());
|
||||
int horizontalRadius = HydrologyCaveCourseFilter.horizontalRadius(
|
||||
entry.x(),
|
||||
entry.z(),
|
||||
minimumX,
|
||||
maximumX,
|
||||
minimumZ,
|
||||
maximumZ
|
||||
);
|
||||
int maximumDepth = Math.max(1, entryY - minimumY + 2);
|
||||
int dryHeadroom = Math.max(0, maximumY - waterHead);
|
||||
int volume = Math.max(1, actions.size());
|
||||
HydrologyCavePlannerSettings settings = new HydrologyCavePlannerSettings(
|
||||
horizontalRadius,
|
||||
maximumDepth,
|
||||
volume,
|
||||
1,
|
||||
1,
|
||||
dryHeadroom,
|
||||
HydrologyCaveFluidPolicy.REJECT_EXISTING,
|
||||
horizontalRadius,
|
||||
maximumDepth
|
||||
);
|
||||
return new HydrologyCaveCandidate(
|
||||
new HydrologyCaveSource(
|
||||
course.id(),
|
||||
entry,
|
||||
target,
|
||||
waterHead,
|
||||
HydrologyCaveMode.GENERATED_GROTTO
|
||||
),
|
||||
course.profileKey(),
|
||||
settings,
|
||||
options.connectToExistingCaves() && course.type() == RiverCourseType.UNDERGROUND,
|
||||
actions,
|
||||
openings
|
||||
);
|
||||
}
|
||||
|
||||
private static int actionPriority(HydrologyCaveAction action) {
|
||||
return switch (action) {
|
||||
case WET_SOURCE -> 0;
|
||||
case FALLING_FLUID -> 1;
|
||||
case DRY_AIR -> 2;
|
||||
case SEAL_GUARD -> 3;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
-395
@@ -1,395 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.cave.CavePosition;
|
||||
import art.arcane.iris.generation.hydrology.cave.CaveVoxelView;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
final class CaveCandidateSpanBuilder {
|
||||
private final RiverCourse course;
|
||||
private final HydraulicSegment representative;
|
||||
private final LinkedHashMap<Long, CaveSpanSet> actions;
|
||||
private final LinkedHashMap<Long, CaveSpanSet> openings;
|
||||
private final LinkedHashMap<Long, CaveSpanSet> coastalGrottoActions;
|
||||
private final LinkedHashMap<Long, CaveSpanSet> inlandGrottoActions;
|
||||
private final List<CaveSurfaceOpening> surfaceOpenings;
|
||||
private int minimumX;
|
||||
private int maximumX;
|
||||
private int minimumY;
|
||||
private int maximumY;
|
||||
private int minimumZ;
|
||||
private int maximumZ;
|
||||
|
||||
CaveCandidateSpanBuilder(RiverCourse course, HydraulicSegment representative) {
|
||||
this.course = course;
|
||||
this.representative = representative;
|
||||
this.actions = new LinkedHashMap<>();
|
||||
this.openings = new LinkedHashMap<>();
|
||||
this.coastalGrottoActions = new LinkedHashMap<>();
|
||||
this.inlandGrottoActions = new LinkedHashMap<>();
|
||||
this.surfaceOpenings = HydrologyCaveCourseFilter.surfaceOpenings(course);
|
||||
this.minimumX = Integer.MAX_VALUE;
|
||||
this.maximumX = Integer.MIN_VALUE;
|
||||
this.minimumY = Integer.MAX_VALUE;
|
||||
this.maximumY = Integer.MIN_VALUE;
|
||||
this.minimumZ = Integer.MAX_VALUE;
|
||||
this.maximumZ = Integer.MIN_VALUE;
|
||||
}
|
||||
|
||||
RiverCourse course() {
|
||||
return course;
|
||||
}
|
||||
|
||||
HydraulicSegment representative() {
|
||||
return representative;
|
||||
}
|
||||
|
||||
boolean isEmpty() {
|
||||
return actions.isEmpty();
|
||||
}
|
||||
|
||||
boolean allowsIntentionalSurfaceExposure() {
|
||||
return course.surfaceSinkholeContinuation();
|
||||
}
|
||||
|
||||
void addAction(int x, int z, HydrologyColumnLayer layer) {
|
||||
int minimumActionY = layer.bedY() + 1;
|
||||
int maximumActionY = layer.ceilingY();
|
||||
if (minimumActionY > maximumActionY) {
|
||||
return;
|
||||
}
|
||||
addSpan(actions, x, z, minimumActionY, maximumActionY);
|
||||
if (layer.feature().type() == HydrologyFeatureType.COASTAL_GROTTO) {
|
||||
addSpan(coastalGrottoActions, x, z, minimumActionY, maximumActionY);
|
||||
} else if (layer.feature().type() == HydrologyFeatureType.INLAND_GROTTO) {
|
||||
addSpan(inlandGrottoActions, x, z, minimumActionY, maximumActionY);
|
||||
}
|
||||
include(x, minimumActionY, z);
|
||||
include(x, maximumActionY, z);
|
||||
for (CaveSurfaceOpening opening : surfaceOpenings) {
|
||||
if (!opening.matchesColumn(layer, x, z)) {
|
||||
continue;
|
||||
}
|
||||
int minimumOpeningY = Math.max(minimumActionY, opening.minimumY());
|
||||
if (minimumOpeningY > maximumActionY) {
|
||||
continue;
|
||||
}
|
||||
if (!opening.includeNeighborhood()) {
|
||||
addOpening(x, z, minimumOpeningY, maximumActionY);
|
||||
continue;
|
||||
}
|
||||
addOpening(x, z, Math.max(minimumOpeningY - 1, opening.minimumY()), maximumActionY + 1);
|
||||
for (int[] offset : HydrologyCaveCourseFilter.HORIZONTAL_NEIGHBORS) {
|
||||
addOpening(x + offset[0], z + offset[1], minimumOpeningY, maximumActionY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void addOceanOpening(int x, int z, int minimumOpeningY, int maximumOpeningY) {
|
||||
if (minimumOpeningY <= maximumOpeningY) {
|
||||
addOpening(x, z, minimumOpeningY, maximumOpeningY);
|
||||
}
|
||||
}
|
||||
|
||||
void addOpeningNeighborhood(
|
||||
int x,
|
||||
int z,
|
||||
int minimumOpeningY,
|
||||
int maximumOpeningY
|
||||
) {
|
||||
if (minimumOpeningY > maximumOpeningY) {
|
||||
return;
|
||||
}
|
||||
addOpening(x, z, minimumOpeningY - 1, maximumOpeningY + 1);
|
||||
for (int[] offset : HydrologyCaveCourseFilter.HORIZONTAL_NEIGHBORS) {
|
||||
addOpening(x + offset[0], z + offset[1], minimumOpeningY, maximumOpeningY);
|
||||
}
|
||||
}
|
||||
|
||||
void addAdjacentSurfaceOpenings(
|
||||
HydrologyFootprintCompiler.ValidationRaster validation
|
||||
) {
|
||||
for (Map.Entry<Long, CaveSpanSet> entry : actions.entrySet()) {
|
||||
int x = RiverFootprint.unpackX(entry.getKey());
|
||||
int z = RiverFootprint.unpackZ(entry.getKey());
|
||||
for (int[] offset : HydrologyCaveCourseFilter.HORIZONTAL_NEIGHBORS) {
|
||||
int neighborX = x + offset[0];
|
||||
int neighborZ = z + offset[1];
|
||||
HydrologyColumnSample neighbor = validation.surfaceColumnAt(neighborX, neighborZ);
|
||||
if (neighbor == null) {
|
||||
continue;
|
||||
}
|
||||
for (HydrologyColumnLayer layer : neighbor.layers()) {
|
||||
if (layer.feature().courseId() != course.id() || !layer.channel() || !layer.fluidOwned()) {
|
||||
continue;
|
||||
}
|
||||
for (CaveYSpan span : entry.getValue().spans()) {
|
||||
int minimumY = Math.max(span.minimumY(), layer.bedY() + 1);
|
||||
int maximumY = Math.min(span.maximumY(), layer.fluidHeadY());
|
||||
if (minimumY <= maximumY) {
|
||||
addAdjacentFluidOpening(neighborX, neighborZ, minimumY, maximumY);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addAdjacentFluidOpening(int x, int z, int minimumY, int maximumY) {
|
||||
CaveSpanSet owned = actions.get(RiverFootprint.pack(x, z));
|
||||
if (owned == null) {
|
||||
addOpening(x, z, minimumY, maximumY);
|
||||
return;
|
||||
}
|
||||
int cursor = minimumY;
|
||||
for (CaveYSpan span : owned.spans()) {
|
||||
if (span.maximumY() < cursor) {
|
||||
continue;
|
||||
}
|
||||
if (span.minimumY() > maximumY) {
|
||||
break;
|
||||
}
|
||||
if (cursor < span.minimumY()) {
|
||||
addOpening(x, z, cursor, Math.min(maximumY, span.minimumY() - 1));
|
||||
}
|
||||
cursor = Math.max(cursor, span.maximumY() + 1);
|
||||
if (cursor > maximumY) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
addOpening(x, z, cursor, maximumY);
|
||||
}
|
||||
|
||||
HydrologyFeatureType oversizedGrotto(HydrologyCaveCourseFilter.Options options) {
|
||||
if (positionCount(coastalGrottoActions)
|
||||
> options.maximumVolume(HydrologyFeatureType.COASTAL_GROTTO)) {
|
||||
return HydrologyFeatureType.COASTAL_GROTTO;
|
||||
}
|
||||
if (positionCount(inlandGrottoActions)
|
||||
> options.maximumVolume(HydrologyFeatureType.INLAND_GROTTO)) {
|
||||
return HydrologyFeatureType.INLAND_GROTTO;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
long positionCount() {
|
||||
return positionCount(actions);
|
||||
}
|
||||
|
||||
boolean exposed(CaveVoxelView view) {
|
||||
CaveCandidateBounds bounds = bounds();
|
||||
for (Map.Entry<Long, CaveSpanSet> entry : actions.entrySet()) {
|
||||
int x = RiverFootprint.unpackX(entry.getKey());
|
||||
int z = RiverFootprint.unpackZ(entry.getKey());
|
||||
CaveSpanSet actionSpans = entry.getValue();
|
||||
CaveSpanSet openingSpans = openings.get(entry.getKey());
|
||||
if (exposedDifference(actionSpans, null, openingSpans, x, z, bounds, view)) {
|
||||
return true;
|
||||
}
|
||||
for (CaveYSpan span : actionSpans.spans()) {
|
||||
int lowerBoundary = span.minimumY() - 1;
|
||||
if (!actionSpans.contains(lowerBoundary)
|
||||
&& !contains(openingSpans, lowerBoundary)
|
||||
&& exposed(x, lowerBoundary, z, bounds, view)) {
|
||||
return true;
|
||||
}
|
||||
int upperBoundary = span.maximumY() + 1;
|
||||
if (!actionSpans.contains(upperBoundary)
|
||||
&& !contains(openingSpans, upperBoundary)
|
||||
&& exposed(x, upperBoundary, z, bounds, view)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (int[] offset : HydrologyCaveCourseFilter.HORIZONTAL_NEIGHBORS) {
|
||||
int neighborX = x + offset[0];
|
||||
int neighborZ = z + offset[1];
|
||||
long neighborKey = RiverFootprint.pack(neighborX, neighborZ);
|
||||
if (exposedDifference(
|
||||
actionSpans,
|
||||
actions.get(neighborKey),
|
||||
openings.get(neighborKey),
|
||||
neighborX,
|
||||
neighborZ,
|
||||
bounds,
|
||||
view
|
||||
)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean exposedDifference(
|
||||
CaveSpanSet source,
|
||||
CaveSpanSet excludedActions,
|
||||
CaveSpanSet excludedOpenings,
|
||||
int x,
|
||||
int z,
|
||||
CaveCandidateBounds bounds,
|
||||
CaveVoxelView view
|
||||
) {
|
||||
for (CaveYSpan span : source.spans()) {
|
||||
if (exposedDifference(
|
||||
span,
|
||||
excludedActions,
|
||||
excludedOpenings,
|
||||
x,
|
||||
z,
|
||||
bounds,
|
||||
view
|
||||
)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean exposedDifference(
|
||||
CaveYSpan source,
|
||||
CaveSpanSet excludedActions,
|
||||
CaveSpanSet excludedOpenings,
|
||||
int x,
|
||||
int z,
|
||||
CaveCandidateBounds bounds,
|
||||
CaveVoxelView view
|
||||
) {
|
||||
List<CaveYSpan> actionSpans = excludedActions == null ? List.of() : excludedActions.spans();
|
||||
List<CaveYSpan> openingSpans = excludedOpenings == null ? List.of() : excludedOpenings.spans();
|
||||
int actionIndex = 0;
|
||||
int openingIndex = 0;
|
||||
long cursor = source.minimumY();
|
||||
while (cursor <= source.maximumY()) {
|
||||
while (actionIndex < actionSpans.size()
|
||||
&& actionSpans.get(actionIndex).maximumY() < cursor) {
|
||||
actionIndex++;
|
||||
}
|
||||
while (openingIndex < openingSpans.size()
|
||||
&& openingSpans.get(openingIndex).maximumY() < cursor) {
|
||||
openingIndex++;
|
||||
}
|
||||
CaveYSpan action = actionIndex < actionSpans.size() ? actionSpans.get(actionIndex) : null;
|
||||
CaveYSpan opening = openingIndex < openingSpans.size() ? openingSpans.get(openingIndex) : null;
|
||||
CaveYSpan excluded = first(action, opening);
|
||||
if (excluded == null || excluded.minimumY() > source.maximumY()) {
|
||||
return exposed(x, (int) cursor, source.maximumY(), z, bounds, view);
|
||||
}
|
||||
if (cursor < excluded.minimumY()
|
||||
&& exposed(x, (int) cursor, excluded.minimumY() - 1, z, bounds, view)) {
|
||||
return true;
|
||||
}
|
||||
cursor = Math.max(cursor, (long) excluded.maximumY() + 1L);
|
||||
if (excluded == action) {
|
||||
actionIndex++;
|
||||
} else {
|
||||
openingIndex++;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private CaveYSpan first(CaveYSpan first, CaveYSpan second) {
|
||||
if (first == null) {
|
||||
return second;
|
||||
}
|
||||
if (second == null) {
|
||||
return first;
|
||||
}
|
||||
return first.minimumY() <= second.minimumY() ? first : second;
|
||||
}
|
||||
|
||||
boolean exposed(
|
||||
int x,
|
||||
int minimumY,
|
||||
int maximumY,
|
||||
int z,
|
||||
CaveCandidateBounds bounds,
|
||||
CaveVoxelView view
|
||||
) {
|
||||
if (!bounds.containsColumn(x, z)) {
|
||||
return false;
|
||||
}
|
||||
int boundedMinimumY = Math.max(minimumY, bounds.minimumY());
|
||||
int boundedMaximumY = Math.min(maximumY, bounds.maximumY());
|
||||
return boundedMinimumY <= boundedMaximumY
|
||||
&& view.hasAboveTerrainSurface(x, z, boundedMinimumY, boundedMaximumY);
|
||||
}
|
||||
|
||||
boolean exposed(
|
||||
int x,
|
||||
int y,
|
||||
int z,
|
||||
CaveCandidateBounds bounds,
|
||||
CaveVoxelView view
|
||||
) {
|
||||
CavePosition position = new CavePosition(x, y, z);
|
||||
return bounds.contains(position)
|
||||
&& view.isInWorld(position)
|
||||
&& view.isAboveTerrainSurface(position);
|
||||
}
|
||||
|
||||
private CaveCandidateBounds bounds() {
|
||||
HydrologyPoint start = representative.start();
|
||||
int waterHead = representative.upstreamHeadY();
|
||||
int entryY = Math.max(waterHead, maximumY);
|
||||
int horizontalRadius = HydrologyCaveCourseFilter.horizontalRadius(
|
||||
start.x(),
|
||||
start.z(),
|
||||
minimumX,
|
||||
maximumX,
|
||||
minimumZ,
|
||||
maximumZ
|
||||
);
|
||||
int maximumDepth = Math.max(1, entryY - minimumY + 2);
|
||||
int dryHeadroom = Math.max(0, maximumY - waterHead);
|
||||
return new CaveCandidateBounds(
|
||||
start.x(),
|
||||
entryY,
|
||||
start.z(),
|
||||
waterHead,
|
||||
horizontalRadius,
|
||||
maximumDepth,
|
||||
dryHeadroom
|
||||
);
|
||||
}
|
||||
|
||||
private void addOpening(int x, int z, int minimumOpeningY, int maximumOpeningY) {
|
||||
addSpan(openings, x, z, minimumOpeningY, maximumOpeningY);
|
||||
include(x, minimumOpeningY, z);
|
||||
include(x, maximumOpeningY, z);
|
||||
}
|
||||
|
||||
private void include(int x, int y, int z) {
|
||||
minimumX = Math.min(minimumX, x);
|
||||
maximumX = Math.max(maximumX, x);
|
||||
minimumY = Math.min(minimumY, y);
|
||||
maximumY = Math.max(maximumY, y);
|
||||
minimumZ = Math.min(minimumZ, z);
|
||||
maximumZ = Math.max(maximumZ, z);
|
||||
}
|
||||
|
||||
private static void addSpan(
|
||||
Map<Long, CaveSpanSet> spans,
|
||||
int x,
|
||||
int z,
|
||||
int minimumY,
|
||||
int maximumY
|
||||
) {
|
||||
spans.computeIfAbsent(RiverFootprint.pack(x, z), (Long ignored) -> new CaveSpanSet())
|
||||
.add(minimumY, maximumY);
|
||||
}
|
||||
|
||||
private static boolean contains(CaveSpanSet spans, int y) {
|
||||
return spans != null && spans.contains(y);
|
||||
}
|
||||
|
||||
static long positionCount(Map<Long, CaveSpanSet> spans) {
|
||||
long count = 0L;
|
||||
for (CaveSpanSet spanSet : spans.values()) {
|
||||
count += spanSet.positionCount();
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
record CaveCourseGraph(
|
||||
List<DrainageNode> nodes,
|
||||
List<DrainageEdge> edges,
|
||||
List<RiverOutlet> outlets
|
||||
) {
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
final class CaveSpanSet {
|
||||
private int[] pending;
|
||||
private int size;
|
||||
private List<CaveYSpan> normalized;
|
||||
|
||||
CaveSpanSet() {
|
||||
this.pending = new int[4];
|
||||
this.normalized = null;
|
||||
}
|
||||
|
||||
void add(int minimumY, int maximumY) {
|
||||
if (minimumY > maximumY) {
|
||||
return;
|
||||
}
|
||||
int requiredLength = Math.multiplyExact(size + 1, 2);
|
||||
if (requiredLength > pending.length) {
|
||||
pending = Arrays.copyOf(pending, Math.multiplyExact(pending.length, 2));
|
||||
}
|
||||
pending[size * 2] = minimumY;
|
||||
pending[size * 2 + 1] = maximumY;
|
||||
size++;
|
||||
normalized = null;
|
||||
}
|
||||
|
||||
List<CaveYSpan> spans() {
|
||||
if (normalized != null) {
|
||||
return normalized;
|
||||
}
|
||||
sortPending();
|
||||
ArrayList<CaveYSpan> merged = new ArrayList<>(size);
|
||||
for (int index = 0; index < size; index++) {
|
||||
CaveYSpan span = new CaveYSpan(pending[index * 2], pending[index * 2 + 1]);
|
||||
if (merged.isEmpty()) {
|
||||
merged.add(span);
|
||||
continue;
|
||||
}
|
||||
CaveYSpan previous = merged.getLast();
|
||||
if ((long) span.minimumY() > (long) previous.maximumY() + 1L) {
|
||||
merged.add(span);
|
||||
continue;
|
||||
}
|
||||
merged.set(
|
||||
merged.size() - 1,
|
||||
new CaveYSpan(previous.minimumY(), Math.max(previous.maximumY(), span.maximumY()))
|
||||
);
|
||||
}
|
||||
normalized = List.copyOf(merged);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private void sortPending() {
|
||||
for (int index = 1; index < size; index++) {
|
||||
int minimumY = pending[index * 2];
|
||||
int maximumY = pending[index * 2 + 1];
|
||||
int insertionIndex = index;
|
||||
while (insertionIndex > 0) {
|
||||
int previousMinimumY = pending[(insertionIndex - 1) * 2];
|
||||
int previousMaximumY = pending[(insertionIndex - 1) * 2 + 1];
|
||||
if (previousMinimumY < minimumY
|
||||
|| previousMinimumY == minimumY && previousMaximumY <= maximumY) {
|
||||
break;
|
||||
}
|
||||
pending[insertionIndex * 2] = previousMinimumY;
|
||||
pending[insertionIndex * 2 + 1] = previousMaximumY;
|
||||
insertionIndex--;
|
||||
}
|
||||
pending[insertionIndex * 2] = minimumY;
|
||||
pending[insertionIndex * 2 + 1] = maximumY;
|
||||
}
|
||||
}
|
||||
|
||||
boolean contains(int y) {
|
||||
List<CaveYSpan> spans = spans();
|
||||
int minimumIndex = 0;
|
||||
int maximumIndex = spans.size() - 1;
|
||||
while (minimumIndex <= maximumIndex) {
|
||||
int index = (minimumIndex + maximumIndex) >>> 1;
|
||||
CaveYSpan span = spans.get(index);
|
||||
if (y < span.minimumY()) {
|
||||
maximumIndex = index - 1;
|
||||
} else if (y > span.maximumY()) {
|
||||
minimumIndex = index + 1;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
long positionCount() {
|
||||
long count = 0L;
|
||||
for (CaveYSpan span : spans()) {
|
||||
count += (long) span.maximumY() - span.minimumY() + 1L;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCandidate;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
record CaveSurfaceComposition(
|
||||
HydrologyCaveCandidate candidate,
|
||||
boolean accepted
|
||||
) {
|
||||
static CaveSurfaceComposition accepted(HydrologyCaveCandidate candidate) {
|
||||
return new CaveSurfaceComposition(Objects.requireNonNull(candidate), true);
|
||||
}
|
||||
|
||||
static CaveSurfaceComposition rejected() {
|
||||
return new CaveSurfaceComposition(null, false);
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.cave.CavePosition;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
record CaveSurfaceOpening(
|
||||
long segmentId,
|
||||
HydrologyPoint point,
|
||||
double interiorX,
|
||||
double interiorZ,
|
||||
long radiusSquared,
|
||||
int minimumY,
|
||||
boolean includeNeighborhood,
|
||||
boolean surfaceFall
|
||||
) {
|
||||
static CaveSurfaceOpening create(
|
||||
HydraulicSegment segment,
|
||||
boolean start,
|
||||
long radiusSquared,
|
||||
int minimumY,
|
||||
boolean includeNeighborhood
|
||||
) {
|
||||
List<HydrologyPoint> centerline = segment.centerline();
|
||||
int boundaryIndex = start ? 0 : centerline.size() - 1;
|
||||
int interiorIndex = start
|
||||
? Math.min(1, centerline.size() - 1)
|
||||
: Math.max(0, centerline.size() - 2);
|
||||
HydrologyPoint point = centerline.get(boundaryIndex);
|
||||
HydrologyPoint interior = centerline.get(interiorIndex);
|
||||
double deltaX = interior.x() - point.x();
|
||||
double deltaZ = interior.z() - point.z();
|
||||
double lengthSquared = deltaX * deltaX + deltaZ * deltaZ;
|
||||
double scale = 0D;
|
||||
if (includeNeighborhood) {
|
||||
scale = lengthSquared <= radiusSquared
|
||||
? 1D
|
||||
: StrictMath.sqrt(radiusSquared / lengthSquared);
|
||||
}
|
||||
return new CaveSurfaceOpening(
|
||||
segment.id(),
|
||||
point,
|
||||
point.x() + deltaX * scale,
|
||||
point.z() + deltaZ * scale,
|
||||
radiusSquared,
|
||||
minimumY,
|
||||
includeNeighborhood,
|
||||
segment.type().isSurface() && segment.fallingFluid()
|
||||
);
|
||||
}
|
||||
|
||||
boolean matches(HydrologyColumnLayer layer, CavePosition position) {
|
||||
return matchesColumn(layer, position.x(), position.z())
|
||||
&& position.y() >= minimumY;
|
||||
}
|
||||
|
||||
boolean matchesColumn(HydrologyColumnLayer layer, int x, int z) {
|
||||
if (surfaceFall) {
|
||||
return layer.fallingFluid() && layer.feature().segmentId() == segmentId;
|
||||
}
|
||||
return (includeNeighborhood || layer.feature().segmentId() == segmentId)
|
||||
&& distanceSquared(x, z) <= radiusSquared;
|
||||
}
|
||||
|
||||
private double distanceSquared(int x, int z) {
|
||||
double pathX = interiorX - point.x();
|
||||
double pathZ = interiorZ - point.z();
|
||||
double pathLengthSquared = pathX * pathX + pathZ * pathZ;
|
||||
double positionX = x - point.x();
|
||||
double positionZ = z - point.z();
|
||||
double progress = pathLengthSquared == 0D
|
||||
? 0D
|
||||
: (positionX * pathX + positionZ * pathZ) / pathLengthSquared;
|
||||
double clampedProgress = Math.max(0D, Math.min(1D, progress));
|
||||
double deltaX = positionX - pathX * clampedProgress;
|
||||
double deltaZ = positionZ - pathZ * clampedProgress;
|
||||
return deltaX * deltaX + deltaZ * deltaZ;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
record CaveYSpan(int minimumY, int maximumY) {
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
record CompiledGraph(
|
||||
List<DrainageNode> nodes,
|
||||
List<DrainageEdge> edges,
|
||||
List<RiverOutlet> outlets,
|
||||
Map<Integer, DrainageEdge> edgeByUpstream
|
||||
) {
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
record CourseEdgeKey(RiverCourseType type, long edgeId) {
|
||||
CourseEdgeKey {
|
||||
Objects.requireNonNull(type, "type");
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
record CrossTileBlockers(
|
||||
List<HydrologyCrossTileCaveAdmission.RankedClaim> caveClaims,
|
||||
List<HydrologyCrossTileSurfaceAdmission.RankedClaim> surfaceClaims
|
||||
) {
|
||||
CrossTileBlockers {
|
||||
caveClaims = List.copyOf(Objects.requireNonNull(caveClaims, "caveClaims"));
|
||||
surfaceClaims = List.copyOf(Objects.requireNonNull(surfaceClaims, "surfaceClaims"));
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
interface CrossTileDraftAdmission {
|
||||
void prepare();
|
||||
|
||||
CrossTilePublicationAdmission admit(HydrologyCaveCourseFilter.Result result);
|
||||
}
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
record CrossTilePublicationAdmission(
|
||||
HydrologyCaveCourseFilter.Result result,
|
||||
List<HydrologyDiagnosticCandidate> diagnostics,
|
||||
boolean rejectedCourses
|
||||
) {
|
||||
CrossTilePublicationAdmission {
|
||||
Objects.requireNonNull(result, "result");
|
||||
diagnostics = List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics"));
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
record CrossTileRejectedCourse(RiverCourse course, long winnerSourceId) {
|
||||
CrossTileRejectedCourse {
|
||||
Objects.requireNonNull(course, "course");
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
record CrossTileRejectionKey(long courseId, long winnerCourseId) {
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
record CrossTileResolution(
|
||||
HydrologyOwnerDraft draft,
|
||||
List<CrossTileRejectedCourse> observedRejections,
|
||||
int iterations,
|
||||
int ownerCount,
|
||||
long resolutionNanos
|
||||
) {
|
||||
CrossTileResolution {
|
||||
Objects.requireNonNull(draft, "draft");
|
||||
observedRejections = List.copyOf(Objects.requireNonNull(observedRejections, "observedRejections"));
|
||||
}
|
||||
}
|
||||
-73
@@ -1,73 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
final class CrossTileResolutionContext {
|
||||
final Map<HydrologyTileKey, CrossTileResolvedOwner> resolved;
|
||||
private final Set<HydrologyTileKey> resolving;
|
||||
final HydrologyTileKey root;
|
||||
private final long maximumTileOffset;
|
||||
private final int maximumOwners;
|
||||
int iterations;
|
||||
|
||||
CrossTileResolutionContext(
|
||||
HydrologyTileKey root,
|
||||
long maximumTileOffset,
|
||||
int maximumOwners
|
||||
) {
|
||||
this.resolved = new HashMap<>();
|
||||
this.resolving = new HashSet<>();
|
||||
this.root = Objects.requireNonNull(root, "root");
|
||||
this.maximumTileOffset = maximumTileOffset;
|
||||
this.maximumOwners = maximumOwners;
|
||||
}
|
||||
|
||||
CrossTileResolvedOwner resolved(HydrologyTileKey key) {
|
||||
return resolved.get(key);
|
||||
}
|
||||
|
||||
void remember(HydrologyTileKey key, CrossTileResolvedOwner owner) {
|
||||
long offsetX = Math.abs((long) key.tileX() - root.tileX());
|
||||
long offsetZ = Math.abs((long) key.tileZ() - root.tileZ());
|
||||
if (offsetX > maximumTileOffset || offsetZ > maximumTileOffset) {
|
||||
throw new IllegalStateException("Cross-tile owner dependency exceeded its color-ranked geometry bound.");
|
||||
}
|
||||
CrossTileResolvedOwner existing = resolved.putIfAbsent(key, owner);
|
||||
if (existing != null
|
||||
&& !existing.withoutFootprintCompiler().equals(owner.withoutFootprintCompiler())) {
|
||||
throw new IllegalStateException("Cross-tile owner resolution produced inconsistent cached results for "
|
||||
+ key.tileX() + "," + key.tileZ() + " while planning " + root.tileX() + "," + root.tileZ() + ".");
|
||||
}
|
||||
if (resolved.size() > maximumOwners) {
|
||||
throw new IllegalStateException("Cross-tile owner dependencies exceeded their color-ranked count bound.");
|
||||
}
|
||||
}
|
||||
|
||||
boolean begin(HydrologyTileKey key) {
|
||||
return resolving.add(key);
|
||||
}
|
||||
|
||||
void end(HydrologyTileKey key) {
|
||||
resolving.remove(key);
|
||||
}
|
||||
|
||||
void recordIteration() {
|
||||
iterations++;
|
||||
}
|
||||
|
||||
int iterations() {
|
||||
return iterations;
|
||||
}
|
||||
|
||||
int ownerCount() {
|
||||
return resolved.size();
|
||||
}
|
||||
|
||||
HydrologyTileKey root() {
|
||||
return root;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
record CrossTileResolvedOwner(
|
||||
HydrologyOwnerDraft draft,
|
||||
List<CrossTileRejectedCourse> observedRejections
|
||||
) {
|
||||
CrossTileResolvedOwner {
|
||||
Objects.requireNonNull(draft, "draft");
|
||||
observedRejections = List.copyOf(Objects.requireNonNull(observedRejections, "observedRejections"));
|
||||
}
|
||||
|
||||
CrossTileResolvedOwner withoutFootprintCompiler() {
|
||||
HydrologyOwnerDraft uncachedDraft = draft.withoutFootprintCompiler();
|
||||
return uncachedDraft == draft
|
||||
? this
|
||||
: new CrossTileResolvedOwner(uncachedDraft, observedRejections);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
enum DraftPhase {
|
||||
CONTEXT,
|
||||
SELECT,
|
||||
SETTLE,
|
||||
PUBLISH
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
|
||||
/**
|
||||
* Wall-clock breakdown of one owner draft for the debug log: sampling and routing context, source
|
||||
* selection, settling, publication passes, and the time spent waiting on lower-rank neighbour drafts.
|
||||
*/
|
||||
final class DraftProfile {
|
||||
private final long[] phaseNanos = new long[DraftPhase.values().length];
|
||||
private long dependencyNanos;
|
||||
private int dependencies;
|
||||
private int admissions;
|
||||
int earlyOwners;
|
||||
int routeCalls;
|
||||
int routeSolves;
|
||||
long routeSolveNanos;
|
||||
int rasterCalls;
|
||||
long rasterNanos;
|
||||
int filterCalls;
|
||||
long filterNanos;
|
||||
|
||||
long record(DraftPhase phase, long started) {
|
||||
long now = System.nanoTime();
|
||||
phaseNanos[phase.ordinal()] += now - started;
|
||||
return now;
|
||||
}
|
||||
|
||||
void recordDependencies(int count, long started) {
|
||||
dependencyNanos += System.nanoTime() - started;
|
||||
dependencies += count;
|
||||
admissions++;
|
||||
}
|
||||
|
||||
void log(HydrologyTileKey key, int rank, long totalNanos) {
|
||||
IrisLogging.debug(
|
||||
"Hydrology owner %d,%d rank=%d drafted in %dms: context=%dms select=%dms settle=%dms publish=%dms deps=%d wait=%dms admissions=%d earlyOwners=%d routes=%d reuses=%d routeSolve=%dms rasters=%d raster=%dms filters=%d filter=%dms on %s",
|
||||
key.tileX(),
|
||||
key.tileZ(),
|
||||
rank,
|
||||
totalNanos / 1_000_000L,
|
||||
phaseNanos[DraftPhase.CONTEXT.ordinal()] / 1_000_000L,
|
||||
phaseNanos[DraftPhase.SELECT.ordinal()] / 1_000_000L,
|
||||
phaseNanos[DraftPhase.SETTLE.ordinal()] / 1_000_000L,
|
||||
phaseNanos[DraftPhase.PUBLISH.ordinal()] / 1_000_000L,
|
||||
dependencies,
|
||||
dependencyNanos / 1_000_000L,
|
||||
admissions,
|
||||
earlyOwners,
|
||||
routeCalls,
|
||||
routeCalls - routeSolves,
|
||||
routeSolveNanos / 1_000_000L,
|
||||
rasterCalls,
|
||||
rasterNanos / 1_000_000L,
|
||||
filterCalls,
|
||||
filterNanos / 1_000_000L,
|
||||
Thread.currentThread().getName()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public record DrainageEdge(
|
||||
long id,
|
||||
long upstreamNodeId,
|
||||
long downstreamNodeId,
|
||||
long outletId,
|
||||
double cost,
|
||||
int contributingSurfaceSources,
|
||||
int contributingUndergroundSources,
|
||||
List<HydrologyPoint> centerline
|
||||
) {
|
||||
public DrainageEdge {
|
||||
if (upstreamNodeId == downstreamNodeId) {
|
||||
throw new IllegalArgumentException("A drainage edge cannot point to the same node.");
|
||||
}
|
||||
if (!Double.isFinite(cost) || cost <= 0D) {
|
||||
throw new IllegalArgumentException("Drainage edge cost must be finite and positive.");
|
||||
}
|
||||
if (contributingSurfaceSources < 0 || contributingUndergroundSources < 0) {
|
||||
throw new IllegalArgumentException("Contributing source counts cannot be negative.");
|
||||
}
|
||||
Objects.requireNonNull(centerline, "centerline");
|
||||
centerline = List.copyOf(centerline);
|
||||
if (centerline.size() < 2) {
|
||||
throw new IllegalArgumentException("A drainage edge requires at least two centerline points.");
|
||||
}
|
||||
}
|
||||
|
||||
public int totalContributingSources() {
|
||||
return contributingSurfaceSources + contributingUndergroundSources;
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record DrainageNode(
|
||||
long id,
|
||||
int x,
|
||||
int z,
|
||||
HydrologyTerrainSample terrain,
|
||||
double potential,
|
||||
long outletId
|
||||
) {
|
||||
public DrainageNode {
|
||||
Objects.requireNonNull(terrain, "terrain");
|
||||
if (!Double.isFinite(potential) || potential < 0D) {
|
||||
throw new IllegalArgumentException("Drainage potential must be finite and non-negative.");
|
||||
}
|
||||
if (terrain.ocean()) {
|
||||
throw new IllegalArgumentException("Drainage nodes must remain landward of the first ocean column.");
|
||||
}
|
||||
}
|
||||
|
||||
public HydrologyPoint naturalPoint() {
|
||||
return new HydrologyPoint(x, terrain.naturalHeight(), z);
|
||||
}
|
||||
}
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
record FootprintCenterlineProjection(
|
||||
int pointIndex,
|
||||
double x,
|
||||
double z,
|
||||
double distance,
|
||||
int flowX,
|
||||
int flowZ
|
||||
) {
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
record FootprintCourseRasterKey(
|
||||
long courseId,
|
||||
RiverCourseType courseType,
|
||||
String profileKey,
|
||||
List<HydraulicSegment> segments
|
||||
) {
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
record FootprintFeatureKey(
|
||||
long courseId,
|
||||
long segmentId,
|
||||
HydrologyFeatureType type,
|
||||
int role,
|
||||
int flowX,
|
||||
int flowZ
|
||||
) {
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
record FootprintLayerShape(
|
||||
int channelRadius,
|
||||
double shoreWidth,
|
||||
double gradingWidth,
|
||||
int bed,
|
||||
int fluidHead,
|
||||
int ceiling,
|
||||
boolean ellipsoid,
|
||||
boolean archedChannel,
|
||||
boolean roundedSurfaceBed,
|
||||
boolean organicBoundary,
|
||||
boolean fallingThroat,
|
||||
boolean inlet
|
||||
) {
|
||||
double totalRadius() {
|
||||
return channelRadius + shoreWidth + gradingWidth;
|
||||
}
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
|
||||
final class FootprintMutableColumn {
|
||||
private final int x;
|
||||
private final int z;
|
||||
private final int naturalHeight;
|
||||
private final int seaLevel;
|
||||
private final boolean ocean;
|
||||
private final String parentBiomeKey;
|
||||
private HydrologyColumnLayer singleLayer;
|
||||
private LinkedHashMap<Long, HydrologyColumnLayer> layers;
|
||||
|
||||
FootprintMutableColumn(int x, int z, HydrologyTerrainSample terrain, int seaLevel) {
|
||||
this.x = x;
|
||||
this.z = z;
|
||||
this.naturalHeight = terrain.naturalHeight();
|
||||
this.seaLevel = seaLevel;
|
||||
this.ocean = terrain.ocean();
|
||||
this.parentBiomeKey = terrain.parentBiomeKey();
|
||||
}
|
||||
|
||||
FootprintMutableColumn(HydrologyColumnSample sample) {
|
||||
this.x = sample.x();
|
||||
this.z = sample.z();
|
||||
this.naturalHeight = sample.naturalHeight();
|
||||
this.seaLevel = sample.seaLevel();
|
||||
this.ocean = sample.ocean();
|
||||
this.parentBiomeKey = sample.parentBiomeKey();
|
||||
for (HydrologyColumnLayer layer : sample.layers()) {
|
||||
add(layer);
|
||||
}
|
||||
}
|
||||
|
||||
void add(HydrologyColumnLayer layer) {
|
||||
if (layers == null) {
|
||||
if (singleLayer == null) {
|
||||
singleLayer = layer;
|
||||
return;
|
||||
}
|
||||
if (singleLayer.feature().id() == layer.feature().id()) {
|
||||
singleLayer = merge(singleLayer, layer);
|
||||
return;
|
||||
}
|
||||
layers = new LinkedHashMap<>();
|
||||
layers.put(singleLayer.feature().id(), singleLayer);
|
||||
singleLayer = null;
|
||||
}
|
||||
HydrologyColumnLayer existing = layers.get(layer.feature().id());
|
||||
layers.put(layer.feature().id(), existing == null ? layer : merge(existing, layer));
|
||||
}
|
||||
|
||||
HydrologyColumnSample build() {
|
||||
List<HydrologyColumnLayer> builtLayers;
|
||||
if (layers != null) {
|
||||
builtLayers = resolveSurfaceHeadConflicts(new ArrayList<>(layers.values()));
|
||||
} else if (singleLayer != null) {
|
||||
builtLayers = List.of(singleLayer);
|
||||
} else {
|
||||
builtLayers = List.of();
|
||||
}
|
||||
return new HydrologyColumnSample(
|
||||
x,
|
||||
z,
|
||||
naturalHeight,
|
||||
seaLevel,
|
||||
ocean,
|
||||
parentBiomeKey,
|
||||
builtLayers
|
||||
);
|
||||
}
|
||||
|
||||
private List<HydrologyColumnLayer> resolveSurfaceHeadConflicts(List<HydrologyColumnLayer> candidates) {
|
||||
HashMap<Long, HydrologyColumnLayer> selectedByCourse = new HashMap<>();
|
||||
for (HydrologyColumnLayer candidate : candidates) {
|
||||
if (!conflictingSurfaceCandidate(candidate)) {
|
||||
continue;
|
||||
}
|
||||
long courseId = candidate.feature().courseId();
|
||||
HydrologyColumnLayer selected = selectedByCourse.get(courseId);
|
||||
if (selected == null || prefersSurfaceLayer(candidate, selected)) {
|
||||
selectedByCourse.put(courseId, candidate);
|
||||
}
|
||||
}
|
||||
if (selectedByCourse.isEmpty()) {
|
||||
return candidates;
|
||||
}
|
||||
ArrayList<HydrologyColumnLayer> resolved = new ArrayList<>(candidates.size());
|
||||
for (HydrologyColumnLayer candidate : candidates) {
|
||||
HydrologyColumnLayer selected = selectedByCourse.get(candidate.feature().courseId());
|
||||
if (!conflictingSurfaceCandidate(candidate)
|
||||
|| selected == null
|
||||
|| candidate.fluidHeadY() == selected.fluidHeadY()) {
|
||||
resolved.add(candidate);
|
||||
}
|
||||
}
|
||||
return List.copyOf(resolved);
|
||||
}
|
||||
|
||||
private boolean conflictingSurfaceCandidate(HydrologyColumnLayer layer) {
|
||||
return layer.feature().type().isSurface()
|
||||
&& layer.channel()
|
||||
&& layer.connectedFluid()
|
||||
&& layer.fluidOwned()
|
||||
&& !layer.fallingFluid();
|
||||
}
|
||||
|
||||
private boolean prefersSurfaceLayer(HydrologyColumnLayer candidate, HydrologyColumnLayer selected) {
|
||||
if (candidate.receivingPool() != selected.receivingPool()) {
|
||||
return candidate.receivingPool();
|
||||
}
|
||||
if (candidate.fluidHeadY() != selected.fluidHeadY()) {
|
||||
return candidate.fluidHeadY() > selected.fluidHeadY();
|
||||
}
|
||||
return candidate.feature().id() < selected.feature().id();
|
||||
}
|
||||
|
||||
void merge(HydrologyColumnSample sample) {
|
||||
if (sample.x() != x
|
||||
|| sample.z() != z
|
||||
|| sample.naturalHeight() != naturalHeight
|
||||
|| sample.seaLevel() != seaLevel
|
||||
|| sample.ocean() != ocean
|
||||
|| !sample.parentBiomeKey().equals(parentBiomeKey)) {
|
||||
throw new IllegalStateException("Hydrology course footprints disagree on terrain metadata at "
|
||||
+ x + "," + z + ".");
|
||||
}
|
||||
for (HydrologyColumnLayer layer : sample.layers()) {
|
||||
add(layer);
|
||||
}
|
||||
}
|
||||
|
||||
private HydrologyColumnLayer merge(HydrologyColumnLayer first, HydrologyColumnLayer second) {
|
||||
if (first.equals(second)) {
|
||||
return first;
|
||||
}
|
||||
HydrologyFeatureRef feature = first.feature().y() <= second.feature().y()
|
||||
? first.feature()
|
||||
: second.feature();
|
||||
boolean drySurfaceBlend = first.feature().type().isSurface()
|
||||
&& second.feature().type().isSurface()
|
||||
&& !first.channel()
|
||||
&& !second.channel();
|
||||
int bedY = drySurfaceBlend
|
||||
? Math.max(first.bedY(), second.bedY())
|
||||
: Math.min(first.bedY(), second.bedY());
|
||||
return new HydrologyColumnLayer(
|
||||
feature,
|
||||
bedY,
|
||||
Math.max(first.fluidHeadY(), second.fluidHeadY()),
|
||||
Math.max(first.ceilingY(), second.ceilingY()),
|
||||
first.channel() || second.channel(),
|
||||
first.shore() || second.shore(),
|
||||
first.grading() || second.grading(),
|
||||
first.connectedFluid() || second.connectedFluid(),
|
||||
first.fallingFluid() || second.fallingFluid(),
|
||||
first.receivingPool() || second.receivingPool(),
|
||||
(first.terrainOwned() || second.terrainOwned()) && (!drySurfaceBlend || bedY != naturalHeight),
|
||||
first.fluidOwned() || second.fluidOwned(),
|
||||
first.oceanApron() || second.oceanApron(),
|
||||
first.profileKey(),
|
||||
first.surfaceBiomeKey(),
|
||||
first.mouthBiomeKey(),
|
||||
first.shoreBiomeKey(),
|
||||
first.bankBiomeKey(),
|
||||
first.floodedCaveBiomeKey()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
record FootprintRasterStencil(
|
||||
int[] deltaXs,
|
||||
int[] deltaZs,
|
||||
double[] distances
|
||||
) {
|
||||
int size() {
|
||||
return deltaXs.length;
|
||||
}
|
||||
}
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
final class FootprintRasterStencilBuilder {
|
||||
private int[] deltaXs;
|
||||
private int[] deltaZs;
|
||||
private double[] distances;
|
||||
private int size;
|
||||
|
||||
FootprintRasterStencilBuilder() {
|
||||
this.deltaXs = new int[64];
|
||||
this.deltaZs = new int[64];
|
||||
this.distances = new double[64];
|
||||
}
|
||||
|
||||
void add(int deltaX, int deltaZ, double distance) {
|
||||
if (size == deltaXs.length) {
|
||||
int expandedSize = Math.multiplyExact(size, 2);
|
||||
deltaXs = Arrays.copyOf(deltaXs, expandedSize);
|
||||
deltaZs = Arrays.copyOf(deltaZs, expandedSize);
|
||||
distances = Arrays.copyOf(distances, expandedSize);
|
||||
}
|
||||
deltaXs[size] = deltaX;
|
||||
deltaZs[size] = deltaZ;
|
||||
distances[size] = distance;
|
||||
size++;
|
||||
}
|
||||
|
||||
FootprintRasterStencil build() {
|
||||
return new FootprintRasterStencil(
|
||||
Arrays.copyOf(deltaXs, size),
|
||||
Arrays.copyOf(deltaZs, size),
|
||||
Arrays.copyOf(distances, size)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
record FootprintRasterStencilKey(
|
||||
int channelRadius,
|
||||
double shoreWidth,
|
||||
double gradingWidth
|
||||
) {
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.surface.SurfaceFootprint;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
record FootprintValidationCourseRaster(
|
||||
long courseId,
|
||||
List<HydrologyColumnSample> columns,
|
||||
SurfaceFootprint surface
|
||||
) {
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
public record HydraulicChannelProfile(double[] widths, double[] depths) {
|
||||
public HydraulicChannelProfile {
|
||||
Objects.requireNonNull(widths, "widths");
|
||||
Objects.requireNonNull(depths, "depths");
|
||||
if (widths.length == 0 || widths.length != depths.length) {
|
||||
throw new IllegalArgumentException("Channel dimensions require equally sized, nonempty station arrays.");
|
||||
}
|
||||
widths = widths.clone();
|
||||
depths = depths.clone();
|
||||
for (int station = 0; station < widths.length; station++) {
|
||||
if (!Double.isFinite(widths[station]) || widths[station] <= 0D
|
||||
|| !Double.isFinite(depths[station]) || depths[station] <= 0D) {
|
||||
throw new IllegalArgumentException("Channel dimensions must be finite and positive.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static HydraulicChannelProfile uniform(double width, double depth) {
|
||||
return new HydraulicChannelProfile(new double[]{width}, new double[]{depth});
|
||||
}
|
||||
|
||||
public static HydraulicChannelProfile range(double[] widths, double[] depths, int from, int to) {
|
||||
Objects.checkFromToIndex(from, to, widths.length);
|
||||
Objects.checkFromToIndex(from, to, depths.length);
|
||||
return new HydraulicChannelProfile(Arrays.copyOfRange(widths, from, to), Arrays.copyOfRange(depths, from, to));
|
||||
}
|
||||
|
||||
@Override
|
||||
public double[] widths() {
|
||||
return widths.clone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public double[] depths() {
|
||||
return depths.clone();
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return widths.length;
|
||||
}
|
||||
|
||||
public double widthAt(int station) {
|
||||
return widths[widths.length == 1 ? 0 : station];
|
||||
}
|
||||
|
||||
public double depthAt(int station) {
|
||||
return depths[depths.length == 1 ? 0 : station];
|
||||
}
|
||||
|
||||
public double maximumWidth() {
|
||||
double maximum = 0D;
|
||||
for (double width : widths) {
|
||||
maximum = Math.max(maximum, width);
|
||||
}
|
||||
return maximum;
|
||||
}
|
||||
|
||||
public double maximumDepth() {
|
||||
double maximum = 0D;
|
||||
for (double depth : depths) {
|
||||
maximum = Math.max(maximum, depth);
|
||||
}
|
||||
return maximum;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
return other instanceof HydraulicChannelProfile profile
|
||||
&& Arrays.equals(widths, profile.widths)
|
||||
&& Arrays.equals(depths, profile.depths);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return 31 * Arrays.hashCode(widths) + Arrays.hashCode(depths);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "HydraulicChannelProfile[widths=" + Arrays.toString(widths) + ", depths=" + Arrays.toString(depths) + "]";
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public record HydraulicSegment(
|
||||
long id,
|
||||
long courseId,
|
||||
HydrologyFeatureType type,
|
||||
int upstreamHeadY,
|
||||
int downstreamHeadY,
|
||||
int width,
|
||||
int depth,
|
||||
boolean fallingFluid,
|
||||
boolean receivingPool,
|
||||
List<HydrologyPoint> centerline,
|
||||
HydraulicChannelProfile channelProfile
|
||||
) {
|
||||
public HydraulicSegment {
|
||||
Objects.requireNonNull(type, "type");
|
||||
Objects.requireNonNull(centerline, "centerline");
|
||||
Objects.requireNonNull(channelProfile, "channelProfile");
|
||||
centerline = List.copyOf(centerline);
|
||||
if (centerline.isEmpty()) {
|
||||
throw new IllegalArgumentException("A hydraulic segment requires a centerline.");
|
||||
}
|
||||
if (upstreamHeadY < downstreamHeadY) {
|
||||
throw new IllegalArgumentException("Hydraulic heads cannot rise downstream.");
|
||||
}
|
||||
if (width <= 0 || depth <= 0) {
|
||||
throw new IllegalArgumentException("Hydraulic segment width and depth must be positive.");
|
||||
}
|
||||
if (channelProfile.size() != 1 && channelProfile.size() != centerline.size()) {
|
||||
throw new IllegalArgumentException("Channel dimensions must cover every centerline station.");
|
||||
}
|
||||
if (channelProfile.maximumWidth() > width || channelProfile.maximumDepth() > depth) {
|
||||
throw new IllegalArgumentException("Hydraulic segment bounds must contain its channel dimensions.");
|
||||
}
|
||||
int drop = upstreamHeadY - downstreamHeadY;
|
||||
if (fallingFluid && drop == 0) {
|
||||
throw new IllegalArgumentException("Falling fluid requires a positive head transition.");
|
||||
}
|
||||
if (drop > 0 && !type.isDrop()) {
|
||||
throw new IllegalArgumentException("A positive head transition requires a drop segment type.");
|
||||
}
|
||||
if (drop == 0 && type.isDrop()) {
|
||||
throw new IllegalArgumentException("A drop segment type requires a positive head transition.");
|
||||
}
|
||||
if (drop > 0 && !fallingFluid) {
|
||||
if (centerline.getFirst().y() != upstreamHeadY || centerline.getLast().y() != downstreamHeadY) {
|
||||
throw new IllegalArgumentException("A graded head transition must span its declared heads.");
|
||||
}
|
||||
int previousHead = upstreamHeadY;
|
||||
for (HydrologyPoint point : centerline) {
|
||||
if (point.y() > previousHead || point.y() < downstreamHeadY) {
|
||||
throw new IllegalArgumentException("A graded head transition must descend monotonically.");
|
||||
}
|
||||
previousHead = point.y();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int drop() {
|
||||
return upstreamHeadY - downstreamHeadY;
|
||||
}
|
||||
|
||||
public HydrologyPoint start() {
|
||||
return centerline.getFirst();
|
||||
}
|
||||
|
||||
public HydrologyPoint end() {
|
||||
return centerline.getLast();
|
||||
}
|
||||
}
|
||||
@@ -1,325 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.stream.ProvisionalSampling;
|
||||
import art.arcane.volmlib.util.cache.CacheKey;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
|
||||
final class HydrologyBasisCache {
|
||||
private static final int MAXIMUM_ENTRIES = 32_768;
|
||||
private static final int MAXIMUM_STRIPES = 64;
|
||||
private final Stripe[] full;
|
||||
private final Stripe[] withoutSlope;
|
||||
|
||||
HydrologyBasisCache() {
|
||||
this(MAXIMUM_ENTRIES);
|
||||
}
|
||||
|
||||
HydrologyBasisCache(int maximumEntries) {
|
||||
if (maximumEntries < 1 || Integer.bitCount(maximumEntries) != 1) {
|
||||
throw new IllegalArgumentException("Terrain basis cache size must be a positive power of two.");
|
||||
}
|
||||
int stripeCount = Math.min(MAXIMUM_STRIPES, maximumEntries);
|
||||
full = new Stripe[stripeCount];
|
||||
withoutSlope = new Stripe[stripeCount];
|
||||
for (int index = 0; index < stripeCount; index++) {
|
||||
full[index] = new Stripe(maximumEntries / stripeCount);
|
||||
withoutSlope[index] = new Stripe(maximumEntries / stripeCount);
|
||||
}
|
||||
}
|
||||
|
||||
HydrologyTerrainSample sample(int x, int z, HydrologyNaturalTerrainSampler sampler) {
|
||||
return sample(x, z, sampler, false);
|
||||
}
|
||||
|
||||
HydrologyTerrainSample sampleWithoutSlope(int x, int z, HydrologyNaturalTerrainSampler sampler) {
|
||||
return sample(x, z, sampler, true);
|
||||
}
|
||||
|
||||
void seed(Long2ObjectMap<HydrologyTerrainSample> samples) {
|
||||
seed(samples, full);
|
||||
}
|
||||
|
||||
HydrologyTerrainSample[] sampleWithoutSlopeBatch(long[] coordinates, int count,
|
||||
HydrologyNaturalTerrainSampler sampler) {
|
||||
Objects.checkFromIndexSize(0, count, coordinates.length);
|
||||
if (count > HydrologyTerrainSampler.MAXIMUM_BATCH_SIZE) {
|
||||
throw new IllegalArgumentException("Terrain basis batch exceeds its sample limit");
|
||||
}
|
||||
HydrologyTerrainSample[] samples = new HydrologyTerrainSample[count];
|
||||
Pending[] pending = new Pending[count];
|
||||
long[] ownedCoordinates = new long[count];
|
||||
Pending[] owned = new Pending[count];
|
||||
int ownedCount = count;
|
||||
Completion completion = new Completion();
|
||||
int[] next = new int[count];
|
||||
int[] heads = groupByStripe(coordinates, count, next);
|
||||
try {
|
||||
for (int stripeIndex = 0; stripeIndex < withoutSlope.length; stripeIndex++) {
|
||||
if (heads[stripeIndex] == 0) {
|
||||
continue;
|
||||
}
|
||||
Stripe stripe = withoutSlope[stripeIndex];
|
||||
synchronized (stripe) {
|
||||
for (int position = heads[stripeIndex]; position != 0; position = next[position - 1]) {
|
||||
int index = position - 1;
|
||||
long packed = coordinates[index];
|
||||
HydrologyTerrainSample cached = stripe.samples.getAndMoveToFirst(packed);
|
||||
if (cached != null || stripe.samples.containsKey(packed)) {
|
||||
samples[index] = cached;
|
||||
continue;
|
||||
}
|
||||
Pending loading = stripe.pending.get(packed);
|
||||
if (loading == null) {
|
||||
loading = new Pending(completion);
|
||||
stripe.pending.put(packed, loading);
|
||||
ownedCoordinates[index] = packed;
|
||||
owned[index] = loading;
|
||||
}
|
||||
pending[index] = loading;
|
||||
}
|
||||
}
|
||||
}
|
||||
int claimed = 0;
|
||||
for (int index = 0; index < count; index++) {
|
||||
if (owned[index] != null) {
|
||||
ownedCoordinates[claimed] = ownedCoordinates[index];
|
||||
owned[claimed++] = owned[index];
|
||||
}
|
||||
}
|
||||
ownedCount = claimed;
|
||||
if (ownedCount > 0) {
|
||||
loadBatch(ownedCoordinates, owned, ownedCount, sampler);
|
||||
}
|
||||
} catch (RuntimeException | Error failure) {
|
||||
for (int index = 0; index < ownedCount; index++) {
|
||||
if (owned[index] != null) {
|
||||
fail(withoutSlope, ownedCoordinates[index], owned[index], failure);
|
||||
}
|
||||
}
|
||||
completion.failure = failure;
|
||||
throw failure;
|
||||
} finally {
|
||||
completion.complete.countDown();
|
||||
}
|
||||
for (int index = 0; index < count; index++) {
|
||||
if (pending[index] != null) {
|
||||
samples[index] = pending[index].await();
|
||||
}
|
||||
}
|
||||
return samples;
|
||||
}
|
||||
|
||||
void seedWithoutSlope(Long2ObjectMap<HydrologyTerrainSample> samples) {
|
||||
seed(samples, withoutSlope);
|
||||
}
|
||||
|
||||
private HydrologyTerrainSample sample(int x, int z, HydrologyNaturalTerrainSampler sampler, boolean omitSlope) {
|
||||
long packed = RiverFootprint.pack(x, z);
|
||||
Stripe[] stripes = omitSlope ? withoutSlope : full;
|
||||
Stripe stripe = stripe(stripes, packed);
|
||||
Pending loading;
|
||||
boolean owner = false;
|
||||
synchronized (stripe) {
|
||||
HydrologyTerrainSample cached = stripe.samples.getAndMoveToFirst(packed);
|
||||
if (cached != null || stripe.samples.containsKey(packed)) {
|
||||
return cached;
|
||||
}
|
||||
loading = stripe.pending.get(packed);
|
||||
if (loading == null) {
|
||||
loading = new Pending(new Completion());
|
||||
stripe.pending.put(packed, loading);
|
||||
owner = true;
|
||||
}
|
||||
}
|
||||
if (owner) {
|
||||
try {
|
||||
HydrologyTerrainSample sampled = ProvisionalSampling.memoizable(() -> omitSlope
|
||||
? sampler.sampleBasisWithoutSlope(x, z) : sampler.sampleBasis(x, z));
|
||||
complete(stripes, packed, loading, sampled, false);
|
||||
} catch (ProvisionalSampling.Unmemoizable provisional) {
|
||||
complete(stripes, packed, loading, provisional.value(), true);
|
||||
} catch (RuntimeException | Error failure) {
|
||||
fail(stripes, packed, loading, failure);
|
||||
throw failure;
|
||||
} finally {
|
||||
loading.completion.complete.countDown();
|
||||
}
|
||||
}
|
||||
return loading.await();
|
||||
}
|
||||
|
||||
private void loadBatch(long[] coordinates, Pending[] pending, int count, HydrologyNaturalTerrainSampler sampler) {
|
||||
HydrologyTerrainSample[] samples;
|
||||
boolean provisional = false;
|
||||
try {
|
||||
samples = ProvisionalSampling.memoizable(() -> sampler.sampleBasisWithoutSlopeBatch(coordinates, count));
|
||||
} catch (ProvisionalSampling.Unmemoizable value) {
|
||||
samples = value.value();
|
||||
provisional = true;
|
||||
}
|
||||
if (samples.length < count) {
|
||||
throw new IllegalStateException("Terrain basis batch returned too few samples");
|
||||
}
|
||||
int[] next = new int[count];
|
||||
int[] heads = groupByStripe(coordinates, count, next);
|
||||
for (int stripeIndex = 0; stripeIndex < withoutSlope.length; stripeIndex++) {
|
||||
if (heads[stripeIndex] == 0) {
|
||||
continue;
|
||||
}
|
||||
Stripe stripe = withoutSlope[stripeIndex];
|
||||
synchronized (stripe) {
|
||||
for (int position = heads[stripeIndex]; position != 0; position = next[position - 1]) {
|
||||
int index = position - 1;
|
||||
complete(stripe, coordinates[index], pending[index], samples[index], provisional);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int[] groupByStripe(long[] coordinates, int count, int[] next) {
|
||||
int[] heads = new int[withoutSlope.length];
|
||||
for (int index = count - 1; index >= 0; index--) {
|
||||
int stripeIndex = (int) CacheKey.mix(coordinates[index]) & (withoutSlope.length - 1);
|
||||
next[index] = heads[stripeIndex];
|
||||
heads[stripeIndex] = index + 1;
|
||||
}
|
||||
return heads;
|
||||
}
|
||||
|
||||
private void complete(Stripe[] stripes, long packed, Pending pending, HydrologyTerrainSample sample,
|
||||
boolean provisional) {
|
||||
Stripe stripe = stripe(stripes, packed);
|
||||
synchronized (stripe) {
|
||||
complete(stripe, packed, pending, sample, provisional);
|
||||
}
|
||||
}
|
||||
|
||||
private void complete(Stripe stripe, long packed, Pending pending, HydrologyTerrainSample sample,
|
||||
boolean provisional) {
|
||||
if (stripe.samples.containsKey(packed)) {
|
||||
sample = stripe.samples.getAndMoveToFirst(packed);
|
||||
provisional = false;
|
||||
} else if (!provisional) {
|
||||
stripe.put(packed, sample);
|
||||
}
|
||||
stripe.pending.remove(packed);
|
||||
pending.sample = sample;
|
||||
pending.provisional = provisional;
|
||||
}
|
||||
|
||||
private void fail(Stripe[] stripes, long packed, Pending pending, Throwable failure) {
|
||||
Stripe stripe = stripe(stripes, packed);
|
||||
synchronized (stripe) {
|
||||
if (stripe.pending.remove(packed, pending)) {
|
||||
pending.completion.failure = failure;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void seed(Long2ObjectMap<HydrologyTerrainSample> samples, Stripe[] stripes) {
|
||||
for (Long2ObjectMap.Entry<HydrologyTerrainSample> entry : samples.long2ObjectEntrySet()) {
|
||||
Stripe stripe = stripe(stripes, entry.getLongKey());
|
||||
synchronized (stripe) {
|
||||
stripe.put(entry.getLongKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Stripe stripe(Stripe[] stripes, long packed) {
|
||||
return stripes[(int) CacheKey.mix(packed) & (stripes.length - 1)];
|
||||
}
|
||||
|
||||
private static final class Stripe {
|
||||
private final int maximumEntries;
|
||||
private final Long2ObjectLinkedOpenHashMap<HydrologyTerrainSample> samples;
|
||||
private final Long2ObjectOpenHashMap<Pending> pending = new Long2ObjectOpenHashMap<>();
|
||||
|
||||
private Stripe(int maximumEntries) {
|
||||
this.maximumEntries = maximumEntries;
|
||||
samples = new Long2ObjectLinkedOpenHashMap<>(Math.min(16, maximumEntries));
|
||||
}
|
||||
|
||||
private void put(long packed, HydrologyTerrainSample value) {
|
||||
if (samples.size() >= maximumEntries && !samples.containsKey(packed)) {
|
||||
samples.removeLast();
|
||||
}
|
||||
samples.putAndMoveToFirst(packed, value);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Pending {
|
||||
private final Completion completion;
|
||||
private HydrologyTerrainSample sample;
|
||||
private boolean provisional;
|
||||
|
||||
private Pending(Completion completion) {
|
||||
this.completion = completion;
|
||||
}
|
||||
|
||||
private HydrologyTerrainSample await() {
|
||||
completion.await();
|
||||
if (provisional) {
|
||||
ProvisionalSampling.mark();
|
||||
}
|
||||
return sample;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Completion implements ForkJoinPool.ManagedBlocker {
|
||||
private final Thread owner = Thread.currentThread();
|
||||
private final CountDownLatch complete = new CountDownLatch(1);
|
||||
private Throwable failure;
|
||||
|
||||
private void await() {
|
||||
if (!isReleasable() && owner == Thread.currentThread()) {
|
||||
throw new IllegalStateException("Recursive terrain basis sampling");
|
||||
}
|
||||
boolean interrupted = false;
|
||||
boolean managed = true;
|
||||
try {
|
||||
while (!isReleasable()) {
|
||||
try {
|
||||
if (managed) {
|
||||
ForkJoinPool.managedBlock(this);
|
||||
} else {
|
||||
complete.await();
|
||||
}
|
||||
} catch (InterruptedException interruption) {
|
||||
interrupted = true;
|
||||
managed = false;
|
||||
} catch (RejectedExecutionException unavailable) {
|
||||
managed = false;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (interrupted) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
if (failure instanceof Error error) {
|
||||
throw error;
|
||||
}
|
||||
if (failure instanceof RuntimeException runtime) {
|
||||
throw runtime;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean block() throws InterruptedException {
|
||||
complete.await();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReleasable() {
|
||||
return complete.getCount() == 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
final class HydrologyCacheBudget {
|
||||
private static final long MEBIBYTE = 1024L * 1024L;
|
||||
private static final HydrologyCacheBudget REGIONAL_RUNTIME = forHeap(Runtime.getRuntime().maxMemory(), true);
|
||||
private static final HydrologyCacheBudget LOCAL_RUNTIME = forHeap(Runtime.getRuntime().maxMemory(), false);
|
||||
private final long plannerBytes;
|
||||
private final long publicationBytes;
|
||||
private final boolean regionalEnabled;
|
||||
|
||||
private HydrologyCacheBudget(long retainedBytes, boolean regionalEnabled) {
|
||||
plannerBytes = retainedBytes / 2L;
|
||||
publicationBytes = retainedBytes - plannerBytes;
|
||||
this.regionalEnabled = regionalEnabled;
|
||||
}
|
||||
|
||||
static HydrologyCacheBudget runtime() {
|
||||
return REGIONAL_RUNTIME;
|
||||
}
|
||||
|
||||
static HydrologyCacheBudget runtime(boolean regionalEnabled) {
|
||||
return regionalEnabled ? REGIONAL_RUNTIME : LOCAL_RUNTIME;
|
||||
}
|
||||
|
||||
static HydrologyCacheBudget forHeap(long maximumHeapBytes, boolean regionalEnabled) {
|
||||
if (maximumHeapBytes <= 0L) {
|
||||
throw new IllegalArgumentException("Maximum heap size must be positive.");
|
||||
}
|
||||
return new HydrologyCacheBudget(Math.clamp(maximumHeapBytes / 8L, 8L * MEBIBYTE, 256L * MEBIBYTE), regionalEnabled);
|
||||
}
|
||||
|
||||
static long sharedStudioBytes() {
|
||||
return Math.clamp(Runtime.getRuntime().maxMemory() / 128L, MEBIBYTE, 16L * MEBIBYTE);
|
||||
}
|
||||
|
||||
long tileBytes() {
|
||||
return publicationBytes * 3L / 4L;
|
||||
}
|
||||
|
||||
long columnBytes() {
|
||||
return publicationBytes / 8L;
|
||||
}
|
||||
|
||||
long diagnosticBytes() {
|
||||
return publicationBytes / 8L;
|
||||
}
|
||||
|
||||
long routingBytes() {
|
||||
return plannerBytes * 3L / 16L;
|
||||
}
|
||||
|
||||
long ownerBytes() {
|
||||
return regionalEnabled ? plannerBytes / 4L : plannerBytes * 3L / 4L;
|
||||
}
|
||||
|
||||
long edgeBytes() {
|
||||
return plannerBytes / 16L;
|
||||
}
|
||||
|
||||
long regionalDraftBytes() {
|
||||
return regionalEnabled ? plannerBytes / 8L : 0L;
|
||||
}
|
||||
|
||||
long regionalCoarseBytes() {
|
||||
return regionalEnabled ? plannerBytes / 8L : 0L;
|
||||
}
|
||||
|
||||
long regionalTerrainBytes() {
|
||||
return regionalEnabled ? plannerBytes / 4L : 0L;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCavePlan;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
final class HydrologyCacheWeights {
|
||||
private static final long ENTRY_BYTES = 192L;
|
||||
private static final long TERRAIN_BYTES = 256L;
|
||||
private static final long POINT_BYTES = 40L;
|
||||
|
||||
private HydrologyCacheWeights() {
|
||||
}
|
||||
|
||||
static int bounded(long estimatedBytes, long budget, int maximumEntries) {
|
||||
return saturated(Math.max(estimatedBytes, Math.ceilDiv(budget, maximumEntries)));
|
||||
}
|
||||
|
||||
static int bounded(long estimatedBytes, long budget, int maximumEntries, int units, int maximumUnits) {
|
||||
return bounded(Math.max(estimatedBytes, Math.ceilDiv((long) units * budget, maximumUnits)), budget, maximumEntries);
|
||||
}
|
||||
|
||||
static int tile(HydrologyTile tile) {
|
||||
long bytes = network(tile.nodes(), tile.edges(), tile.outlets(), tile.courses(), tile.cavePlans());
|
||||
bytes += diagnostics(tile.localDiagnosticCandidates());
|
||||
bytes += (long) tile.regionalCourseIds().size() * 64L;
|
||||
if (tile.resolvedOwner() != null) {
|
||||
bytes += owner(tile.resolvedOwner());
|
||||
}
|
||||
RiverFootprint footprint = tile.footprint();
|
||||
if (footprint != null) {
|
||||
bytes += (long) footprint.size() * 64L;
|
||||
for (HydrologyColumnSample sample : footprint.columns().values()) {
|
||||
bytes += column(sample);
|
||||
}
|
||||
}
|
||||
return saturated(bytes);
|
||||
}
|
||||
|
||||
static int column(HydrologyColumnSample sample) {
|
||||
return saturated(96L + (long) sample.layers().size() * 192L);
|
||||
}
|
||||
|
||||
static int diagnostics(List<HydrologyDiagnosticCandidate> diagnostics) {
|
||||
return saturated(ENTRY_BYTES + (long) diagnostics.size() * 112L);
|
||||
}
|
||||
|
||||
static int routing(SourceRoutingContext context) {
|
||||
return saturated(ENTRY_BYTES + grid(context.grid()) + routingPlan(context.surfaceRouting())
|
||||
+ routingPlan(context.undergroundRouting()) + diagnostics(context.diagnostics()));
|
||||
}
|
||||
|
||||
static int owner(CrossTileResolvedOwner owner) {
|
||||
HydrologyCaveCourseFilter.Result result = owner.draft().result();
|
||||
long bytes = network(result.nodes(), result.edges(), result.outlets(), result.courses(), result.cavePlans())
|
||||
+ diagnostics(owner.draft().diagnostics());
|
||||
for (CrossTileRejectedCourse rejection : owner.observedRejections()) {
|
||||
bytes += 48L + course(rejection.course());
|
||||
}
|
||||
return saturated(bytes);
|
||||
}
|
||||
|
||||
static int points(List<HydrologyPoint> points) {
|
||||
return saturated(ENTRY_BYTES + (long) points.size() * POINT_BYTES);
|
||||
}
|
||||
|
||||
static int regionalNetwork(HydrologyRegionalNetwork network) {
|
||||
return saturated(network(network.nodes(), network.edges(), network.outlets(), network.courses(), network.cavePlans())
|
||||
+ diagnostics(network.diagnostics()));
|
||||
}
|
||||
|
||||
static int regionalCoarse(HydrologyRegionalCoarsePlan plan) {
|
||||
HydrologyRegionalGraph.Tree tree = plan.tree();
|
||||
long treeBytes = tree == null ? 0L : 96L + (long) tree.selected().length * 8L
|
||||
+ ((long) tree.accumulation().length + tree.heads().length) * 4L + tree.retainedLabelCount() * 96L;
|
||||
return saturated(ENTRY_BYTES + grid(plan.grid()) + treeBytes + (long) plan.outlets().size() * 224L
|
||||
+ (long) plan.sources().size() * 32L + (long) plan.envelopes().size() * 64L);
|
||||
}
|
||||
|
||||
static int terrain(HydrologyTerrainSample terrain) {
|
||||
return saturated(128L + (terrain == null ? 0L : TERRAIN_BYTES));
|
||||
}
|
||||
|
||||
private static long grid(HydrologySampledGrid grid) {
|
||||
return grid == null ? 0L : 96L + (long) grid.nodes().size() * (64L + TERRAIN_BYTES + 104L);
|
||||
}
|
||||
|
||||
private static long routingPlan(HydrologyRoutingPlan plan) {
|
||||
if (plan == null) {
|
||||
return 0L;
|
||||
}
|
||||
return 128L + (long) plan.potential().length * 8L
|
||||
+ ((long) plan.parent().length + plan.outletIndex().length + plan.routeLengths().length) * 4L
|
||||
+ (long) plan.outlets().size() * 224L
|
||||
+ (plan.surfaceDrainage() == null ? 0L : plan.surfaceDrainage().maximumRetainedBytes());
|
||||
}
|
||||
|
||||
private static long network(List<DrainageNode> nodes, List<DrainageEdge> edges, List<RiverOutlet> outlets,
|
||||
List<RiverCourse> courses, List<HydrologyCavePlan> cavePlans) {
|
||||
long bytes = ENTRY_BYTES + (long) nodes.size() * (96L + TERRAIN_BYTES) + (long) outlets.size() * 224L;
|
||||
for (DrainageEdge edge : edges) {
|
||||
bytes += 96L + points(edge.centerline());
|
||||
}
|
||||
for (RiverCourse course : courses) {
|
||||
bytes += course(course);
|
||||
}
|
||||
for (HydrologyCavePlan plan : cavePlans) {
|
||||
bytes += plan.estimatedRetainedBytes();
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static long course(RiverCourse course) {
|
||||
long bytes = 128L;
|
||||
for (DrainageEdge edge : course.drainageEdges()) {
|
||||
bytes += 96L + points(edge.centerline());
|
||||
}
|
||||
for (HydraulicSegment segment : course.segments()) {
|
||||
bytes += 128L + points(segment.centerline()) + (long) segment.channelProfile().size() * 16L;
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static int saturated(long bytes) {
|
||||
return (int) Math.clamp(bytes, 1L, Integer.MAX_VALUE);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
public enum HydrologyCandidateKind {
|
||||
SOURCE,
|
||||
OUTLET,
|
||||
DEEP_FLUID,
|
||||
POOL,
|
||||
/** A later course to an outlet that could not be joined to the outlet's main course. */
|
||||
TRIBUTARY,
|
||||
REGIONAL_SOURCE,
|
||||
COASTAL_CHANNEL
|
||||
}
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
public enum HydrologyCandidateRejection {
|
||||
NO_LEGAL_OUTLET,
|
||||
NO_DRAINAGE_PATH,
|
||||
CONFINED_NO_OUTLET,
|
||||
ROUTE_LIMIT,
|
||||
COURSE_TOO_SHORT,
|
||||
SOURCE_QUOTA,
|
||||
SOURCE_SPACING,
|
||||
OUTLET_LIMIT,
|
||||
OUTLET_LEVEL,
|
||||
CAVE_CONTAINMENT,
|
||||
VOLUME_LIMIT,
|
||||
SURFACE_HEAD_RANGE,
|
||||
SURFACE_HEAD_DISTRIBUTION,
|
||||
SURFACE_EXPOSURE,
|
||||
SURFACE_DROP_UNSUPPORTED,
|
||||
SURFACE_CORRIDOR_UNSUPPORTED,
|
||||
SURFACE_TRANSITION_UNSUPPORTED,
|
||||
SURFACE_SEGMENT_UNSUPPORTED,
|
||||
SURFACE_SINKHOLE_CLEARANCE,
|
||||
SURFACE_SHAPE_UNSUPPORTED,
|
||||
SURFACE_MOUTH_DISCONNECTED,
|
||||
SURFACE_WATER_CONTAINMENT,
|
||||
SURFACE_BANK_BUDGET,
|
||||
POLICY_EXCLUDED;
|
||||
}
|
||||
-1284
File diff suppressed because it is too large.
Load diff
-14
@@ -1,14 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.cave.CaveVoxelView;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface HydrologyCaveVoxelViewFactory {
|
||||
CaveVoxelView create(PlannedSurface plannedSurface);
|
||||
|
||||
interface PlannedSurface {
|
||||
int resolve(int x, int z, int naturalHeight);
|
||||
|
||||
boolean ownsTerrain(int x, int z);
|
||||
}
|
||||
}
|
||||
Loaded 100 of 1065 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user