mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-27 12:41:43 +00:00
d
d
This commit is contained in:
@@ -184,6 +184,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
private final AtomicBoolean serverStopTeardownDeferred = new AtomicBoolean(false);
|
||||
private final AtomicBoolean servicesDisabled = 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 volatile PlaceholderRegistration papiRegistration;
|
||||
private volatile IrisPapiListener papiListener;
|
||||
@@ -588,6 +589,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
serverStopTeardownDeferred.set(false);
|
||||
servicesDisabled.set(false);
|
||||
sharedRuntimeClosed.set(false);
|
||||
startupBoundaryRestart.set(false);
|
||||
terminalCleanupCompleted.set(false);
|
||||
deferredShutdownGenerators.clear();
|
||||
MultiBurst.burst.reopen();
|
||||
@@ -827,6 +829,12 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
BukkitGuiHost.install();
|
||||
// super.onEnable() already registers this instance as a listener.
|
||||
super.onEnable();
|
||||
if (IrisStartupValidation.isRestartRequired()) {
|
||||
String restartReason = IrisStartupValidation.denialReason()
|
||||
.orElse("Iris startup validation requires a restart.");
|
||||
startupBoundaryRestart.set(true);
|
||||
ServerConfigurator.restartAtStartupBoundary(restartReason);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -863,7 +871,10 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
public void onDisable() {
|
||||
teardownPapi();
|
||||
boolean serverStopping = IrisToolbelt.isServerStopping();
|
||||
if (serverStopping) {
|
||||
boolean restartingAtStartupBoundary = startupBoundaryRestart.get();
|
||||
if (restartingAtStartupBoundary) {
|
||||
teardownRuntime("startup-boundary-restart", 30L);
|
||||
} else if (serverStopping) {
|
||||
quiesceRuntimeForServerShutdown("onDisable");
|
||||
startPostStopFinisher();
|
||||
} else {
|
||||
@@ -881,7 +892,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
}
|
||||
// super.onDisable() cancels plugin tasks and unregisters every listener.
|
||||
super.onDisable();
|
||||
if (!serverStopping) {
|
||||
if (!serverStopping || restartingAtStartupBoundary) {
|
||||
finishTerminalCleanup();
|
||||
}
|
||||
}
|
||||
@@ -1017,6 +1028,10 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
}
|
||||
|
||||
private void runShutdownHook() {
|
||||
if (startupBoundaryRestart.get()) {
|
||||
finishDeferredRuntimeTeardown("startup-boundary-restart-hook", 30L);
|
||||
return;
|
||||
}
|
||||
if (!awaitServerShutdownBoundary()) {
|
||||
Iris.warn("Iris skipped JVM-hook runtime teardown because Paper did not reach its post-world-close boundary.");
|
||||
return;
|
||||
|
||||
+6
-1
@@ -2,6 +2,11 @@ package art.arcane.iris.api.terrain;
|
||||
|
||||
public enum IrisColumnField {
|
||||
SURFACE_HEIGHT,
|
||||
NATURAL_HEIGHT,
|
||||
SURFACE_KIND,
|
||||
BIOME_KEY
|
||||
BIOME_KEY,
|
||||
RIVER_STATE,
|
||||
RIVER_DISTANCE,
|
||||
RIVER_FLOW,
|
||||
RIVER_WATER_SURFACE_Y
|
||||
}
|
||||
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package art.arcane.iris.api.terrain;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record IrisColumnSample(
|
||||
int blockX,
|
||||
int blockZ,
|
||||
int surfaceHeight,
|
||||
int naturalHeight,
|
||||
IrisSurfaceKind surfaceKind,
|
||||
String biomeKey,
|
||||
IrisRiverState riverState,
|
||||
double riverDistance,
|
||||
int riverFlow,
|
||||
int riverWaterSurfaceY
|
||||
) {
|
||||
public static final int UNAVAILABLE_HEIGHT = Integer.MIN_VALUE;
|
||||
public static final double UNAVAILABLE_RIVER_DISTANCE = Double.NaN;
|
||||
public static final int UNAVAILABLE_RIVER_FLOW = -1;
|
||||
|
||||
public IrisColumnSample {
|
||||
Objects.requireNonNull(surfaceKind, "surfaceKind");
|
||||
Objects.requireNonNull(riverState, "riverState");
|
||||
biomeKey = biomeKey == null || biomeKey.isBlank() ? null : biomeKey;
|
||||
if (!Double.isNaN(riverDistance) && (!Double.isFinite(riverDistance) || riverDistance < 0D)) {
|
||||
throw new IllegalArgumentException("riverDistance must be non-negative, finite, or unavailable");
|
||||
}
|
||||
if (riverFlow < UNAVAILABLE_RIVER_FLOW) {
|
||||
throw new IllegalArgumentException("riverFlow must be non-negative or unavailable");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasSurfaceHeight() {
|
||||
return surfaceHeight != UNAVAILABLE_HEIGHT;
|
||||
}
|
||||
|
||||
public boolean hasNaturalHeight() {
|
||||
return naturalHeight != UNAVAILABLE_HEIGHT;
|
||||
}
|
||||
|
||||
public boolean hasSurfaceKind() {
|
||||
return surfaceKind != IrisSurfaceKind.UNKNOWN;
|
||||
}
|
||||
|
||||
public boolean hasBiomeKey() {
|
||||
return biomeKey != null;
|
||||
}
|
||||
|
||||
public boolean hasRiverState() {
|
||||
return riverState != IrisRiverState.NONE;
|
||||
}
|
||||
|
||||
public boolean hasRiverDistance() {
|
||||
return !Double.isNaN(riverDistance);
|
||||
}
|
||||
|
||||
public boolean hasRiverFlow() {
|
||||
return riverFlow != UNAVAILABLE_RIVER_FLOW;
|
||||
}
|
||||
|
||||
public boolean hasRiverWaterSurfaceY() {
|
||||
return riverWaterSurfaceY != UNAVAILABLE_HEIGHT;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -2,5 +2,5 @@ package art.arcane.iris.api.terrain;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface IrisColumnSink {
|
||||
void accept(int blockX, int blockZ, int surfaceHeight, IrisSurfaceKind kind, String biomeKey);
|
||||
void accept(IrisColumnSample sample);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package art.arcane.iris.api.terrain;
|
||||
|
||||
public enum IrisRiverState {
|
||||
NONE,
|
||||
WET,
|
||||
DRY
|
||||
}
|
||||
@@ -4,6 +4,9 @@ public enum IrisSurfaceKind {
|
||||
UNKNOWN,
|
||||
LAND,
|
||||
SHORE,
|
||||
RIVER,
|
||||
RIVER_SHORE,
|
||||
DRY_CHANNEL,
|
||||
OCEAN,
|
||||
VOID
|
||||
}
|
||||
|
||||
+85
-17
@@ -31,22 +31,21 @@ import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* Answer to a generator-plugin discovery probe.
|
||||
* <p>
|
||||
* Multiverse-Core calls {@code getDefaultWorldGenerator} with an empty dimension id and the name of
|
||||
* an already loaded world purely to find out whether a plugin is a generator plugin, then throws the
|
||||
* returned instance away. A non-null answer keeps Iris in {@code /mv generators} and in Multiverse's
|
||||
* generator tab-completion without Multiverse logging a warning block on every boot.
|
||||
* <p>
|
||||
* Nothing here can build terrain: every generation entry point refuses, so an instance that escapes
|
||||
* the probe fails loudly instead of silently producing vanilla chunks.
|
||||
*/
|
||||
final class IrisProbeChunkGenerator extends ChunkGenerator {
|
||||
private final String worldName;
|
||||
final class IrisFailClosedChunkGenerator extends ChunkGenerator {
|
||||
private final String refusalMessage;
|
||||
|
||||
IrisProbeChunkGenerator(String worldName) {
|
||||
this.worldName = Objects.requireNonNull(worldName, "worldName");
|
||||
private IrisFailClosedChunkGenerator(String refusalMessage) {
|
||||
this.refusalMessage = Objects.requireNonNull(refusalMessage, "refusalMessage");
|
||||
}
|
||||
|
||||
static IrisFailClosedChunkGenerator discoveryProbe(String worldName) {
|
||||
return new IrisFailClosedChunkGenerator("Iris generator-discovery probe for '" + worldName
|
||||
+ "' was asked to generate terrain. Iris worlds are created with /iris create.");
|
||||
}
|
||||
|
||||
static IrisFailClosedChunkGenerator startupLock(String worldName, String denialReason) {
|
||||
return new IrisFailClosedChunkGenerator("Iris generation for '" + worldName
|
||||
+ "' remains locked: " + denialReason);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -84,6 +83,11 @@ final class IrisProbeChunkGenerator extends ChunkGenerator {
|
||||
throw refusal();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canSpawn(@NotNull World world, int x, int z) {
|
||||
throw refusal();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BlockPopulator> getDefaultPopulators(@NotNull World world) {
|
||||
throw refusal();
|
||||
@@ -94,8 +98,72 @@ final class IrisProbeChunkGenerator extends ChunkGenerator {
|
||||
throw refusal();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldGenerateNoise() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldGenerateNoise(@NotNull WorldInfo worldInfo, @NotNull Random random, int x, int z) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldGenerateSurface() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldGenerateSurface(@NotNull WorldInfo worldInfo, @NotNull Random random, int x, int z) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldGenerateBedrock() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldGenerateCaves() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldGenerateCaves(@NotNull WorldInfo worldInfo, @NotNull Random random, int x, int z) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldGenerateDecorations() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldGenerateDecorations(@NotNull WorldInfo worldInfo, @NotNull Random random, int x, int z) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldGenerateMobs() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldGenerateMobs(@NotNull WorldInfo worldInfo, @NotNull Random random, int x, int z) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldGenerateStructures() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldGenerateStructures(@NotNull WorldInfo worldInfo, @NotNull Random random, int x, int z) {
|
||||
return false;
|
||||
}
|
||||
|
||||
private IllegalStateException refusal() {
|
||||
return new IllegalStateException("Iris generator-discovery probe for '" + worldName
|
||||
+ "' was asked to generate terrain. Iris worlds are created with /iris create.");
|
||||
return new IllegalStateException(refusalMessage);
|
||||
}
|
||||
}
|
||||
+7
-2
@@ -349,9 +349,14 @@ public final class IrisWorldGeneratorResolver {
|
||||
}
|
||||
if (isGeneratorDiscoveryProbe(worldName, id)) {
|
||||
Iris.debug("Generator discovery probe for loaded world " + worldName);
|
||||
return new IrisProbeChunkGenerator(worldName);
|
||||
return IrisFailClosedChunkGenerator.discoveryProbe(worldName);
|
||||
}
|
||||
Optional<String> startupDenial = IrisStartupValidation.denialReason();
|
||||
if (startupDenial.isPresent()) {
|
||||
Iris.warn("Keeping configured Iris world '" + worldName
|
||||
+ "' generation-locked: " + startupDenial.get());
|
||||
return IrisFailClosedChunkGenerator.startupLock(worldName, startupDenial.get());
|
||||
}
|
||||
IrisStartupValidation.requireWorldCreationReady();
|
||||
ChunkGenerator stagedGenerator = WorldLifecycleStaging.consumeGenerator(worldName);
|
||||
if (stagedGenerator != null) {
|
||||
Iris.debug("Using staged runtime generator for " + worldName);
|
||||
|
||||
+3
-1
@@ -250,7 +250,9 @@ public class CommandObject implements DirectorExecutor {
|
||||
|
||||
@Override
|
||||
public int getFluidHeight() {
|
||||
return 63;
|
||||
return targetEngine == null
|
||||
? 63
|
||||
: targetEngine.getMinHeight() + targetEngine.getDimension().getFluidHeight();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+14
-14
@@ -18,7 +18,6 @@
|
||||
|
||||
package art.arcane.iris.core.commands;
|
||||
|
||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
@@ -47,7 +46,6 @@ import art.arcane.iris.engine.object.IrisNoiseGenerator;
|
||||
import art.arcane.iris.engine.object.IrisObject;
|
||||
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.IrisWorld;
|
||||
import art.arcane.iris.engine.object.NoiseStyle;
|
||||
import art.arcane.iris.engine.platform.EngineBukkitOps;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
@@ -62,7 +60,6 @@ import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
import art.arcane.volmlib.util.function.Function2;
|
||||
import art.arcane.volmlib.util.function.NoiseProvider;
|
||||
import art.arcane.iris.util.project.interpolation.InterpolationMethod;
|
||||
import art.arcane.volmlib.util.io.IO;
|
||||
@@ -102,7 +99,6 @@ import java.util.Objects;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import art.arcane.iris.core.localization.IrisLanguage;
|
||||
import art.arcane.iris.core.localization.BukkitCommandMessages;
|
||||
@@ -264,8 +260,8 @@ public class CommandStudio implements DirectorExecutor {
|
||||
return;
|
||||
}
|
||||
|
||||
Supplier<Function2<Double, Double, Double>> supplier = () -> (x, z) -> generator.getHeight(x, z, new RNG(seed).nextParallelRNG(3245).lmax());
|
||||
NoiseExplorerGUI.launch(supplier, "Custom Generator");
|
||||
String generatorKey = generator.getLoadKey();
|
||||
NoiseExplorerGUI.launchGeneratorKey(generatorKey, generator, seed);
|
||||
}
|
||||
|
||||
@Director(description = "Show loot if a chest were right here", descriptionKey = "iris.director.commandstudio.director.show_loot_if_chest_were_right_here", origin = DirectorOrigin.PLAYER, sync = true)
|
||||
@@ -648,7 +644,8 @@ public class CommandStudio implements DirectorExecutor {
|
||||
|
||||
@Director(description = "Teleport to the active studio world", descriptionKey = "iris.director.commandstudio.director.teleport_active_studio_world", aliases = "stp", origin = DirectorOrigin.PLAYER, sync = true)
|
||||
public void tpstudio() {
|
||||
if (!Iris.service(StudioSVC.class).isProjectOpen()) {
|
||||
StudioSVC studioService = Iris.service(StudioSVC.class);
|
||||
if (!studioService.isProjectOpen()) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_NO_STUDIO_WORLD_IS_OPEN));
|
||||
return;
|
||||
}
|
||||
@@ -660,13 +657,16 @@ public class CommandStudio implements DirectorExecutor {
|
||||
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_SENDING_YOU_STUDIO_WORLD));
|
||||
Player player = player();
|
||||
IrisWorld studioWorld = Iris.service(StudioSVC.class)
|
||||
.getActiveProject()
|
||||
.getActiveProvider()
|
||||
.getTarget()
|
||||
.getWorld();
|
||||
BukkitPlatform.teleportAsync(player, BukkitWorldBinding.spawnLocation(studioWorld))
|
||||
.thenRun(() -> player.setGameMode(GameMode.CREATIVE));
|
||||
studioService.teleportToActiveProject(player)
|
||||
.whenComplete((teleported, failure) -> {
|
||||
if (failure != null) {
|
||||
Iris.reportError("Studio teleport failed for player \"" + player.getName() + "\".", failure);
|
||||
return;
|
||||
}
|
||||
if (Boolean.TRUE.equals(teleported)) {
|
||||
J.runEntity(player, () -> player.setGameMode(GameMode.CREATIVE));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Director(description = "Update your dimension projects VSCode workspace", descriptionKey = "iris.director.commandstudio.director.update_your_dimension_projects_vscode_workspace")
|
||||
|
||||
+1
-1
@@ -169,7 +169,7 @@ public final class BukkitVisionOverlay implements GuiOverlay {
|
||||
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 ->
|
||||
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();
|
||||
|
||||
+67
-5
@@ -2,7 +2,9 @@ package art.arcane.iris.core.service;
|
||||
|
||||
import art.arcane.iris.api.terrain.IrisColumnField;
|
||||
import art.arcane.iris.api.terrain.IrisColumnQuery;
|
||||
import art.arcane.iris.api.terrain.IrisColumnSample;
|
||||
import art.arcane.iris.api.terrain.IrisColumnSink;
|
||||
import art.arcane.iris.api.terrain.IrisRiverState;
|
||||
import art.arcane.iris.api.terrain.IrisSurfaceKind;
|
||||
import art.arcane.iris.api.terrain.IrisTerrainService;
|
||||
import art.arcane.iris.api.terrain.IrisWorldInfo;
|
||||
@@ -17,6 +19,8 @@ import art.arcane.iris.engine.object.InferredType;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.iris.engine.river.RiverRouteState;
|
||||
import art.arcane.iris.engine.river.runtime.IrisRiverSurfaceSample;
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
@@ -101,13 +105,14 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService {
|
||||
|
||||
try {
|
||||
int surface = engine.getHeight(blockX, blockZ);
|
||||
int fluid = engine.getDimension().getFluidHeight();
|
||||
IrisRiverSurfaceSample riverSurface = engine.getComplex().getRiverSurfaceStream().get(blockX, blockZ);
|
||||
int fluid = (int) Math.round(riverSurface.waterSurfaceY());
|
||||
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);
|
||||
return IrisSurfaceClassifier.classify(surface, fluid, inferredType, riverSurface);
|
||||
} catch (Throwable error) {
|
||||
reportQueryFault("surfaceKind", world, error);
|
||||
return IrisSurfaceKind.UNKNOWN;
|
||||
@@ -224,26 +229,72 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService {
|
||||
|
||||
EnumSet<IrisColumnField> fields = query.fields();
|
||||
boolean wantHeight = fields.contains(IrisColumnField.SURFACE_HEIGHT);
|
||||
boolean wantNaturalHeight = fields.contains(IrisColumnField.NATURAL_HEIGHT);
|
||||
boolean wantKind = fields.contains(IrisColumnField.SURFACE_KIND);
|
||||
boolean wantBiome = fields.contains(IrisColumnField.BIOME_KEY);
|
||||
boolean wantRiverState = fields.contains(IrisColumnField.RIVER_STATE);
|
||||
boolean wantRiverDistance = fields.contains(IrisColumnField.RIVER_DISTANCE);
|
||||
boolean wantRiverFlow = fields.contains(IrisColumnField.RIVER_FLOW);
|
||||
boolean wantRiverWaterSurface = fields.contains(IrisColumnField.RIVER_WATER_SURFACE_Y);
|
||||
boolean wantRiver = wantKind || wantRiverState || wantRiverDistance || wantRiverFlow
|
||||
|| wantRiverWaterSurface;
|
||||
|
||||
try {
|
||||
int minHeight = engine.getMinHeight();
|
||||
int fluid = engine.getDimension().getFluidHeight();
|
||||
long visited = IrisColumnWalk.walk(query, (int blockX, int blockZ) -> {
|
||||
if (engine.isClosed()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
IrisRiverSurfaceSample riverSurface = wantRiver
|
||||
? engine.getComplex().getRiverSurfaceStream().get(blockX, blockZ)
|
||||
: null;
|
||||
int surface = wantHeight || wantKind ? engine.getHeight(blockX, blockZ) : 0;
|
||||
int fluid = riverSurface == null
|
||||
? engine.getDimension().getFluidHeight()
|
||||
: (int) Math.round(riverSurface.waterSurfaceY());
|
||||
boolean needsBiome = wantBiome
|
||||
|| (wantKind && IrisSurfaceClassifier.requiresSurfaceBiome(surface, fluid));
|
||||
IrisBiome biome = needsBiome ? engine.getSurfaceBiome(blockX, blockZ) : null;
|
||||
IrisSurfaceKind kind = wantKind
|
||||
? IrisSurfaceClassifier.classify(surface, fluid, biome == null ? null : biome.getInferredType())
|
||||
? IrisSurfaceClassifier.classify(
|
||||
surface,
|
||||
fluid,
|
||||
biome == null ? null : biome.getInferredType(),
|
||||
riverSurface
|
||||
)
|
||||
: IrisSurfaceKind.UNKNOWN;
|
||||
String biomeKey = wantBiome && biome != null ? biome.getLoadKey() : null;
|
||||
sink.accept(blockX, blockZ, wantHeight ? surface + minHeight : -1, kind, biomeKey);
|
||||
int natural = wantNaturalHeight
|
||||
? (int) Math.round(engine.getComplex().getNaturalHeightStream().get(blockX, blockZ)) + minHeight
|
||||
: IrisColumnSample.UNAVAILABLE_HEIGHT;
|
||||
boolean riverPresent = riverSurface != null && riverSurface.river().present();
|
||||
IrisRiverState riverState = wantRiverState && riverSurface != null
|
||||
? riverState(riverSurface)
|
||||
: IrisRiverState.NONE;
|
||||
double riverDistance = wantRiverDistance && riverPresent
|
||||
? riverSurface.river().distance()
|
||||
: IrisColumnSample.UNAVAILABLE_RIVER_DISTANCE;
|
||||
int riverFlow = wantRiverFlow && riverPresent
|
||||
? riverSurface.river().flow()
|
||||
: IrisColumnSample.UNAVAILABLE_RIVER_FLOW;
|
||||
int riverWaterSurfaceY = wantRiverWaterSurface
|
||||
&& riverPresent
|
||||
&& riverSurface.river().state() == RiverRouteState.WET
|
||||
? fluid + minHeight
|
||||
: IrisColumnSample.UNAVAILABLE_HEIGHT;
|
||||
sink.accept(new IrisColumnSample(
|
||||
blockX,
|
||||
blockZ,
|
||||
wantHeight ? surface + minHeight : IrisColumnSample.UNAVAILABLE_HEIGHT,
|
||||
natural,
|
||||
kind,
|
||||
biomeKey,
|
||||
riverState,
|
||||
riverDistance,
|
||||
riverFlow,
|
||||
riverWaterSurfaceY
|
||||
));
|
||||
return true;
|
||||
});
|
||||
return visited == query.columnCount();
|
||||
@@ -253,6 +304,17 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService {
|
||||
}
|
||||
}
|
||||
|
||||
static IrisRiverState riverState(IrisRiverSurfaceSample surface) {
|
||||
if (!surface.river().present()) {
|
||||
return IrisRiverState.NONE;
|
||||
}
|
||||
return switch (surface.river().state()) {
|
||||
case WET -> IrisRiverState.WET;
|
||||
case DRY -> IrisRiverState.DRY;
|
||||
case SUPPRESSED -> IrisRiverState.NONE;
|
||||
};
|
||||
}
|
||||
|
||||
private static Optional<String> key(IrisBiome biome) {
|
||||
String loadKey = biome == null ? null : biome.getLoadKey();
|
||||
return loadKey == null || loadKey.isEmpty() ? Optional.empty() : Optional.of(loadKey);
|
||||
|
||||
+31
@@ -2,6 +2,9 @@ package art.arcane.iris.core.service.terrain;
|
||||
|
||||
import art.arcane.iris.api.terrain.IrisSurfaceKind;
|
||||
import art.arcane.iris.engine.object.InferredType;
|
||||
import art.arcane.iris.engine.river.RiverRouteState;
|
||||
import art.arcane.iris.engine.river.RiverSection;
|
||||
import art.arcane.iris.engine.river.runtime.IrisRiverSurfaceSample;
|
||||
|
||||
public final class IrisSurfaceClassifier {
|
||||
private IrisSurfaceClassifier() {
|
||||
@@ -22,4 +25,32 @@ public final class IrisSurfaceClassifier {
|
||||
|
||||
return inferredType == InferredType.SHORE ? IrisSurfaceKind.SHORE : IrisSurfaceKind.LAND;
|
||||
}
|
||||
|
||||
public static IrisSurfaceKind classify(
|
||||
int engineSurfaceHeight,
|
||||
int engineFluidHeight,
|
||||
InferredType inferredType,
|
||||
IrisRiverSurfaceSample riverSurface
|
||||
) {
|
||||
if (engineSurfaceHeight <= 0) {
|
||||
return IrisSurfaceKind.VOID;
|
||||
}
|
||||
if (riverSurface != null && riverSurface.river().present() && !riverSurface.subterranean()) {
|
||||
if (riverSurface.river().state() == RiverRouteState.DRY) {
|
||||
return riverSurface.river().section() == RiverSection.DRY_CHANNEL
|
||||
? IrisSurfaceKind.DRY_CHANNEL
|
||||
: IrisSurfaceKind.LAND;
|
||||
}
|
||||
if (riverSurface.river().state() == RiverRouteState.WET) {
|
||||
RiverSection section = riverSurface.river().section();
|
||||
if (section == RiverSection.BANK) {
|
||||
return IrisSurfaceKind.RIVER_SHORE;
|
||||
}
|
||||
if (section == RiverSection.CHANNEL || section == RiverSection.MOUTH) {
|
||||
return IrisSurfaceKind.RIVER;
|
||||
}
|
||||
}
|
||||
}
|
||||
return classify(engineSurfaceHeight, engineFluidHeight, inferredType);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,12 +31,17 @@ public class IrisShutdownOrderingTest {
|
||||
"public void quiesceForServerShutdown()", "public boolean isStudio()");
|
||||
|
||||
assertOrdered(onDisable,
|
||||
"if (serverStopping)",
|
||||
"startupBoundaryRestart.get()",
|
||||
"teardownRuntime(\"startup-boundary-restart\", 30L)",
|
||||
"else if (serverStopping)",
|
||||
"quiesceRuntimeForServerShutdown(\"onDisable\")",
|
||||
"startPostStopFinisher()",
|
||||
"else",
|
||||
"} else {",
|
||||
"teardownRuntime(\"onDisable\", 30L)");
|
||||
assertOrdered(shutdownHook,
|
||||
"startupBoundaryRestart.get()",
|
||||
"finishDeferredRuntimeTeardown(\"startup-boundary-restart-hook\", 30L)",
|
||||
"return;",
|
||||
"awaitServerShutdownBoundary()",
|
||||
"finishDeferredRuntimeTeardown(\"shutdown-hook\", 30L)");
|
||||
assertOrdered(finisher,
|
||||
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
package art.arcane.iris.api.terrain;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
public class IrisColumnSampleTest {
|
||||
@Test
|
||||
public void unavailableFieldsHaveUnambiguousSentinels() {
|
||||
IrisColumnSample sample = sample(
|
||||
IrisColumnSample.UNAVAILABLE_HEIGHT,
|
||||
IrisColumnSample.UNAVAILABLE_HEIGHT,
|
||||
IrisSurfaceKind.UNKNOWN,
|
||||
null,
|
||||
IrisRiverState.NONE,
|
||||
IrisColumnSample.UNAVAILABLE_RIVER_DISTANCE,
|
||||
IrisColumnSample.UNAVAILABLE_RIVER_FLOW,
|
||||
IrisColumnSample.UNAVAILABLE_HEIGHT
|
||||
);
|
||||
|
||||
assertFalse(sample.hasSurfaceHeight());
|
||||
assertFalse(sample.hasNaturalHeight());
|
||||
assertFalse(sample.hasSurfaceKind());
|
||||
assertFalse(sample.hasBiomeKey());
|
||||
assertFalse(sample.hasRiverState());
|
||||
assertFalse(sample.hasRiverDistance());
|
||||
assertFalse(sample.hasRiverFlow());
|
||||
assertFalse(sample.hasRiverWaterSurfaceY());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void negativeWorldHeightsRemainAvailableValues() {
|
||||
IrisColumnSample sample = sample(
|
||||
-1,
|
||||
-64,
|
||||
IrisSurfaceKind.DRY_CHANNEL,
|
||||
"test:river",
|
||||
IrisRiverState.DRY,
|
||||
0D,
|
||||
0,
|
||||
-1
|
||||
);
|
||||
|
||||
assertTrue(sample.hasSurfaceHeight());
|
||||
assertTrue(sample.hasNaturalHeight());
|
||||
assertTrue(sample.hasSurfaceKind());
|
||||
assertTrue(sample.hasBiomeKey());
|
||||
assertTrue(sample.hasRiverState());
|
||||
assertTrue(sample.hasRiverDistance());
|
||||
assertTrue(sample.hasRiverFlow());
|
||||
assertTrue(sample.hasRiverWaterSurfaceY());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void blankBiomeKeysNormalizeToUnavailable() {
|
||||
IrisColumnSample sample = sample(
|
||||
64,
|
||||
65,
|
||||
IrisSurfaceKind.LAND,
|
||||
" ",
|
||||
IrisRiverState.NONE,
|
||||
IrisColumnSample.UNAVAILABLE_RIVER_DISTANCE,
|
||||
IrisColumnSample.UNAVAILABLE_RIVER_FLOW,
|
||||
IrisColumnSample.UNAVAILABLE_HEIGHT
|
||||
);
|
||||
|
||||
assertNull(sample.biomeKey());
|
||||
assertFalse(sample.hasBiomeKey());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void theSinkReceivesTheTypedSample() {
|
||||
IrisColumnSample sample = sample(
|
||||
64,
|
||||
65,
|
||||
IrisSurfaceKind.RIVER,
|
||||
"test:river",
|
||||
IrisRiverState.WET,
|
||||
0.5D,
|
||||
3,
|
||||
67
|
||||
);
|
||||
AtomicReference<IrisColumnSample> received = new AtomicReference<>();
|
||||
IrisColumnSink sink = received::set;
|
||||
|
||||
sink.accept(sample);
|
||||
|
||||
assertSame(sample, received.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidHydrologyValuesAreRejected() {
|
||||
assertInvalid(Double.POSITIVE_INFINITY, 1);
|
||||
assertInvalid(-0.1D, 1);
|
||||
assertInvalid(0D, -2);
|
||||
}
|
||||
|
||||
private static void assertInvalid(double distance, int flow) {
|
||||
try {
|
||||
sample(
|
||||
64,
|
||||
65,
|
||||
IrisSurfaceKind.RIVER,
|
||||
"test:river",
|
||||
IrisRiverState.WET,
|
||||
distance,
|
||||
flow,
|
||||
67
|
||||
);
|
||||
fail("Expected invalid hydrology values to be rejected");
|
||||
} catch (IllegalArgumentException expected) {
|
||||
assertTrue(expected.getMessage().startsWith("river"));
|
||||
}
|
||||
}
|
||||
|
||||
private static IrisColumnSample sample(
|
||||
int surfaceHeight,
|
||||
int naturalHeight,
|
||||
IrisSurfaceKind surfaceKind,
|
||||
String biomeKey,
|
||||
IrisRiverState riverState,
|
||||
double riverDistance,
|
||||
int riverFlow,
|
||||
int riverWaterSurfaceY
|
||||
) {
|
||||
return new IrisColumnSample(
|
||||
12,
|
||||
-7,
|
||||
surfaceHeight,
|
||||
naturalHeight,
|
||||
surfaceKind,
|
||||
biomeKey,
|
||||
riverState,
|
||||
riverDistance,
|
||||
riverFlow,
|
||||
riverWaterSurfaceY
|
||||
);
|
||||
}
|
||||
}
|
||||
+12
@@ -29,6 +29,18 @@ public class IrisStartupOrderingTest {
|
||||
"generatorResolver.validateAllPacks();");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void restartRequiredStartupTerminatesAfterPluginInitialization() throws Exception {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.startupSource")));
|
||||
String onEnable = section(source, "public void onEnable()", "public void onDisable()");
|
||||
|
||||
assertOrdered(onEnable,
|
||||
"BukkitGuiHost.install();",
|
||||
"super.onEnable();",
|
||||
"IrisStartupValidation.isRestartRequired()",
|
||||
"ServerConfigurator.restartAtStartupBoundary(restartReason);");
|
||||
}
|
||||
|
||||
private static String section(String source, String startMarker, String endMarker) {
|
||||
int start = source.indexOf(startMarker);
|
||||
int end = source.indexOf(endMarker, start);
|
||||
|
||||
+139
-4
@@ -1,6 +1,7 @@
|
||||
package art.arcane.iris.core;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.lifecycle.WorldLifecycleStaging;
|
||||
import art.arcane.iris.core.pack.BrokenPackException;
|
||||
import art.arcane.iris.core.pack.PackValidationRegistry;
|
||||
import art.arcane.iris.core.pack.PackValidationResult;
|
||||
@@ -22,11 +23,14 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@@ -39,8 +43,10 @@ public class IrisWorldGeneratorResolverTest {
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
@After
|
||||
public void clearValidationRegistry() {
|
||||
public void clearValidationState() {
|
||||
PackValidationRegistry.clear();
|
||||
IrisStartupValidation.disable();
|
||||
WorldLifecycleStaging.clearAll("world_nether");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -234,7 +240,9 @@ public class IrisWorldGeneratorResolverTest {
|
||||
|
||||
int plotSquaredProbe = resolver.indexOf("isPlotSquaredGeneratorDiscoveryProbe(worldName, id)");
|
||||
int probe = resolver.indexOf("isGeneratorDiscoveryProbe(worldName, id)", plotSquaredProbe);
|
||||
int readiness = resolver.indexOf("IrisStartupValidation.requireWorldCreationReady()");
|
||||
int denial = resolver.indexOf("IrisStartupValidation.denialReason()", probe);
|
||||
int failClosed = resolver.indexOf("IrisFailClosedChunkGenerator.startupLock(", denial);
|
||||
int staged = resolver.indexOf("WorldLifecycleStaging.consumeGenerator(worldName)", failClosed);
|
||||
int duplicateGuard = resolver.indexOf("requireWorldKeyAvailable(worldName, worldKey)");
|
||||
int ownership = resolver.indexOf("requireOwnedWorld(worldName, levelRoot, worldKey)");
|
||||
int frozen = resolver.indexOf("return resolveFrozenWorldGenerator(", ownership);
|
||||
@@ -245,8 +253,10 @@ public class IrisWorldGeneratorResolverTest {
|
||||
|
||||
assertTrue(plotSquaredProbe >= 0);
|
||||
assertTrue(probe > plotSquaredProbe);
|
||||
assertTrue(readiness > probe);
|
||||
assertTrue(duplicateGuard > readiness);
|
||||
assertTrue(denial > probe);
|
||||
assertTrue(failClosed > denial);
|
||||
assertTrue(staged > failClosed);
|
||||
assertTrue(duplicateGuard > staged);
|
||||
assertTrue(ownership > duplicateGuard);
|
||||
assertTrue(frozen > ownership);
|
||||
assertTrue(failureCapture > frozen);
|
||||
@@ -302,6 +312,8 @@ public class IrisWorldGeneratorResolverTest {
|
||||
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
|
||||
MockedStatic<Iris> iris = mockStatic(Iris.class)) {
|
||||
bukkit.when(() -> Bukkit.getWorld("world")).thenReturn(mock(World.class));
|
||||
IrisStartupValidation.begin();
|
||||
IrisStartupValidation.requireRestart("restart boundary");
|
||||
|
||||
ChunkGenerator probe = new IrisWorldGeneratorResolver(null)
|
||||
.resolveDefaultWorldGenerator("world", "");
|
||||
@@ -312,6 +324,95 @@ public class IrisWorldGeneratorResolverTest {
|
||||
IllegalStateException.class,
|
||||
() -> probe.generateNoise(mock(WorldInfo.class), new Random(), 0, 0, null));
|
||||
assertTrue(refusal.getMessage(), refusal.getMessage().contains("'world'"));
|
||||
assertTrue(refusal.getMessage(), refusal.getMessage().contains("discovery probe"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void restartRequiredDefaultWorldCannotFallBackToVanilla() {
|
||||
ChunkGenerator stagedGenerator = mock(ChunkGenerator.class);
|
||||
ChunkGenerator vanillaFallback = mock(ChunkGenerator.class);
|
||||
WorldLifecycleStaging.stageGenerator("world_nether", stagedGenerator, null);
|
||||
IrisStartupValidation.begin();
|
||||
IrisStartupValidation.requireRestart("updated external datapacks require restart");
|
||||
|
||||
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
|
||||
MockedStatic<Iris> iris = mockStatic(Iris.class)) {
|
||||
bukkit.when(() -> Bukkit.getWorld("world_nether")).thenReturn(null);
|
||||
|
||||
ChunkGenerator selected = craftBukkitGeneratorOrFallback(
|
||||
() -> new IrisWorldGeneratorResolver(null)
|
||||
.resolveDefaultWorldGenerator("world_nether", "underworld"),
|
||||
vanillaFallback
|
||||
);
|
||||
|
||||
assertNotSame(vanillaFallback, selected);
|
||||
assertNotSame(stagedGenerator, selected);
|
||||
assertSame(stagedGenerator, WorldLifecycleStaging.consumeGenerator("world_nether"));
|
||||
WorldInfo worldInfo = mock(WorldInfo.class);
|
||||
Random random = new Random();
|
||||
assertFalse(selected.shouldGenerateNoise());
|
||||
assertFalse(selected.shouldGenerateNoise(worldInfo, random, 0, 0));
|
||||
assertFalse(selected.shouldGenerateSurface());
|
||||
assertFalse(selected.shouldGenerateSurface(worldInfo, random, 0, 0));
|
||||
assertFalse(selected.shouldGenerateBedrock());
|
||||
assertFalse(selected.shouldGenerateCaves());
|
||||
assertFalse(selected.shouldGenerateCaves(worldInfo, random, 0, 0));
|
||||
assertFalse(selected.shouldGenerateDecorations());
|
||||
assertFalse(selected.shouldGenerateDecorations(worldInfo, random, 0, 0));
|
||||
assertFalse(selected.shouldGenerateMobs());
|
||||
assertFalse(selected.shouldGenerateMobs(worldInfo, random, 0, 0));
|
||||
assertFalse(selected.shouldGenerateStructures());
|
||||
assertFalse(selected.shouldGenerateStructures(worldInfo, random, 0, 0));
|
||||
IllegalStateException refusal = assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> selected.generateNoise(
|
||||
worldInfo,
|
||||
random,
|
||||
0,
|
||||
0,
|
||||
mock(ChunkGenerator.ChunkData.class)
|
||||
)
|
||||
);
|
||||
assertTrue(refusal.getMessage(), refusal.getMessage().contains("world_nether"));
|
||||
assertTrue(refusal.getMessage(), refusal.getMessage().contains("updated external datapacks require restart"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void everyBlockingStartupStateReturnsFailClosedGenerator() {
|
||||
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
|
||||
MockedStatic<Iris> iris = mockStatic(Iris.class)) {
|
||||
IrisStartupValidation.begin();
|
||||
assertStartupStateFailsClosed("world_pending", "external datapacks");
|
||||
|
||||
IrisStartupValidation.begin();
|
||||
IrisStartupValidation.markDatapacksInvalid("invalid external datapack state");
|
||||
assertStartupStateFailsClosed("world_datapack_invalid", "invalid external datapack state");
|
||||
|
||||
IrisStartupValidation.begin();
|
||||
IrisStartupValidation.markDatapacksReady();
|
||||
IrisStartupValidation.markPacksInvalid(List.of("invalid dimension pack state"));
|
||||
assertStartupStateFailsClosed("world_pack_invalid", "invalid dimension pack state");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readyStartupStillConsumesStagedGenerator() {
|
||||
ChunkGenerator stagedGenerator = mock(ChunkGenerator.class);
|
||||
WorldLifecycleStaging.stageGenerator("world_nether", stagedGenerator, null);
|
||||
IrisStartupValidation.begin();
|
||||
IrisStartupValidation.markDatapacksReady();
|
||||
IrisStartupValidation.markPacksReady();
|
||||
|
||||
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
|
||||
MockedStatic<Iris> iris = mockStatic(Iris.class)) {
|
||||
bukkit.when(() -> Bukkit.getWorld("world_nether")).thenReturn(null);
|
||||
|
||||
ChunkGenerator selected = new IrisWorldGeneratorResolver(null)
|
||||
.resolveDefaultWorldGenerator("world_nether", "underworld");
|
||||
|
||||
assertSame(stagedGenerator, selected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -436,4 +537,38 @@ public class IrisWorldGeneratorResolverTest {
|
||||
"{\"name\":\"Biome\"}",
|
||||
StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static void assertStartupStateFailsClosed(String worldName, String expectedReason) {
|
||||
ChunkGenerator vanillaFallback = mock(ChunkGenerator.class);
|
||||
ChunkGenerator selected = craftBukkitGeneratorOrFallback(
|
||||
() -> new IrisWorldGeneratorResolver(null)
|
||||
.resolveDefaultWorldGenerator(worldName, "overworld"),
|
||||
vanillaFallback
|
||||
);
|
||||
assertNotSame(vanillaFallback, selected);
|
||||
IllegalStateException refusal = assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> selected.generateNoise(
|
||||
mock(WorldInfo.class),
|
||||
new Random(),
|
||||
0,
|
||||
0,
|
||||
mock(ChunkGenerator.ChunkData.class)
|
||||
)
|
||||
);
|
||||
assertTrue(refusal.getMessage(), refusal.getMessage().contains(worldName));
|
||||
assertTrue(refusal.getMessage(), refusal.getMessage().contains(expectedReason));
|
||||
}
|
||||
|
||||
private static ChunkGenerator craftBukkitGeneratorOrFallback(
|
||||
Supplier<ChunkGenerator> resolution,
|
||||
ChunkGenerator fallback
|
||||
) {
|
||||
try {
|
||||
ChunkGenerator selected = resolution.get();
|
||||
return selected == null ? fallback : selected;
|
||||
} catch (Throwable ignoredGeneratorFailure) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package art.arcane.iris.core.commands;
|
||||
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IObjectPlacer;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.util.project.stream.ProceduralStream;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class CommandObjectFluidHeightTest {
|
||||
@Test
|
||||
public void absoluteObjectPlacementShiftsTheColumnRiverHeadFromNegativeMinY() {
|
||||
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);
|
||||
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);
|
||||
|
||||
IObjectPlacer placer = CommandObject.createPlacer(world, future, engine);
|
||||
|
||||
assertEquals(63, placer.getFluidHeight());
|
||||
assertEquals(67, placer.getFluidHeight(12, -7));
|
||||
verify(riverHead).get(12, -7);
|
||||
}
|
||||
}
|
||||
+15
@@ -21,4 +21,19 @@ public class StudioPlayerModeContractTest {
|
||||
assertTrue(plugin.contains("GameMode.CREATIVE"));
|
||||
assertTrue(commands.contains("GameMode.CREATIVE"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tpStudioUsesThePreparedCoordinatorEntry() throws IOException {
|
||||
String commands = Files.readString(Path.of(
|
||||
"src/main/java/art/arcane/iris/core/commands/CommandStudio.java")).replace("\r\n", "\n");
|
||||
int methodStart = commands.indexOf("public void tpstudio()");
|
||||
int methodEnd = commands.indexOf("\n @Director", methodStart);
|
||||
String method = commands.substring(methodStart, methodEnd);
|
||||
|
||||
assertTrue(method.contains("StudioSVC studioService = Iris.service(StudioSVC.class)"));
|
||||
assertTrue(method.contains("studioService.teleportToActiveProject(player)"));
|
||||
assertFalse(method.contains("getActiveProject()"));
|
||||
assertFalse(method.contains("BukkitPlatform.teleportAsync"));
|
||||
assertFalse(method.contains("BukkitWorldBinding.spawnLocation"));
|
||||
}
|
||||
}
|
||||
|
||||
+42
-3
@@ -2,8 +2,15 @@ package art.arcane.iris.core.service;
|
||||
|
||||
import art.arcane.iris.api.terrain.IrisColumnField;
|
||||
import art.arcane.iris.api.terrain.IrisColumnQuery;
|
||||
import art.arcane.iris.api.terrain.IrisRiverState;
|
||||
import art.arcane.iris.api.terrain.IrisSurfaceKind;
|
||||
import art.arcane.iris.api.terrain.IrisTerrainService;
|
||||
import art.arcane.iris.engine.river.RiverEdgeId;
|
||||
import art.arcane.iris.engine.river.RiverNodeId;
|
||||
import art.arcane.iris.engine.river.RiverRouteState;
|
||||
import art.arcane.iris.engine.river.RiverSample;
|
||||
import art.arcane.iris.engine.river.RiverSection;
|
||||
import art.arcane.iris.engine.river.runtime.IrisRiverSurfaceSample;
|
||||
import art.arcane.iris.util.common.plugin.IrisService;
|
||||
import org.bukkit.World;
|
||||
import org.junit.Test;
|
||||
@@ -82,9 +89,7 @@ public class IrisTerrainSVCTest {
|
||||
IrisTerrainSVC service = new IrisTerrainSVC();
|
||||
AtomicInteger sinkCalls = new AtomicInteger();
|
||||
|
||||
boolean answered = service.sampleColumns(null, SMALL,
|
||||
(int blockX, int blockZ, int surfaceHeight, IrisSurfaceKind kind, String biomeKey)
|
||||
-> sinkCalls.incrementAndGet());
|
||||
boolean answered = service.sampleColumns(null, SMALL, sample -> sinkCalls.incrementAndGet());
|
||||
|
||||
assertFalse(answered);
|
||||
assertEquals(0, sinkCalls.get());
|
||||
@@ -97,4 +102,38 @@ public class IrisTerrainSVCTest {
|
||||
assertFalse(service.sampleColumns(null, null, null));
|
||||
assertFalse(service.sampleColumns(null, SMALL, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void riverRouteStatesMapToThePublicDiagnosticStates() {
|
||||
assertEquals(IrisRiverState.NONE, IrisTerrainSVC.riverState(
|
||||
new IrisRiverSurfaceSample(RiverSample.none(), 70D, 70D, 70D, false, false)
|
||||
));
|
||||
assertEquals(IrisRiverState.WET, IrisTerrainSVC.riverState(river(RiverRouteState.WET)));
|
||||
assertEquals(IrisRiverState.DRY, IrisTerrainSVC.riverState(river(RiverRouteState.DRY)));
|
||||
assertEquals(IrisRiverState.NONE, IrisTerrainSVC.riverState(river(RiverRouteState.SUPPRESSED)));
|
||||
}
|
||||
|
||||
private static IrisRiverSurfaceSample river(RiverRouteState state) {
|
||||
RiverSection section = switch (state) {
|
||||
case WET -> RiverSection.CHANNEL;
|
||||
case DRY -> RiverSection.DRY_CHANNEL;
|
||||
case SUPPRESSED -> RiverSection.NONE;
|
||||
};
|
||||
RiverSample sample = new RiverSample(
|
||||
true,
|
||||
state,
|
||||
section,
|
||||
0D,
|
||||
0.5D,
|
||||
1D,
|
||||
1,
|
||||
1,
|
||||
8D,
|
||||
4D,
|
||||
3D,
|
||||
false,
|
||||
RiverEdgeId.of(new RiverNodeId(0, 0), new RiverNodeId(1, 0))
|
||||
);
|
||||
return new IrisRiverSurfaceSample(sample, 70D, 60D, 63D, false, state == RiverRouteState.WET);
|
||||
}
|
||||
}
|
||||
|
||||
+88
@@ -2,6 +2,12 @@ package art.arcane.iris.core.service.terrain;
|
||||
|
||||
import art.arcane.iris.api.terrain.IrisSurfaceKind;
|
||||
import art.arcane.iris.engine.object.InferredType;
|
||||
import art.arcane.iris.engine.river.RiverEdgeId;
|
||||
import art.arcane.iris.engine.river.RiverNodeId;
|
||||
import art.arcane.iris.engine.river.RiverRouteState;
|
||||
import art.arcane.iris.engine.river.RiverSample;
|
||||
import art.arcane.iris.engine.river.RiverSection;
|
||||
import art.arcane.iris.engine.river.runtime.IrisRiverSurfaceSample;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
@@ -58,4 +64,86 @@ public class IrisSurfaceClassifierTest {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void activeRiverGeometryOverridesGenericOceanAndLandKinds() {
|
||||
assertEquals(IrisSurfaceKind.RIVER, IrisSurfaceClassifier.classify(
|
||||
60,
|
||||
63,
|
||||
InferredType.SEA,
|
||||
river(RiverRouteState.WET, RiverSection.CHANNEL)
|
||||
));
|
||||
assertEquals(IrisSurfaceKind.RIVER, IrisSurfaceClassifier.classify(
|
||||
60,
|
||||
63,
|
||||
InferredType.SEA,
|
||||
river(RiverRouteState.WET, RiverSection.MOUTH)
|
||||
));
|
||||
assertEquals(IrisSurfaceKind.RIVER_SHORE, IrisSurfaceClassifier.classify(
|
||||
64,
|
||||
63,
|
||||
InferredType.SHORE,
|
||||
river(RiverRouteState.WET, RiverSection.BANK)
|
||||
));
|
||||
assertEquals(IrisSurfaceKind.DRY_CHANNEL, IrisSurfaceClassifier.classify(
|
||||
60,
|
||||
60,
|
||||
InferredType.LAND,
|
||||
river(RiverRouteState.DRY, RiverSection.DRY_CHANNEL)
|
||||
));
|
||||
assertEquals(IrisSurfaceKind.LAND, IrisSurfaceClassifier.classify(
|
||||
60,
|
||||
60,
|
||||
InferredType.LAND,
|
||||
river(RiverRouteState.DRY, RiverSection.DRY_BANK)
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void voidClassificationWinsOverRiverGeometry() {
|
||||
for (RiverSection section : RiverSection.values()) {
|
||||
if (section == RiverSection.NONE) {
|
||||
continue;
|
||||
}
|
||||
RiverRouteState state = section == RiverSection.DRY_CHANNEL || section == RiverSection.DRY_BANK
|
||||
? RiverRouteState.DRY
|
||||
: RiverRouteState.WET;
|
||||
assertEquals(IrisSurfaceKind.VOID, IrisSurfaceClassifier.classify(
|
||||
0,
|
||||
63,
|
||||
InferredType.LAND,
|
||||
river(state, section)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void suppressedRoutesDoNotCreatePublicRiverSurfaceKinds() {
|
||||
assertEquals(IrisSurfaceKind.LAND, IrisSurfaceClassifier.classify(
|
||||
64,
|
||||
63,
|
||||
InferredType.LAND,
|
||||
river(RiverRouteState.SUPPRESSED, RiverSection.CHANNEL)
|
||||
));
|
||||
}
|
||||
|
||||
private static IrisRiverSurfaceSample river(RiverRouteState state, RiverSection section) {
|
||||
RiverSample sample = new RiverSample(
|
||||
true,
|
||||
state,
|
||||
section,
|
||||
0D,
|
||||
0.5D,
|
||||
1D,
|
||||
1,
|
||||
1,
|
||||
8D,
|
||||
4D,
|
||||
3D,
|
||||
false,
|
||||
RiverEdgeId.of(new RiverNodeId(0, 0), new RiverNodeId(1, 0))
|
||||
);
|
||||
double waterSurface = state == RiverRouteState.WET ? 63D : 60D;
|
||||
return new IrisRiverSurfaceSample(sample, 70D, 60D, waterSurface, false, state == RiverRouteState.WET);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user