mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-25 21:40:05 +00:00
f
This commit is contained in:
228 files changed
+5716
-3577
No files matched your search
@@ -5,6 +5,8 @@ import org.junit.Test;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
@@ -42,15 +44,46 @@ public class DiagnosticSeverityPolicyTest {
|
||||
assertLoggedWith("core/pregenerator/PregenMantleBackpressure.java", "Pregen mantle backpressure: ", "info");
|
||||
assertLoggedWith("core/pregenerator/PregenMantleBackpressure.java", "Pregen heap pressure: pausing generation", "info");
|
||||
assertLoggedWith("core/pregenerator/MantleHeapPressure.java", "Iris heap remained at", "info");
|
||||
assertLoggedWith("core/pregenerator/methods/AsyncPregenMethod.java", "is still pending after", "info");
|
||||
assertLoggedWith("core/pregenerator/methods/AsyncPregenMethod.java", "is still pending after", "debug");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void probesOnOptionalDependenciesAreNotWarnings() {
|
||||
public void generationTracingStaysAtDebugLevel() {
|
||||
assertLoggedWith("engine/mantle/components/MantleObjectComponent.java", "Regen object layer start:", "debug");
|
||||
assertLoggedWith("engine/mantle/components/MantleObjectComponent.java", "Goldendebug object attempt:", "debug");
|
||||
assertLoggedWith("engine/mantle/components/GoldenDebugObjectPlacer.java", "Goldendebug query:", "debug");
|
||||
assertLoggedWith("engine/mantle/components/IrisStructureComponent.java", "[StructTrace] ORIGIN", "debug");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void coreProductionDoesNotWriteDirectlyToProcessStreams() throws IOException {
|
||||
try (Stream<Path> files = Files.walk(Path.of("src/main/java"))) {
|
||||
List<Path> bypasses = files
|
||||
.filter(path -> path.toString().endsWith(".java"))
|
||||
.filter(path -> {
|
||||
try {
|
||||
String source = Files.readString(path);
|
||||
return source.contains("System.out")
|
||||
|| source.contains("System.err")
|
||||
|| source.contains(".printStackTrace();");
|
||||
} catch (IOException unreadable) {
|
||||
throw new IllegalStateException(unreadable);
|
||||
}
|
||||
})
|
||||
.toList();
|
||||
assertTrue(bypasses.toString(), bypasses.isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void probesOnOptionalDependenciesAreNotWarnings() throws IOException {
|
||||
assertLoggedWith("core/link/MultiverseCoreLink.java", "is not reachable; Iris world settings", "info");
|
||||
assertLoggedWith("core/link/MultiverseCoreLink.java", "Multiverse will record the live name", "info");
|
||||
assertLoggedWith("core/gui/GuiHost.java", "Unable to install the Iris desktop quit guard", "info");
|
||||
assertLoggedWith("util/common/misc/SlimJar.java", "Failed to inject the library loader", "info");
|
||||
String slimJar = read("util/common/misc/SlimJar.java");
|
||||
assertTrue(slimJar.contains("debug(plugin, \"Failed to inject the library loader"));
|
||||
assertTrue(slimJar.contains("if (DEBUG)"));
|
||||
assertTrue(slimJar.contains("plugin.getLogger().info(\"[DEBUG] \" + message)"));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,21 +13,23 @@ import static org.junit.Assert.assertThrows;
|
||||
|
||||
public class IrisSettingsDefaultsTest {
|
||||
@Test
|
||||
public void concurrencyIsNotSerializedIntoSettingsJson() {
|
||||
public void concurrencyIsNotSerializedIntoIrisJson() {
|
||||
String json = new Gson().toJson(new IrisSettings());
|
||||
|
||||
assertFalse("settings.json must not advertise an unconfigurable concurrency block",
|
||||
assertFalse("iris.json must not advertise an unconfigurable concurrency block",
|
||||
json.contains("\"concurrency\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void legacyConcurrencyBlockStillLoadsAndKeepsDerivedValues() {
|
||||
IrisSettings loaded = new Gson().fromJson(
|
||||
"{\"concurrency\":{\"parallelism\":8,\"ioParallelism\":4}}", IrisSettings.class);
|
||||
public void languageAndMetricsAreTheFirstGeneralControls() {
|
||||
String json = new Gson().toJson(new IrisSettings());
|
||||
|
||||
assertNotNull(loaded.getConcurrency());
|
||||
assertEquals(Math.max(2, Runtime.getRuntime().availableProcessors()),
|
||||
loaded.getConcurrency().getParallelism());
|
||||
int language = json.indexOf("\"language\"");
|
||||
int metrics = json.indexOf("\"metrics\"");
|
||||
int commandSounds = json.indexOf("\"commandSounds\"");
|
||||
assertTrue(language >= 0);
|
||||
assertTrue(language < metrics);
|
||||
assertTrue(metrics < commandSounds);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -34,8 +34,12 @@ public class ProgressBossBarToggleTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void worldCreationBossBarIsGatedBySettings() throws Exception {
|
||||
assertGated("core/tools/WorldCreationProgressReporter.java");
|
||||
public void worldCreationAlwaysUsesTheActionBarInsteadOfABossBar() throws Exception {
|
||||
String source = source("core/tools/WorldCreationProgressReporter.java");
|
||||
|
||||
assertFalse(source.contains("isProgressBossBar()"));
|
||||
assertFalse(source.contains("Bukkit.createBossBar("));
|
||||
assertTrue(source.contains("WORLD_CREATE_LIFECYCLE_ACTION"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -4,9 +4,14 @@ import art.arcane.iris.spi.IrisLogging;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.objectweb.asm.ClassReader;
|
||||
import org.objectweb.asm.ClassVisitor;
|
||||
import org.objectweb.asm.MethodVisitor;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
@@ -45,6 +50,48 @@ public class ServerConfiguratorStartupRestartTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void immediateRestartCapabilityIsOptional() throws ReflectiveOperationException {
|
||||
RestartCapableApi.restarted = false;
|
||||
|
||||
assertTrue(ServerConfigurator.invokeImmediateRestartIfSupported(RestartCapableApi.class));
|
||||
assertTrue(RestartCapableApi.restarted);
|
||||
assertFalse(ServerConfigurator.invokeImmediateRestartIfSupported(ShutdownOnlyApi.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void productionBytecodeDoesNotLinkBukkitRestartDirectly() throws Exception {
|
||||
AtomicBoolean directRestartInvocation = new AtomicBoolean(false);
|
||||
ClassReader reader = new ClassReader(ServerConfigurator.class.getName());
|
||||
reader.accept(new ClassVisitor(Opcodes.ASM9) {
|
||||
@Override
|
||||
public MethodVisitor visitMethod(
|
||||
int access,
|
||||
String name,
|
||||
String descriptor,
|
||||
String signature,
|
||||
String[] exceptions
|
||||
) {
|
||||
return new MethodVisitor(Opcodes.ASM9) {
|
||||
@Override
|
||||
public void visitMethodInsn(
|
||||
int opcode,
|
||||
String owner,
|
||||
String methodName,
|
||||
String methodDescriptor,
|
||||
boolean isInterface
|
||||
) {
|
||||
if ("org/bukkit/Bukkit".equals(owner) && "restart".equals(methodName)) {
|
||||
directRestartInvocation.set(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
|
||||
|
||||
assertFalse(directRestartInvocation.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void startupRestartPathsPromoteTheValidationStateAndBypassTickQueues() throws Exception {
|
||||
String source = Files.readString(Path.of(
|
||||
@@ -62,12 +109,24 @@ public class ServerConfiguratorStartupRestartTest {
|
||||
int validationRestart = configure.indexOf("requireDatapackRestart();", restartResult);
|
||||
assertTrue(restartResult >= 0);
|
||||
assertTrue(validationRestart > restartResult);
|
||||
assertTrue(startupRestart.indexOf("Bukkit.restart();")
|
||||
assertTrue(startupRestart.indexOf("invokeImmediateRestartIfSupported(Bukkit.class)")
|
||||
< startupRestart.indexOf("Bukkit.shutdown();"));
|
||||
assertFalse(startupRestart.contains("Bukkit.restart"));
|
||||
assertFalse(startupRestart.contains("J.s("));
|
||||
assertFalse(startupRestart.contains("dispatchCommand"));
|
||||
}
|
||||
|
||||
public static final class RestartCapableApi {
|
||||
private static boolean restarted;
|
||||
|
||||
public static void restart() {
|
||||
restarted = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class ShutdownOnlyApi {
|
||||
}
|
||||
|
||||
private static String section(String source, String startMarker, String endMarker) {
|
||||
int start = source.indexOf(startMarker);
|
||||
int end = source.indexOf(endMarker, start);
|
||||
|
||||
@@ -43,7 +43,7 @@ public class SettingsHotloadWatchTest {
|
||||
public void setUp() throws Exception {
|
||||
previousSettings = IrisSettings.settings;
|
||||
dataFolder = temporaryFolder.newFolder("iris-hotload");
|
||||
settingsFile = new File(dataFolder, "settings.json");
|
||||
settingsFile = new File(dataFolder, "iris.json");
|
||||
overrideFolder = new File(dataFolder, "languages/overrides");
|
||||
Files.createDirectories(overrideFolder.toPath());
|
||||
String settings = settings("en_US");
|
||||
|
||||
@@ -25,6 +25,13 @@ public class VisionRenderTypeTest {
|
||||
assertEquals("iris.desktop.vision.mode.river", DesktopUiMessages.VISION_MODE_RIVER.id());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void teleportCoordinatesFloorAcrossTheNegativeOrigin() {
|
||||
assertEquals(-1, VisionGUI.floorWorldCoordinate(-0.01D));
|
||||
assertEquals(0, VisionGUI.floorWorldCoordinate(0.99D));
|
||||
assertEquals(1, VisionGUI.floorWorldCoordinate(1D));
|
||||
}
|
||||
|
||||
private static void assertTrueCatalogMember(MessageKey key) {
|
||||
assertEquals(key.id(), 1, DesktopUiMessages.keys().stream()
|
||||
.filter(candidate -> candidate.id().equals(key.id()))
|
||||
|
||||
@@ -84,11 +84,11 @@ public class WorldLifecycleSelectionTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void persistentCreateFallsBackToBukkitBackendOnFoliaWithoutRuntime() {
|
||||
WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshot.forTesting(ServerFamily.FOLIA, true, false, false));
|
||||
public void persistentCreateFailsClosedOnFoliaWithoutRuntime() {
|
||||
WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshot.forTesting(ServerFamily.FOLIA, false, false, false));
|
||||
WorldLifecycleRequest request = new WorldLifecycleRequest("persistent", NamespacedKey.minecraft("persistent"), World.Environment.NORMAL, null, null, null, true, false, 1337L, false, false, WorldLifecycleCaller.CREATE);
|
||||
|
||||
assertEquals("bukkit_public", service.selectCreateBackend(request).backendName());
|
||||
assertThrows(IllegalStateException.class, () -> service.selectCreateBackend(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -10,6 +10,8 @@ import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
@@ -57,6 +59,40 @@ public class WorldLifecycleUnloadAsyncTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void paperLikeManualUnloadDetachesBeforeOffThreadChunkDrain() throws Exception {
|
||||
String source = Files.readString(Path.of(
|
||||
"src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleSupport.java"))
|
||||
.replace("\r\n", "\n");
|
||||
int unloadStart = source.indexOf("private static CompletableFuture<Boolean> unloadWorldWithoutAsyncApi(");
|
||||
int unloadEnd = source.indexOf("static boolean announceManualWorldUnload", unloadStart);
|
||||
String unloadMethod = source.substring(unloadStart, unloadEnd);
|
||||
int capabilityFallback = unloadMethod.indexOf(
|
||||
"capabilities.minecraftServer() == null || capabilities.removeLevelMethod() == null");
|
||||
int bukkitUnload = unloadMethod.indexOf("Bukkit.unloadWorld(world, save)", capabilityFallback);
|
||||
int detach = unloadMethod.indexOf("detachServerLevelAsync(capabilities, serverLevel, world)");
|
||||
int drain = unloadMethod.indexOf("drainChunkTasksAsync(world, serverLevel)");
|
||||
int close = unloadMethod.indexOf("closeServerLevelAsync(world, serverLevel)");
|
||||
int closeStart = source.indexOf("private static CompletableFuture<Void> closeServerLevelAsync(");
|
||||
int closeEnd = source.indexOf("private static CompletableFuture<Void> drainChunkTasksAsync(", closeStart);
|
||||
String closeMethod = source.substring(closeStart, closeEnd);
|
||||
int drainStart = closeEnd;
|
||||
int drainEnd = source.indexOf("private static void removeWorldFromCraftServerMap", drainStart);
|
||||
String drainMethod = source.substring(drainStart, drainEnd);
|
||||
|
||||
assertTrue(capabilityFallback >= 0);
|
||||
assertTrue(bukkitUnload > capabilityFallback);
|
||||
assertTrue(detach > bukkitUnload);
|
||||
assertTrue(drain > detach);
|
||||
assertTrue(close > drain);
|
||||
assertTrue(drainMethod.contains("return J.afut(() ->"));
|
||||
assertTrue(drainMethod.contains("\"moonrise$getChunkTaskScheduler\""));
|
||||
assertTrue(drainMethod.contains("\"halt\""));
|
||||
assertTrue(closeMethod.contains("return runGlobalAsync(closeTask)"));
|
||||
assertFalse(closeMethod.contains("J.afut("));
|
||||
assertFalse(closeMethod.contains("J.runRegion("));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reflectedAsyncUnloadWaitsForTrueCallback() throws Exception {
|
||||
CallbackServer server = new CallbackServer();
|
||||
@@ -101,6 +137,49 @@ public class WorldLifecycleUnloadAsyncTest {
|
||||
assertFalse(result.join());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reflectedAsyncUnloadAcceptsCanvasResultCallback() throws Exception {
|
||||
CanvasCallbackServer server = new CanvasCallbackServer();
|
||||
Method unloadMethod = CanvasCallbackServer.class.getMethod(
|
||||
"unloadWorldAsync",
|
||||
World.class,
|
||||
boolean.class,
|
||||
Consumer.class
|
||||
);
|
||||
|
||||
CompletableFuture<Boolean> result = WorldLifecycleSupport.invokeAsyncUnload(
|
||||
server,
|
||||
unloadMethod,
|
||||
mock(World.class),
|
||||
true
|
||||
);
|
||||
|
||||
assertFalse(result.isDone());
|
||||
server.complete(CanvasUnloadResult.SUCCESS);
|
||||
assertTrue(result.join());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reflectedAsyncUnloadMapsCanvasFailureResultToFalse() throws Exception {
|
||||
CanvasCallbackServer server = new CanvasCallbackServer();
|
||||
Method unloadMethod = CanvasCallbackServer.class.getMethod(
|
||||
"unloadWorldAsync",
|
||||
World.class,
|
||||
boolean.class,
|
||||
Consumer.class
|
||||
);
|
||||
|
||||
CompletableFuture<Boolean> result = WorldLifecycleSupport.invokeAsyncUnload(
|
||||
server,
|
||||
unloadMethod,
|
||||
mock(World.class),
|
||||
false
|
||||
);
|
||||
|
||||
server.complete(CanvasUnloadResult.FAILURE);
|
||||
assertFalse(result.join());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reflectedAsyncUnloadPropagatesInvocationFailure() throws Exception {
|
||||
FailingServer server = new FailingServer();
|
||||
@@ -232,6 +311,27 @@ public class WorldLifecycleUnloadAsyncTest {
|
||||
}
|
||||
}
|
||||
|
||||
public static final class CanvasCallbackServer {
|
||||
private Consumer<CanvasUnloadResult> callback;
|
||||
|
||||
public void unloadWorldAsync(World world, boolean save, Consumer<CanvasUnloadResult> callback) {
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
private void complete(CanvasUnloadResult result) {
|
||||
callback.accept(result);
|
||||
}
|
||||
}
|
||||
|
||||
public enum CanvasUnloadResult {
|
||||
SUCCESS,
|
||||
FAILURE;
|
||||
|
||||
public boolean isSuccess() {
|
||||
return this == SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ControlledBackend implements WorldLifecycleBackend {
|
||||
private final String name;
|
||||
private final CompletableFuture<Boolean> unloadResult = new CompletableFuture<>();
|
||||
|
||||
@@ -79,12 +79,27 @@ public class PackRiverValidatorTest {
|
||||
"minimumSourcesPerTile": 2,
|
||||
"sinkSearchReaches": 8,
|
||||
"routingBasinCells": 7,
|
||||
"routingDeviationScaleCells": 7,
|
||||
"routingDeviationStrengthCells": 33,
|
||||
"routingPlateauHeight": 0,
|
||||
"flowAlignmentWeight": 1025,
|
||||
"confluenceWeight": -1,
|
||||
"branchSoftCap": 9,
|
||||
"branchChildShrinkFactor": 2,
|
||||
"routingStyle": {"zoom": 0}
|
||||
},
|
||||
"terrain": {
|
||||
"channelWidth": {"min": 40, "max": 12},
|
||||
"depth": {}
|
||||
"depth": {},
|
||||
"tunnelWidthMultiplier": {"min": 0.5, "max": 9},
|
||||
"tunnelMouthBlend": 17,
|
||||
"tunnelFloorVariation": 9,
|
||||
"tunnelRoofVariation": 17,
|
||||
"tunnelFloorStyle": {"zoom": 0},
|
||||
"tunnelRoofStyle": {"zoom": 0}
|
||||
},
|
||||
"caves": {
|
||||
"parentBiomeInheritance": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -96,10 +111,24 @@ public class PackRiverValidatorTest {
|
||||
assertContains(result.errors(), "rivers.topology.minimumSourcesPerTile must not exceed tileCells squared.");
|
||||
assertContains(result.errors(), "rivers.topology.sinkSearchReaches must be at most 7");
|
||||
assertContains(result.errors(), "rivers.topology.routingBasinCells must be at least 8");
|
||||
assertContains(result.errors(), "rivers.topology.routingDeviationScaleCells must be at least 8");
|
||||
assertContains(result.errors(), "rivers.topology.routingDeviationStrengthCells must be at most 32");
|
||||
assertContains(result.errors(), "rivers.topology.routingPlateauHeight must be at least 1");
|
||||
assertContains(result.errors(), "rivers.topology.flowAlignmentWeight must be at most 1024");
|
||||
assertContains(result.errors(), "rivers.topology.confluenceWeight must be at least 0");
|
||||
assertContains(result.errors(), "rivers.topology.branchSoftCap must be at most 8");
|
||||
assertContains(result.errors(), "rivers.topology.branchChildShrinkFactor must be at most 1");
|
||||
assertContains(result.errors(), "rivers.topology.routingStyle.zoom must be at least");
|
||||
assertContains(result.errors(), "rivers.terrain.channelWidth.min must not exceed");
|
||||
assertContains(result.errors(), "rivers.terrain.depth must set min and max explicitly.");
|
||||
assertContains(result.errors(), "rivers.terrain.tunnelWidthMultiplier.min must be at least 1");
|
||||
assertContains(result.errors(), "rivers.terrain.tunnelWidthMultiplier.max must be at most 8");
|
||||
assertContains(result.errors(), "rivers.terrain.tunnelMouthBlend must be at most 16");
|
||||
assertContains(result.errors(), "rivers.terrain.tunnelFloorVariation must be at most 8");
|
||||
assertContains(result.errors(), "rivers.terrain.tunnelRoofVariation must be at most 16");
|
||||
assertContains(result.errors(), "rivers.terrain.tunnelFloorStyle.zoom must be at least");
|
||||
assertContains(result.errors(), "rivers.terrain.tunnelRoofStyle.zoom must be at least");
|
||||
assertContains(result.errors(), "rivers.caves.parentBiomeInheritance must be at most 1");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -177,6 +206,27 @@ public class PackRiverValidatorTest {
|
||||
assertContains(result.errors(), "rivers.terrain.maxDepth");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsTunnelFootprintsAboveDerivedHydrologyBudget() throws Exception {
|
||||
File pack = pack("""
|
||||
{
|
||||
"regions": ["region"],
|
||||
"rivers": {
|
||||
"enabled": true,
|
||||
"terrain": {
|
||||
"maxChannelWidth": 2048,
|
||||
"tunnelMouthBlend": 16
|
||||
}
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
PackRiverValidator.Validation result = validate(pack);
|
||||
|
||||
assertContains(result.errors(), "exceeds the safe derived hydrology budget");
|
||||
assertContains(result.errors(), "columns per generated chunk");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsMissingBiomeReferencesAndWarnsAboutInferredRoles() throws Exception {
|
||||
File pack = pack("""
|
||||
@@ -199,6 +249,44 @@ public class PackRiverValidatorTest {
|
||||
assertContains(result.warnings(), "inferred river role SHORE");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void netherRiverBiomesKeepNetherDerivativesWithoutOverworldRoleWarnings() throws Exception {
|
||||
File pack = pack("""
|
||||
{
|
||||
"environment": "NETHER",
|
||||
"regions": ["region"],
|
||||
"rivers": {
|
||||
"enabled": true,
|
||||
"biomes": {
|
||||
"channel": ["biome"],
|
||||
"bank": ["biome"]
|
||||
}
|
||||
}
|
||||
}
|
||||
""");
|
||||
write(pack, "biomes/biome.json", """
|
||||
{
|
||||
"name": "Nether River",
|
||||
"derivative": "minecraft:basalt_deltas",
|
||||
"vanillaDerivative": "minecraft:basalt_deltas"
|
||||
}
|
||||
""");
|
||||
write(pack, "regions/region.json", """
|
||||
{
|
||||
"landBiomes": ["biome"],
|
||||
"riverOverride": {
|
||||
"channelBiomes": ["biome"],
|
||||
"bankBiomes": ["biome"]
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
PackRiverValidator.Validation result = validate(pack);
|
||||
|
||||
assertFalse(result.errors().toString(), contains(result.errors(), "biome"));
|
||||
assertFalse(result.warnings().toString(), contains(result.warnings(), "inferred river role"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validatesRegionAndBiomeOverridesWhenRiversAreEnabled() throws Exception {
|
||||
File pack = pack("""
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core.protocol;
|
||||
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.spi.protocol.IrisProtocol;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class IrisCursorRequestServiceTest {
|
||||
private static final Executor DIRECT = Runnable::run;
|
||||
|
||||
@Test
|
||||
public void burstCoalescesToLatestRequestPerSessionAndOneDrain() {
|
||||
IrisSessionRegistry registry = new IrisSessionRegistry();
|
||||
registerReady(registry, "s1");
|
||||
QueuedExecutor executor = new QueuedExecutor();
|
||||
IrisCursorRequestService service = new IrisCursorRequestService(
|
||||
sessionId -> {
|
||||
throw new AssertionError("queued work must not run early");
|
||||
},
|
||||
registry,
|
||||
executor,
|
||||
8);
|
||||
|
||||
for (int index = 0; index < 10; index++) {
|
||||
service.handle("s1", index, -index);
|
||||
}
|
||||
|
||||
assertEquals(1, service.pendingSize());
|
||||
assertEquals(9L, service.coalescedCount());
|
||||
assertEquals(1, executor.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saturationDropsOldestSessionsButKeepsQueueBounded() {
|
||||
IrisSessionRegistry registry = new IrisSessionRegistry();
|
||||
QueuedExecutor executor = new QueuedExecutor();
|
||||
IrisCursorRequestService service = new IrisCursorRequestService(sessionId -> null, registry, executor, 2);
|
||||
|
||||
service.handle("s1", 1, 1);
|
||||
service.handle("s2", 2, 2);
|
||||
service.handle("s3", 3, 3);
|
||||
|
||||
assertEquals(2, service.pendingSize());
|
||||
assertEquals(1L, service.droppedSaturatedCount());
|
||||
assertEquals(1, executor.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void directDrainResolvesAndSendsCursorResponse() {
|
||||
IrisSessionRegistry registry = new IrisSessionRegistry();
|
||||
CountingTransport transport = registerReady(registry, "s1");
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisDimension dimension = mock(IrisDimension.class);
|
||||
when(engine.isClosed()).thenReturn(false);
|
||||
when(engine.getSurfaceBiome(4, 8)).thenReturn(null);
|
||||
when(engine.getRegion(4, 8)).thenReturn(null);
|
||||
when(engine.getCaveBiome(4, 8)).thenReturn(null);
|
||||
when(engine.getMinHeight()).thenReturn(-64);
|
||||
when(engine.getHeight(4, 8)).thenReturn(80);
|
||||
when(engine.getDimension()).thenReturn(dimension);
|
||||
when(dimension.getLoadKey()).thenReturn("overworld");
|
||||
IrisCursorRequestService service = new IrisCursorRequestService(sessionId -> engine, registry, DIRECT, 8);
|
||||
|
||||
service.handle("s1", 4, 8);
|
||||
|
||||
assertEquals(1L, service.resolvedCount());
|
||||
assertEquals(1, transport.sent.get());
|
||||
assertEquals(0, service.pendingSize());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void blankSessionIsRejectedWithoutSchedulingWork() {
|
||||
IrisSessionRegistry registry = new IrisSessionRegistry();
|
||||
QueuedExecutor executor = new QueuedExecutor();
|
||||
IrisCursorRequestService service = new IrisCursorRequestService(sessionId -> null, registry, executor, 8);
|
||||
|
||||
service.handle(null, 0, 0);
|
||||
service.handle("", 0, 0);
|
||||
|
||||
assertEquals(2L, service.droppedNoSessionCount());
|
||||
assertEquals(0, service.pendingSize());
|
||||
assertEquals(0, executor.size());
|
||||
}
|
||||
|
||||
private static CountingTransport registerReady(IrisSessionRegistry registry, String sessionId) {
|
||||
CountingTransport transport = new CountingTransport();
|
||||
IrisSession session = new IrisSession(sessionId, transport);
|
||||
session.markReady(IrisProtocol.PROTOCOL_VERSION, IrisProtocol.CAPABILITY_CURSOR);
|
||||
registry.register(session);
|
||||
return transport;
|
||||
}
|
||||
|
||||
private static final class CountingTransport implements IrisServerTransport {
|
||||
private final AtomicInteger sent = new AtomicInteger(0);
|
||||
|
||||
@Override
|
||||
public void sendToClient(String sessionId, byte[] frame) {
|
||||
sent.incrementAndGet();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class QueuedExecutor implements Executor {
|
||||
private final ArrayDeque<Runnable> tasks = new ArrayDeque<>();
|
||||
|
||||
@Override
|
||||
public void execute(Runnable command) {
|
||||
tasks.addLast(command);
|
||||
}
|
||||
|
||||
private int size() {
|
||||
return tasks.size();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -350,6 +350,28 @@ public class IrisProtocolServerTest {
|
||||
assertEquals(1L, server.visionTileForwardedCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void visionTileOutsideWorldBoundsIsRejectedBeforeRendering() {
|
||||
RecordingTransport transport = new RecordingTransport();
|
||||
IrisSessionRegistry registry = new IrisSessionRegistry();
|
||||
IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true);
|
||||
IrisSession session = new IrisSession("s1", transport);
|
||||
registry.register(session);
|
||||
server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.ClientHello(
|
||||
IrisProtocol.PROTOCOL_VERSION,
|
||||
IrisProtocol.CAPABILITY_VISION)));
|
||||
RecordingVisionHandler handler = new RecordingVisionHandler();
|
||||
server.setVisionTileHandler(handler);
|
||||
|
||||
server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.VisionTileRequest(Integer.MAX_VALUE, 0, 0)));
|
||||
server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.VisionTileRequest(0, Integer.MIN_VALUE, 8)));
|
||||
server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.VisionTileRequest(0, 0, Integer.MAX_VALUE)));
|
||||
|
||||
assertEquals(1, handler.count);
|
||||
assertEquals(2L, server.visionOutOfBoundsCount());
|
||||
assertEquals(1L, server.visionTileForwardedCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void visionTileRequestBudgetDropsExcessWithinWindow() {
|
||||
RecordingTransport transport = new RecordingTransport();
|
||||
|
||||
@@ -21,6 +21,9 @@ package art.arcane.iris.core.protocol;
|
||||
import art.arcane.iris.spi.protocol.IrisProtocol;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
@@ -50,6 +53,94 @@ public class IrisVisionRequestServiceTest {
|
||||
assertEquals(maxPending, service.pendingSize());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void burstUsesAtMostTwoDrainsAndDrainsEveryRetainedRequest() {
|
||||
IrisSessionRegistry registry = new IrisSessionRegistry();
|
||||
registerReady(registry, "s1", IrisProtocol.CAPABILITY_VISION);
|
||||
EngineResolver resolver = sessionId -> null;
|
||||
QueuedExecutor executor = new QueuedExecutor();
|
||||
int maxPending = 4;
|
||||
IrisVisionRequestService service = new IrisVisionRequestService(resolver, registry, executor, maxPending);
|
||||
|
||||
for (int index = 0; index < 10; index++) {
|
||||
service.handle("s1", index, 0, 0);
|
||||
}
|
||||
|
||||
assertEquals(2, executor.size());
|
||||
assertEquals(maxPending, service.pendingSize());
|
||||
executor.runAll();
|
||||
assertEquals(0, service.pendingSize());
|
||||
assertEquals(maxPending, service.droppedNoEngineCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void duplicateTileRequestsCoalesceWithoutConsumingCapacity() {
|
||||
IrisSessionRegistry registry = new IrisSessionRegistry();
|
||||
registerReady(registry, "s1", IrisProtocol.CAPABILITY_VISION);
|
||||
IrisVisionRequestService service = new IrisVisionRequestService(sessionId -> null, registry, DISABLED, 8);
|
||||
|
||||
service.handle("s1", 4, -7, 2);
|
||||
service.handle("s1", 4, -7, 2);
|
||||
service.handle("s1", 4, -7, 2);
|
||||
|
||||
assertEquals(1, service.pendingSize());
|
||||
assertEquals(0L, service.droppedSaturatedCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void drainRotatesAcrossSessionsInsteadOfServingOneBurstFirst() {
|
||||
IrisSessionRegistry registry = new IrisSessionRegistry();
|
||||
registerReady(registry, "s1", IrisProtocol.CAPABILITY_VISION);
|
||||
registerReady(registry, "s2", IrisProtocol.CAPABILITY_VISION);
|
||||
ArrayList<String> resolutionOrder = new ArrayList<>();
|
||||
EngineResolver resolver = sessionId -> {
|
||||
resolutionOrder.add(sessionId);
|
||||
return null;
|
||||
};
|
||||
QueuedExecutor executor = new QueuedExecutor();
|
||||
IrisVisionRequestService service = new IrisVisionRequestService(resolver, registry, executor, 8);
|
||||
|
||||
service.handle("s1", 0, 0, 0);
|
||||
service.handle("s1", 1, 0, 0);
|
||||
service.handle("s2", 0, 0, 0);
|
||||
service.handle("s2", 1, 0, 0);
|
||||
executor.runAll();
|
||||
|
||||
assertEquals(List.of("s1", "s2", "s1", "s2"), resolutionOrder);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saturatedSessionCannotExcludeAnotherSession() {
|
||||
IrisSessionRegistry registry = new IrisSessionRegistry();
|
||||
registerReady(registry, "noisy", IrisProtocol.CAPABILITY_VISION);
|
||||
registerReady(registry, "other", IrisProtocol.CAPABILITY_VISION);
|
||||
IrisVisionRequestService service = new IrisVisionRequestService(sessionId -> null, registry, DISABLED, 4);
|
||||
|
||||
for (int tile = 0; tile < 20; tile++) {
|
||||
service.handle("noisy", tile, 0, 0);
|
||||
}
|
||||
service.handle("other", 0, 0, 0);
|
||||
|
||||
assertEquals(4, service.pendingSize());
|
||||
service.clearSession("noisy");
|
||||
assertEquals(1, service.pendingSize());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void blankSessionIsRejectedWithoutSchedulingWork() {
|
||||
IrisSessionRegistry registry = new IrisSessionRegistry();
|
||||
QueuedExecutor executor = new QueuedExecutor();
|
||||
IrisVisionRequestService service = new IrisVisionRequestService(sessionId -> null, registry, executor, 8);
|
||||
|
||||
service.handle(null, 0, 0, 0);
|
||||
service.handle("", 0, 0, 0);
|
||||
service.handle(" ", 0, 0, 0);
|
||||
|
||||
assertEquals(3L, service.droppedNoSessionCount());
|
||||
assertEquals(0, service.pendingSize());
|
||||
assertEquals(0, executor.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nullEngineDropsAndCounts() {
|
||||
IrisSessionRegistry registry = new IrisSessionRegistry();
|
||||
@@ -157,4 +248,27 @@ public class IrisVisionRequestServiceTest {
|
||||
sent.incrementAndGet();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class QueuedExecutor implements Executor {
|
||||
private final ArrayDeque<Runnable> tasks = new ArrayDeque<>();
|
||||
|
||||
@Override
|
||||
public void execute(Runnable command) {
|
||||
tasks.addLast(command);
|
||||
}
|
||||
|
||||
private int size() {
|
||||
return tasks.size();
|
||||
}
|
||||
|
||||
private void runNext() {
|
||||
tasks.removeFirst().run();
|
||||
}
|
||||
|
||||
private void runAll() {
|
||||
while (!tasks.isEmpty()) {
|
||||
runNext();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,12 +36,12 @@ public class StudioEntitySpawningTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generalStudioUsesAPlayerModeEligibleForNaturalSpawning() throws IOException {
|
||||
public void standardStudioDefaultsToSpectatorMode() throws IOException {
|
||||
String source = Files.readString(Path.of(
|
||||
"src/main/java/art/arcane/iris/core/project/IrisProject.java")).replace("\r\n", "\n");
|
||||
"src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java")).replace("\r\n", "\n");
|
||||
|
||||
assertFalse(source.contains("GameMode.SPECTATOR"));
|
||||
assertTrue(source.contains("GameMode.CREATIVE"));
|
||||
assertTrue(source.contains("request.openKind().teleportThroughStandardEntry()"));
|
||||
assertTrue(source.contains("GameMode.SPECTATOR"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+12
-116
@@ -1,19 +1,19 @@
|
||||
package art.arcane.iris.core.runtime;
|
||||
|
||||
import art.arcane.iris.core.ServerConfigurator;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
|
||||
public class StudioOpenCoordinatorCloseSequenceTest {
|
||||
@Test
|
||||
@@ -104,126 +104,22 @@ public class StudioOpenCoordinatorCloseSequenceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pendingEntryLoadDefersCleanupUntilSettlement() {
|
||||
CompletableFuture<Void> entryLoad = new CompletableFuture<>();
|
||||
AtomicInteger cleanups = new AtomicInteger(0);
|
||||
public void closeFailureAfterUnloadBeginsRequestsTerminalRestart() {
|
||||
try (MockedStatic<ServerConfigurator> configurator = mockStatic(ServerConfigurator.class)) {
|
||||
StudioOpenCoordinator.requestRestartAfterPartialClose("studio-world", true);
|
||||
|
||||
CompletableFuture<Void> deferred = StudioOpenCoordinator.deferCleanupUntilEntrySettlement(
|
||||
entryLoad,
|
||||
() -> {
|
||||
cleanups.incrementAndGet();
|
||||
return CompletableFuture.completedFuture(null);
|
||||
});
|
||||
|
||||
assertEquals(0, cleanups.get());
|
||||
assertFalse(deferred.isDone());
|
||||
entryLoad.complete(null);
|
||||
deferred.join();
|
||||
assertEquals(1, cleanups.get());
|
||||
entryLoad.complete(null);
|
||||
assertEquals(1, cleanups.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onlyAnUnsettledEntryLoadRequiresDeferredCleanup() {
|
||||
CompletableFuture<Void> pending = new CompletableFuture<>();
|
||||
CompletableFuture<Void> failed = CompletableFuture.failedFuture(
|
||||
new IllegalStateException("entry failed"));
|
||||
|
||||
assertTrue(StudioOpenCoordinator.requiresDeferredEntryCleanup(pending));
|
||||
assertFalse(StudioOpenCoordinator.requiresDeferredEntryCleanup(
|
||||
CompletableFuture.completedFuture(null)));
|
||||
assertFalse(StudioOpenCoordinator.requiresDeferredEntryCleanup(failed));
|
||||
assertFalse(StudioOpenCoordinator.requiresDeferredEntryCleanup(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void retryRemainsFencedUntilDeferredCleanupCompletes() {
|
||||
StudioOpenCoordinator.EntryLoadRegistry registry =
|
||||
new StudioOpenCoordinator.EntryLoadRegistry();
|
||||
CompletableFuture<Void> entryLoad = new CompletableFuture<>();
|
||||
CompletableFuture<Void> cleanup = new CompletableFuture<>();
|
||||
registry.register("iris-entry-timeout", entryLoad);
|
||||
CompletableFuture<Void> deferred = StudioOpenCoordinator.deferCleanupUntilEntrySettlement(
|
||||
entryLoad,
|
||||
() -> cleanup);
|
||||
registry.releaseAfterSuccessfulCompletion(
|
||||
"iris-entry-timeout",
|
||||
entryLoad,
|
||||
deferred);
|
||||
|
||||
entryLoad.complete(null);
|
||||
assertFalse(deferred.isDone());
|
||||
assertTrue(registry.isFenced("iris-entry-timeout"));
|
||||
try {
|
||||
registry.rejectNewOpen();
|
||||
fail("Expected the retry to be rejected");
|
||||
} catch (IllegalStateException exception) {
|
||||
assertTrue(exception.getMessage().contains("iris-entry-timeout"));
|
||||
configurator.verify(() -> ServerConfigurator.restart(
|
||||
"Studio close failed after world unload began for \"studio-world\"."));
|
||||
}
|
||||
|
||||
cleanup.complete(null);
|
||||
deferred.join();
|
||||
assertFalse(registry.isFenced("iris-entry-timeout"));
|
||||
registry.rejectNewOpen();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exceptionalEntrySettlementStillTriggersCleanupOnce() {
|
||||
CompletableFuture<Void> entryLoad = new CompletableFuture<>();
|
||||
AtomicInteger cleanups = new AtomicInteger(0);
|
||||
public void closeFailureBeforeUnloadDoesNotRequestRestart() {
|
||||
try (MockedStatic<ServerConfigurator> configurator = mockStatic(ServerConfigurator.class)) {
|
||||
StudioOpenCoordinator.requestRestartAfterPartialClose("studio-world", false);
|
||||
|
||||
CompletableFuture<Void> deferred = StudioOpenCoordinator.deferCleanupUntilEntrySettlement(
|
||||
entryLoad,
|
||||
() -> {
|
||||
cleanups.incrementAndGet();
|
||||
return CompletableFuture.completedFuture(null);
|
||||
});
|
||||
|
||||
entryLoad.completeExceptionally(new IllegalStateException("entry failed"));
|
||||
deferred.join();
|
||||
assertEquals(1, cleanups.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nonSettlingEntryLoadNeverStartsCleanup() {
|
||||
CompletableFuture<Void> entryLoad = new CompletableFuture<>();
|
||||
AtomicInteger cleanups = new AtomicInteger(0);
|
||||
|
||||
CompletableFuture<Void> deferred = StudioOpenCoordinator.deferCleanupUntilEntrySettlement(
|
||||
entryLoad,
|
||||
() -> {
|
||||
cleanups.incrementAndGet();
|
||||
return CompletableFuture.completedFuture(null);
|
||||
});
|
||||
|
||||
assertFalse(deferred.isDone());
|
||||
assertEquals(0, cleanups.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deferredCleanupFailureIsPreserved() {
|
||||
StudioOpenCoordinator.EntryLoadRegistry registry =
|
||||
new StudioOpenCoordinator.EntryLoadRegistry();
|
||||
CompletableFuture<Void> entryLoad = new CompletableFuture<>();
|
||||
IllegalStateException cleanupFailure = new IllegalStateException("cleanup failed");
|
||||
registry.register("iris-cleanup-failure", entryLoad);
|
||||
CompletableFuture<Void> deferred = StudioOpenCoordinator.deferCleanupUntilEntrySettlement(
|
||||
entryLoad,
|
||||
() -> CompletableFuture.failedFuture(cleanupFailure));
|
||||
registry.releaseAfterSuccessfulCompletion(
|
||||
"iris-cleanup-failure",
|
||||
entryLoad,
|
||||
deferred);
|
||||
|
||||
entryLoad.complete(null);
|
||||
try {
|
||||
deferred.join();
|
||||
fail("Expected deferred cleanup failure");
|
||||
} catch (CompletionException exception) {
|
||||
assertSame(cleanupFailure, exception.getCause());
|
||||
configurator.verifyNoInteractions();
|
||||
}
|
||||
assertTrue(registry.isFenced("iris-cleanup-failure"));
|
||||
}
|
||||
|
||||
private static CompletableFuture<Void> phase(List<String> phases, String phase) {
|
||||
|
||||
+51
-65
@@ -27,7 +27,7 @@ public class StudioOpenCoordinatorOpenKindTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bothStudioKindsReuseOnlyAnUnchangedLoadedRuntime() {
|
||||
public void allStudioKindsReuseOnlyAnUnchangedLoadedRuntime() {
|
||||
for (StudioOpenCoordinator.StudioOpenKind kind : StudioOpenCoordinator.StudioOpenKind.values()) {
|
||||
assertEquals(
|
||||
IrisCreator.DatapackPreparation.REUSE_LOADED_RUNTIME_IF_READY,
|
||||
@@ -35,6 +35,15 @@ public class StudioOpenCoordinatorOpenKindTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void objectStudioOwnsWorkspaceWithoutUsingTheStandardEntry() {
|
||||
StudioOpenCoordinator.StudioOpenKind kind = StudioOpenCoordinator.StudioOpenKind.OBJECT;
|
||||
|
||||
assertTrue(kind.openWorkspace());
|
||||
assertFalse(kind.teleportThroughStandardEntry());
|
||||
assertTrue(kind.prepareGeneratorState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void jigsawStudioLeavesWorkspaceClosedAndUsesItsDestinationTeleport() {
|
||||
StudioOpenCoordinator.StudioOpenKind kind = StudioOpenCoordinator.StudioOpenKind.JIGSAW;
|
||||
@@ -62,22 +71,6 @@ public class StudioOpenCoordinatorOpenKindTest {
|
||||
assertEquals(StudioOpenCoordinator.StudioOpenKind.JIGSAW, request.openKind());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onlyAStandardPlayerOpenLoadsTheEntry() {
|
||||
assertTrue(StudioOpenCoordinator.requiresLoadedEntry(request(
|
||||
StudioOpenCoordinator.StudioOpenKind.STANDARD,
|
||||
"Magic_Psycho")));
|
||||
assertFalse(StudioOpenCoordinator.requiresLoadedEntry(request(
|
||||
StudioOpenCoordinator.StudioOpenKind.STANDARD,
|
||||
null)));
|
||||
assertFalse(StudioOpenCoordinator.requiresLoadedEntry(request(
|
||||
StudioOpenCoordinator.StudioOpenKind.STANDARD,
|
||||
" ")));
|
||||
assertFalse(StudioOpenCoordinator.requiresLoadedEntry(request(
|
||||
StudioOpenCoordinator.StudioOpenKind.JIGSAW,
|
||||
"Magic_Psycho")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void activeStudioTeleportIsSerializedAndBoundedBeforeNativeDelegation() throws Exception {
|
||||
String coordinator = Files.readString(Path.of(
|
||||
@@ -103,42 +96,57 @@ public class StudioOpenCoordinatorOpenKindTest {
|
||||
int nativeClaim = coordinatorMethod.indexOf(
|
||||
"activeAdmission.compareAndSet(true, false)");
|
||||
int nativeDelegation = coordinatorMethod.indexOf(
|
||||
"WorldRuntimeControlService.get().teleport(player, entry)");
|
||||
"WorldRuntimeControlService.get().teleportInMode(player, entry, GameMode.SPECTATOR)");
|
||||
|
||||
assertTrue(transitionAdmission >= 0);
|
||||
assertTrue(projectCapture > transitionAdmission);
|
||||
assertTrue(publicDeadline > projectCapture);
|
||||
assertTrue(admissionClose > publicDeadline);
|
||||
assertTrue(serviceMethod.contains("deadlineNanos"));
|
||||
assertTrue(coordinatorMethod.contains("beforeStudioTeleportDeadline("));
|
||||
assertTrue(coordinatorMethod.contains("CompletableFuture<T> bounded = new CompletableFuture<>()"));
|
||||
assertTrue(coordinatorMethod.contains(
|
||||
"CompletableFuture.delayedExecutor(remainingNanos, TimeUnit.NANOSECONDS)"));
|
||||
assertFalse(coordinatorMethod.contains("stage.orTimeout("));
|
||||
assertTrue(coordinatorMethod.contains("EntryChunkResolution entryResolution"));
|
||||
assertFalse(coordinatorMethod.contains("addPluginChunkTicket("));
|
||||
assertFalse(coordinatorMethod.contains("removePluginChunkTicket("));
|
||||
"WorldRuntimeControlService.get().resolveEntryAnchor(world, provider)"));
|
||||
assertTrue(coordinatorMethod.contains(
|
||||
"project.getActiveOpenKind() == StudioOpenKind.STANDARD"));
|
||||
assertFalse(coordinatorMethod.contains("requestChunkAsync("));
|
||||
assertFalse(coordinatorMethod.contains("getHighestBlockYAt("));
|
||||
assertTrue(nativeClaim >= 0);
|
||||
assertTrue(nativeDelegation > nativeClaim);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void entryLoadUsesOnlyTheUrgentExactChunkRequest() throws Exception {
|
||||
public void standardEntryDelegatesWithoutAnExplicitChunkOrSurfaceLookup() throws Exception {
|
||||
String source = Files.readString(Path.of(
|
||||
"src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java"))
|
||||
.replace("\r\n", "\n");
|
||||
int loadStart = source.indexOf("private EntryChunkResolution loadEntryChunk(");
|
||||
int loadEnd = source.indexOf("private void settleEntryUseAfterOperation(", loadStart);
|
||||
String load = source.substring(loadStart, loadEnd);
|
||||
|
||||
assertTrue(load.contains("requestChunkAsync("));
|
||||
assertTrue(load.contains("true,\n true"));
|
||||
assertTrue(load.contains("J.isOwnedByCurrentRegion(world, chunkX, chunkZ)"));
|
||||
assertTrue(load.contains("J.runRegion(world, chunkX, chunkZ"));
|
||||
assertTrue(load.contains("WorldRuntimeControlService.findTopSafeStudioLocation(world, entryAnchor)"));
|
||||
assertTrue(source.contains("Location entryLocation = entryAnchor;"));
|
||||
assertFalse(source.contains("prepareStudioEntryChunks("));
|
||||
assertFalse(source.contains("findStudioEntryLocation"));
|
||||
assertFalse(source.contains("EntryChunkResolution"));
|
||||
assertFalse(source.contains("resolveSafeEntry(world, entryAnchor)"));
|
||||
assertFalse(source.contains("addPluginChunkTicket("));
|
||||
assertFalse(source.contains("removePluginChunkTicket("));
|
||||
assertFalse(source.contains("getHighestBlockYAt("));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void standardEntryBindsSpectatorToTheNativeTeleport() throws Exception {
|
||||
String source = Files.readString(Path.of(
|
||||
"src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java"))
|
||||
.replace("\r\n", "\n");
|
||||
String runtime = Files.readString(Path.of(
|
||||
"src/main/java/art/arcane/iris/core/runtime/WorldRuntimeControlService.java"))
|
||||
.replace("\r\n", "\n");
|
||||
int mode = runtime.indexOf("modeRestore.apply(gameMode)");
|
||||
int teleport = runtime.indexOf("teleporter.teleport(player, location)", mode);
|
||||
|
||||
assertTrue(source.contains("WorldRuntimeControlService.get().teleportInMode("));
|
||||
int openKind = source.indexOf("request.project().setActiveOpenKind(request.openKind())");
|
||||
int provider = source.indexOf("request.project().setActiveProvider(provider)", openKind);
|
||||
assertTrue(openKind >= 0);
|
||||
assertTrue(provider > openKind);
|
||||
assertTrue(source.contains("project.setActiveOpenKind(null)"));
|
||||
assertTrue(mode >= 0);
|
||||
assertTrue(teleport > mode);
|
||||
assertTrue(runtime.contains("modeRestore.restore()"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -153,9 +161,6 @@ public class StudioOpenCoordinatorOpenKindTest {
|
||||
"prepare_generator",
|
||||
"resolve_entry_anchor",
|
||||
"prepare_structure_rings",
|
||||
"prepare_entry_chunks",
|
||||
"load_entry_chunk",
|
||||
"resolve_safe_entry",
|
||||
"teleport_standard_entry",
|
||||
"finalize_open")) {
|
||||
int current = source.indexOf("\"" + phase + "\"", previous + 1);
|
||||
@@ -166,18 +171,16 @@ public class StudioOpenCoordinatorOpenKindTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void structureStateActivatesBeforeTheRealEntryChunkLoads() throws Exception {
|
||||
public void structureStateCompletesBeforeImmediateNativeTeleport() throws Exception {
|
||||
String source = Files.readString(Path.of(
|
||||
"src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java")).replace("\r\n", "\n");
|
||||
int completionCall = source.indexOf("endStudioEntryBootstrap(world, provider)");
|
||||
int entryReady = source.indexOf(
|
||||
"entryResolution.chunk().get(STUDIO_ENTRY_LOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS)", completionCall);
|
||||
int teleport = source.indexOf(
|
||||
"WorldRuntimeControlService.get().teleport(player, safeEntry)", entryReady);
|
||||
"WorldRuntimeControlService.get().teleportInMode(", completionCall);
|
||||
int finalizeOpen = source.indexOf(
|
||||
"updateStage(request, \"finalize_open\", 1.00D)", teleport);
|
||||
int futureComplete = source.indexOf(
|
||||
"future.complete(new StudioOpenResult(world, safeEntry))", finalizeOpen);
|
||||
"future.complete(new StudioOpenResult(world, entryLocation))", finalizeOpen);
|
||||
int methodStart = source.indexOf(
|
||||
"private void endStudioEntryBootstrap(World world, PlatformChunkGenerator provider)");
|
||||
int methodEnd = source.indexOf("private void abandonStudioEntryBootstrap", methodStart);
|
||||
@@ -188,8 +191,8 @@ public class StudioOpenCoordinatorOpenKindTest {
|
||||
int gateRelease = method.indexOf("bukkitGenerator::endStudioEntryBootstrap");
|
||||
|
||||
assertTrue(completionCall >= 0);
|
||||
assertTrue(entryReady > completionCall);
|
||||
assertTrue(teleport > entryReady);
|
||||
assertTrue(teleport > completionCall);
|
||||
assertFalse(source.contains("preparedEntryChunks"));
|
||||
assertTrue(finalizeOpen > teleport);
|
||||
assertTrue(futureComplete > finalizeOpen);
|
||||
assertTrue(scheduled >= 0);
|
||||
@@ -208,7 +211,7 @@ public class StudioOpenCoordinatorOpenKindTest {
|
||||
"src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java")).replace("\r\n", "\n");
|
||||
int methodStart = source.indexOf(
|
||||
"private void abandonStudioEntryBootstrap(World world, Throwable failure)");
|
||||
int methodEnd = source.indexOf("private void deferFailedOpenCleanup", methodStart);
|
||||
int methodEnd = source.indexOf("private CompletableFuture<Void> cleanupFailedOpen", methodStart);
|
||||
String method = source.substring(methodStart, methodEnd);
|
||||
|
||||
assertTrue(method.contains("if (J.isPrimaryThread())"));
|
||||
@@ -247,7 +250,7 @@ public class StudioOpenCoordinatorOpenKindTest {
|
||||
"src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java")).replace("\r\n", "\n");
|
||||
int finalizerCall = source.indexOf("runOpenFinalizer(request.onDone(), world);");
|
||||
int futureCompletion = source.indexOf(
|
||||
"future.complete(new StudioOpenResult(world, safeEntry))", finalizerCall);
|
||||
"future.complete(new StudioOpenResult(world, entryLocation))", finalizerCall);
|
||||
int methodStart = source.indexOf(
|
||||
"private void runOpenFinalizer(Consumer<World> finalizer, World world)");
|
||||
int methodEnd = source.indexOf("private long elapsedMillis", methodStart);
|
||||
@@ -260,21 +263,4 @@ public class StudioOpenCoordinatorOpenKindTest {
|
||||
assertTrue(method.contains(
|
||||
"completion.get(STUDIO_STRUCTURE_ACTIVATION_TIMEOUT_SECONDS, TimeUnit.SECONDS)"));
|
||||
}
|
||||
|
||||
private static StudioOpenCoordinator.StudioOpenRequest request(
|
||||
StudioOpenCoordinator.StudioOpenKind openKind,
|
||||
String playerName
|
||||
) {
|
||||
return new StudioOpenCoordinator.StudioOpenRequest(
|
||||
"overworld",
|
||||
null,
|
||||
null,
|
||||
1337L,
|
||||
"iris-test",
|
||||
playerName,
|
||||
openKind,
|
||||
false,
|
||||
null,
|
||||
null);
|
||||
}
|
||||
}
|
||||
+10
-18
@@ -11,10 +11,10 @@ import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class StudioOpenCoordinatorSpawnStuckRegressionTest {
|
||||
@Test
|
||||
public void waitForSafeEntryRetryLoopIsRemoved() {
|
||||
public void legacySafeEntryRetryLoopIsRemoved() {
|
||||
boolean found = Arrays.stream(StudioOpenCoordinator.class.getDeclaredMethods())
|
||||
.anyMatch(m -> m.getName().equals("waitForSafeEntry"));
|
||||
assertFalse("waitForSafeEntry retry loop must be removed — it burns up to 120s on ocean columns", found);
|
||||
assertFalse("waitForSafeEntry retry loop must remain removed", found);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -32,31 +32,23 @@ public class StudioOpenCoordinatorSpawnStuckRegressionTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void entryChunkLoadUsesTheUrgentAsyncRequestWithoutRetentionTickets() throws Exception {
|
||||
public void entryTeleportDoesNotIssueASecondChunkRequest() throws Exception {
|
||||
String source = Files.readString(Path.of(
|
||||
"src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java")).replace("\r\n", "\n");
|
||||
int asyncLoad = source.indexOf("requested = WorldRuntimeControlService.get().requestChunkAsync(");
|
||||
int urgentFlag = source.indexOf("true);", asyncLoad);
|
||||
|
||||
assertTrue(asyncLoad >= 0);
|
||||
assertTrue(urgentFlag > asyncLoad);
|
||||
assertFalse(source.contains("addPluginChunkTicket"));
|
||||
assertFalse(source.contains("removePluginChunkTicket"));
|
||||
assertTrue(source.contains("WorldRuntimeControlService.get().teleportInMode("));
|
||||
assertFalse(source.contains("prepareStudioEntryChunks("));
|
||||
assertFalse(source.contains("requestChunkAsync("));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void foliaUsesTheSameNonBlockingAsyncEntryPath() throws Exception {
|
||||
public void foliaEntryPathNeverReadsTerrainOnARegionThread() throws Exception {
|
||||
String source = Files.readString(Path.of(
|
||||
"src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java")).replace("\r\n", "\n");
|
||||
int loadStart = source.indexOf("private EntryChunkResolution loadEntryChunk(");
|
||||
int loadEnd = source.indexOf("private void settleEntryUseAfterOperation(", loadStart);
|
||||
String load = source.substring(loadStart, loadEnd);
|
||||
|
||||
assertTrue(load.contains("requestChunkAsync("));
|
||||
assertTrue(load.contains("J.isOwnedByCurrentRegion(world, chunkX, chunkZ)"));
|
||||
assertTrue(load.contains("J.runRegion(world, chunkX, chunkZ"));
|
||||
assertTrue(load.contains("findTopSafeStudioLocation(world, entryAnchor)"));
|
||||
assertTrue(source.contains("resolveEntryAnchor(world, provider)"));
|
||||
assertFalse(source.contains("findStudioEntryLocation"));
|
||||
assertFalse(source.contains("getHighestBlockYAt("));
|
||||
assertFalse(source.contains("resolveSafeEntry(world, entryAnchor)"));
|
||||
assertFalse(load.contains("J.isFolia()"));
|
||||
}
|
||||
}
|
||||
-129
@@ -7,7 +7,6 @@ import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.Levelled;
|
||||
import org.bukkit.block.data.Waterlogged;
|
||||
import org.bukkit.util.BoundingBox;
|
||||
import org.bukkit.util.VoxelShape;
|
||||
@@ -78,35 +77,6 @@ public class WorldRuntimeControlServiceSafeEntryTest {
|
||||
assertEquals(63, result.getBlockY());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void studioPrefersNearbyDryGroundWithinTheOwnedChunk() {
|
||||
World world = loadedWorld(0, 0);
|
||||
Block water = block(Material.WATER, true, true);
|
||||
Block stone = block(Material.STONE, false, false, FULL_BLOCK);
|
||||
Block air = block(Material.AIR, false, true);
|
||||
doReturn(62).when(world).getHighestBlockYAt(anyInt(), anyInt(), eq(HeightMap.MOTION_BLOCKING_NO_LEAVES));
|
||||
doAnswer(invocation -> {
|
||||
int x = invocation.getArgument(0);
|
||||
int y = invocation.getArgument(1);
|
||||
int z = invocation.getArgument(2);
|
||||
if (x < 0 || x > 15 || z < 0 || z > 15) {
|
||||
throw new AssertionError("Safe-entry search crossed its Folia-owned source chunk");
|
||||
}
|
||||
if (x == 1 && z == 0) {
|
||||
return y == 62 ? stone : air;
|
||||
}
|
||||
return water;
|
||||
}).when(world).getBlockAt(anyInt(), anyInt(), anyInt());
|
||||
|
||||
Location source = new Location(world, 0.5D, 63D, 0.5D);
|
||||
Location result = WorldRuntimeControlService.findTopSafeStudioLocation(world, source);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(1, result.getBlockX());
|
||||
assertEquals(63, result.getBlockY());
|
||||
assertEquals(0, result.getBlockZ());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsFluidHazardousAndCollisionBlockedCandidates() {
|
||||
World world = loadedWorld(0, 0);
|
||||
@@ -199,98 +169,6 @@ public class WorldRuntimeControlServiceSafeEntryTest {
|
||||
assertEquals(-1, lowestReadY.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void studioFallsBackAboveWaterSurfaceAfterExhaustingDryCandidates() {
|
||||
World world = loadedWorld(0, 0);
|
||||
Block water = waterBlock(0);
|
||||
Block air = block(Material.AIR, false, true);
|
||||
doReturn(62).when(world).getHighestBlockYAt(anyInt(), anyInt(), eq(HeightMap.MOTION_BLOCKING_NO_LEAVES));
|
||||
doAnswer(invocation -> {
|
||||
int y = invocation.getArgument(1);
|
||||
return y <= 62 ? water : air;
|
||||
}).when(world).getBlockAt(anyInt(), anyInt(), anyInt());
|
||||
|
||||
Location source = new Location(world, 0.5D, 63D, 0.5D);
|
||||
Location result = WorldRuntimeControlService.findTopSafeStudioLocation(world, source);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(0, result.getBlockX());
|
||||
assertEquals(63, result.getBlockY());
|
||||
assertEquals(0, result.getBlockZ());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void studioDoesNotFallBackAboveLavaSurface() {
|
||||
World world = loadedWorld(0, 0);
|
||||
Block lava = block(Material.LAVA, true, true);
|
||||
Block air = block(Material.AIR, false, true);
|
||||
doReturn(62).when(world).getHighestBlockYAt(anyInt(), anyInt(), eq(HeightMap.MOTION_BLOCKING_NO_LEAVES));
|
||||
doAnswer(invocation -> {
|
||||
int y = invocation.getArgument(1);
|
||||
return y == 62 ? lava : air;
|
||||
}).when(world).getBlockAt(anyInt(), anyInt(), anyInt());
|
||||
|
||||
Location source = new Location(world, 0.5D, 63D, 0.5D);
|
||||
Location result = WorldRuntimeControlService.findTopSafeStudioLocation(world, source);
|
||||
|
||||
assertNull(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void studioRejectsWaterSurfaceWithoutClearHeadroom() {
|
||||
World world = loadedWorld(0, 0);
|
||||
Block water = waterBlock(0);
|
||||
Block air = block(Material.AIR, false, true);
|
||||
Block stone = block(Material.STONE, false, false, FULL_BLOCK);
|
||||
doReturn(62).when(world).getHighestBlockYAt(anyInt(), anyInt(), eq(HeightMap.MOTION_BLOCKING_NO_LEAVES));
|
||||
doAnswer(invocation -> {
|
||||
int y = invocation.getArgument(1);
|
||||
if (y <= 62) {
|
||||
return water;
|
||||
}
|
||||
return y == 64 ? stone : air;
|
||||
}).when(world).getBlockAt(anyInt(), anyInt(), anyInt());
|
||||
|
||||
Location source = new Location(world, 0.5D, 63D, 0.5D);
|
||||
Location result = WorldRuntimeControlService.findTopSafeStudioLocation(world, source);
|
||||
|
||||
assertNull(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void studioRejectsFlowingWaterSurface() {
|
||||
World world = loadedWorld(0, 0);
|
||||
Block flowingWater = waterBlock(1);
|
||||
Block air = block(Material.AIR, false, true);
|
||||
doReturn(62).when(world).getHighestBlockYAt(anyInt(), anyInt(), eq(HeightMap.MOTION_BLOCKING_NO_LEAVES));
|
||||
doAnswer(invocation -> {
|
||||
int y = invocation.getArgument(1);
|
||||
return y <= 62 ? flowingWater : air;
|
||||
}).when(world).getBlockAt(anyInt(), anyInt(), anyInt());
|
||||
|
||||
Location source = new Location(world, 0.5D, 63D, 0.5D);
|
||||
Location result = WorldRuntimeControlService.findTopSafeStudioLocation(world, source);
|
||||
|
||||
assertNull(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void studioRejectsUnsupportedShallowWaterSurface() {
|
||||
World world = loadedWorld(0, 0);
|
||||
Block water = waterBlock(0);
|
||||
Block air = block(Material.AIR, false, true);
|
||||
doReturn(62).when(world).getHighestBlockYAt(anyInt(), anyInt(), eq(HeightMap.MOTION_BLOCKING_NO_LEAVES));
|
||||
doAnswer(invocation -> {
|
||||
int y = invocation.getArgument(1);
|
||||
return y == 62 ? water : air;
|
||||
}).when(world).getBlockAt(anyInt(), anyInt(), anyInt());
|
||||
|
||||
Location source = new Location(world, 0.5D, 63D, 0.5D);
|
||||
Location result = WorldRuntimeControlService.findTopSafeStudioLocation(world, source);
|
||||
|
||||
assertNull(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsNullWithoutReadingAnUnloadedChunk() {
|
||||
World world = loadedWorld(0, 0);
|
||||
@@ -331,11 +209,4 @@ public class WorldRuntimeControlServiceSafeEntryTest {
|
||||
return block;
|
||||
}
|
||||
|
||||
private static Block waterBlock(int level) {
|
||||
Block block = block(Material.WATER, true, true);
|
||||
Levelled blockData = mock(Levelled.class);
|
||||
doReturn(level).when(blockData).getLevel();
|
||||
doReturn(blockData).when(block).getBlockData();
|
||||
return block;
|
||||
}
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
package art.arcane.iris.core.runtime;
|
||||
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import org.bukkit.GameMode;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.junit.Test;
|
||||
import org.mockito.InOrder;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class WorldRuntimeControlServiceTeleportTest {
|
||||
@Test
|
||||
public void failedModeTeleportRestoresThePreviousGameMode() {
|
||||
Player player = mock(Player.class);
|
||||
Location destination = mock(Location.class);
|
||||
CompletableFuture<Boolean> nativeTeleport = new CompletableFuture<>();
|
||||
when(player.getGameMode()).thenReturn(GameMode.SURVIVAL);
|
||||
|
||||
try (MockedStatic<J> scheduling = immediateEntityScheduling()) {
|
||||
CompletableFuture<Boolean> result = WorldRuntimeControlService.scheduleTeleport(
|
||||
player,
|
||||
destination,
|
||||
GameMode.SPECTATOR,
|
||||
(target, location) -> nativeTeleport);
|
||||
nativeTeleport.complete(false);
|
||||
|
||||
assertFalse(result.join());
|
||||
InOrder gameModes = inOrder(player);
|
||||
gameModes.verify(player).setGameMode(GameMode.SPECTATOR);
|
||||
gameModes.verify(player).setGameMode(GameMode.SURVIVAL);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exceptionalModeTeleportRestoresThePreviousGameMode() {
|
||||
Player player = mock(Player.class);
|
||||
Location destination = mock(Location.class);
|
||||
CompletableFuture<Boolean> nativeTeleport = new CompletableFuture<>();
|
||||
when(player.getGameMode()).thenReturn(GameMode.ADVENTURE);
|
||||
|
||||
try (MockedStatic<J> scheduling = immediateEntityScheduling()) {
|
||||
CompletableFuture<Boolean> result = WorldRuntimeControlService.scheduleTeleport(
|
||||
player,
|
||||
destination,
|
||||
GameMode.SPECTATOR,
|
||||
(target, location) -> nativeTeleport);
|
||||
nativeTeleport.completeExceptionally(new IllegalStateException("teleport failed"));
|
||||
|
||||
assertThrows(CompletionException.class, result::join);
|
||||
InOrder gameModes = inOrder(player);
|
||||
gameModes.verify(player).setGameMode(GameMode.SPECTATOR);
|
||||
gameModes.verify(player).setGameMode(GameMode.ADVENTURE);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void timedOutModeTeleportCancelsNativeWorkAndRestoresThePreviousGameMode() {
|
||||
Player player = mock(Player.class);
|
||||
Location destination = mock(Location.class);
|
||||
CompletableFuture<Boolean> nativeTeleport = new CompletableFuture<>();
|
||||
when(player.getGameMode()).thenReturn(GameMode.CREATIVE);
|
||||
|
||||
try (MockedStatic<J> scheduling = immediateEntityScheduling()) {
|
||||
CompletableFuture<Boolean> result = WorldRuntimeControlService.scheduleTeleport(
|
||||
player,
|
||||
destination,
|
||||
GameMode.SPECTATOR,
|
||||
(target, location) -> nativeTeleport);
|
||||
assertTrue(result.completeExceptionally(new TimeoutException("timed out")));
|
||||
|
||||
assertThrows(CompletionException.class, result::join);
|
||||
assertTrue(nativeTeleport.isCancelled());
|
||||
InOrder gameModes = inOrder(player);
|
||||
gameModes.verify(player).setGameMode(GameMode.SPECTATOR);
|
||||
gameModes.verify(player).setGameMode(GameMode.CREATIVE);
|
||||
}
|
||||
}
|
||||
|
||||
private static MockedStatic<J> immediateEntityScheduling() {
|
||||
MockedStatic<J> scheduling = mockStatic(J.class);
|
||||
scheduling.when(() -> J.runEntity(any(Player.class), any(Runnable.class))).thenAnswer(invocation -> {
|
||||
invocation.getArgument(1, Runnable.class).run();
|
||||
return true;
|
||||
});
|
||||
return scheduling;
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -10,13 +10,15 @@ import static org.junit.Assert.assertFalse;
|
||||
|
||||
public class StudioSVCWorldPackFeedbackContractTest {
|
||||
@Test
|
||||
public void persistentPackCopyUsesLocalizedStyledOperatorFeedback() throws Exception {
|
||||
public void persistentPackCopyUsesLifecycleProgressWithoutLegacySnapshotFeedback() throws Exception {
|
||||
String source = Files.readString(Path.of(
|
||||
"src/main/java/art/arcane/iris/core/service/StudioSVC.java"
|
||||
)).replace("\r\n", "\n");
|
||||
|
||||
assertEquals(1, occurrences(source, "STUDIO_S_V_C_PACK_COPY_REQUIRES_ASYNC_THREAD"));
|
||||
assertEquals(2, occurrences(source, "STUDIO_S_V_C_PACK_INSTALL_FAILED"));
|
||||
assertFalse(source.contains("STUDIO_S_V_C_INSTALLING_PACKAGE"));
|
||||
assertFalse(source.contains("Publishing snapshot"));
|
||||
assertFalse(source.contains("sender.sendMessage(\"Iris refused to copy a pack"));
|
||||
assertFalse(source.contains("sender.sendMessage(\"Failed to install studio pack"));
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
|
||||
public class IrisCreatorTeleportTest {
|
||||
@Test
|
||||
@@ -50,7 +52,7 @@ public class IrisCreatorTeleportTest {
|
||||
Chunk chunk = mock(Chunk.class);
|
||||
WorldRuntimeControlService runtimeControl = mock(WorldRuntimeControlService.class);
|
||||
Location anchor = new Location(world, 32.5D, 80D, -15.5D);
|
||||
Location safeEntry = new Location(world, 32.5D, 94D, -15.5D);
|
||||
Location safeEntry = new Location(world, 33.5D, 91D, -14.5D);
|
||||
doReturn(anchor).when(runtimeControl).resolveEntryAnchor(world);
|
||||
doReturn(CompletableFuture.completedFuture(chunk))
|
||||
.when(runtimeControl).requestChunkAsync(world, 2, -1, true);
|
||||
@@ -67,6 +69,7 @@ public class IrisCreatorTeleportTest {
|
||||
order.verify(runtimeControl).requestChunkAsync(world, 2, -1, true);
|
||||
order.verify(runtimeControl).resolveSafeEntry(world, anchor);
|
||||
order.verify(runtimeControl).teleport(player, safeEntry);
|
||||
verifyNoMoreInteractions(runtimeControl);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -89,7 +92,22 @@ public class IrisCreatorTeleportTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void teleportSenderToCreatedWorld_failsWhenSafeEntryCannotResolve() {
|
||||
public void teleportSenderToCreatedWorld_failsWhenEntryAnchorCannotResolve() {
|
||||
Player player = mock(Player.class);
|
||||
World world = mock(World.class);
|
||||
WorldRuntimeControlService runtimeControl = mock(WorldRuntimeControlService.class);
|
||||
doReturn("irisworld").when(world).getName();
|
||||
doReturn(null).when(runtimeControl).resolveEntryAnchor(world);
|
||||
|
||||
CompletableFuture<Boolean> result = IrisCreator.teleportSenderToCreatedWorld(player, world, runtimeControl);
|
||||
|
||||
assertThrows(CompletionException.class, result::join);
|
||||
verify(runtimeControl).resolveEntryAnchor(world);
|
||||
verifyNoMoreInteractions(runtimeControl);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void teleportSenderToCreatedWorld_failsWhenNoSafeEntryExists() {
|
||||
Player player = mock(Player.class);
|
||||
World world = mock(World.class);
|
||||
WorldRuntimeControlService runtimeControl = mock(WorldRuntimeControlService.class);
|
||||
@@ -104,6 +122,11 @@ public class IrisCreatorTeleportTest {
|
||||
CompletableFuture<Boolean> result = IrisCreator.teleportSenderToCreatedWorld(player, world, runtimeControl);
|
||||
|
||||
assertThrows(CompletionException.class, result::join);
|
||||
InOrder order = inOrder(runtimeControl);
|
||||
order.verify(runtimeControl).resolveEntryAnchor(world);
|
||||
order.verify(runtimeControl).requestChunkAsync(world, 0, 0, true);
|
||||
order.verify(runtimeControl).resolveSafeEntry(world, anchor);
|
||||
verifyNoMoreInteractions(runtimeControl);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+26
-6
@@ -59,18 +59,38 @@ public class WorldCreationProgressReporterTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bukkitHudMutationIsMarshalledOffTheAsyncReporterThread() throws Exception {
|
||||
public void terminalPlayerMessagesHonorStrictPlaceholderContracts() {
|
||||
String ready = C.stripColor(WorldCreationProgressReporter.terminalPlayerMessage(
|
||||
false,
|
||||
1.0D,
|
||||
"World ready",
|
||||
"",
|
||||
1_000L
|
||||
));
|
||||
String failed = C.stripColor(WorldCreationProgressReporter.terminalPlayerMessage(
|
||||
true,
|
||||
0.98D,
|
||||
"Entering world",
|
||||
"",
|
||||
1_000L
|
||||
));
|
||||
|
||||
assertTrue(ready.contains("100% | World ready"));
|
||||
assertTrue(failed.contains("FAILED | Entering world"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void playerProgressUsesTheEntityScheduledActionBarWithoutABossBar() throws Exception {
|
||||
String source = Files.readString(Path.of(
|
||||
"src/main/java/art/arcane/iris/core/tools/WorldCreationProgressReporter.java"
|
||||
)).replace("\r\n", "\n");
|
||||
|
||||
assertEquals(1, occurrences(source, "Bukkit.createBossBar("));
|
||||
assertTrue(source.contains("J.sfut(reporter::initializePlayerHud)"));
|
||||
assertFalse(source.contains("Bukkit.createBossBar("));
|
||||
assertFalse(source.contains("WORLD_CREATE_BOSSBAR_"));
|
||||
assertFalse(source.contains("isProgressBossBar()"));
|
||||
assertTrue(source.contains("RuntimeProgressMessages.WORLD_CREATE_LIFECYCLE_ACTION"));
|
||||
assertTrue(source.contains("J.runEntity(sender.player(), guardedRender)"));
|
||||
assertTrue(source.contains("J.runEntity(sender.player(), render)"));
|
||||
assertTrue(source.contains("J.runEntity(sender.player(), cleanup, 60, cleanup)"));
|
||||
assertFalse(source.contains("HudSlotRequest"));
|
||||
assertFalse(source.contains("hudLanes()"));
|
||||
}
|
||||
|
||||
private static int occurrences(String value, String match) {
|
||||
|
||||
@@ -1,18 +1,51 @@
|
||||
package art.arcane.iris.engine;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.LivingEntity;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class IrisWorldManagerMarkerTest {
|
||||
@Test
|
||||
public void cancelledMantleCleanupLeavesNoDelayedExecutorEntry() {
|
||||
ScheduledThreadPoolExecutor executor = IrisWorldManager.createCleanupExecutor("test");
|
||||
try {
|
||||
ScheduledFuture<?> future = executor.schedule(() -> {
|
||||
}, 1, TimeUnit.DAYS);
|
||||
|
||||
assertEquals(1, executor.getQueue().size());
|
||||
assertTrue(future.cancel(false));
|
||||
assertEquals(0, executor.getQueue().size());
|
||||
} finally {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mantleCleanupDoesNotExecuteDelayedTasksAfterShutdown() throws InterruptedException {
|
||||
ScheduledThreadPoolExecutor executor = IrisWorldManager.createCleanupExecutor("test");
|
||||
AtomicBoolean executed = new AtomicBoolean();
|
||||
executor.schedule(() -> executed.set(true), 1, TimeUnit.DAYS);
|
||||
|
||||
executor.shutdown();
|
||||
|
||||
assertTrue(executor.awaitTermination(1, TimeUnit.SECONDS));
|
||||
assertFalse(executed.get());
|
||||
assertEquals(0, executor.getQueue().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void worldBlockHeightIsTranslatedToMantleHeight() {
|
||||
assertEquals(64, WorldBlockDropRouter.toMantleY(0, -64));
|
||||
@@ -28,28 +61,13 @@ public class IrisWorldManagerMarkerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void completedEntityTasksAreAccepted() {
|
||||
assertTrue(WorldEntitySpawner.awaitEntityTasks(new CountDownLatch(0), 0, TimeUnit.MILLISECONDS));
|
||||
}
|
||||
public void entitySaturationUsesTheCompleteLivingEntitySnapshot() {
|
||||
World world = mock(World.class);
|
||||
LivingEntity first = mock(LivingEntity.class);
|
||||
LivingEntity second = mock(LivingEntity.class);
|
||||
when(world.getLivingEntities()).thenReturn(List.of(first, second));
|
||||
|
||||
@Test
|
||||
public void incompleteEntityTasksAreRejected() {
|
||||
assertFalse(WorldEntitySpawner.awaitEntityTasks(new CountDownLatch(1), 0, TimeUnit.MILLISECONDS));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void interruptedEntityWaitPreservesInterruptStatus() throws Exception {
|
||||
AtomicBoolean preserved = new AtomicBoolean();
|
||||
Thread thread = new Thread(() -> {
|
||||
Thread.currentThread().interrupt();
|
||||
boolean completed = WorldEntitySpawner.awaitEntityTasks(new CountDownLatch(1), 1, TimeUnit.SECONDS);
|
||||
preserved.set(!completed && Thread.currentThread().isInterrupted());
|
||||
});
|
||||
|
||||
thread.start();
|
||||
thread.join();
|
||||
|
||||
assertTrue(preserved.get());
|
||||
assertEquals(2, WorldEntitySpawner.livingEntityCount(world));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+34
@@ -2,16 +2,21 @@ package art.arcane.iris.engine.mantle.components;
|
||||
|
||||
import art.arcane.iris.engine.river.cave.CavePosition;
|
||||
import art.arcane.iris.engine.river.cave.CaveVoxel;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveAction;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.mantle.runtime.TectonicPlate;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.matter.MatterSlice;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class MantleRiverCaveVoxelViewTest {
|
||||
@@ -34,4 +39,33 @@ public class MantleRiverCaveVoxelViewTest {
|
||||
assertEquals(CaveVoxel.SOLID, view.voxelAt(terrain));
|
||||
assertFalse(view.isOpenToSurface(terrain));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void publishedRiverActionsDoNotReplaceTheUnderlyingPlanningBaseline() {
|
||||
Mantle<Matter> mantle = mock(Mantle.class);
|
||||
TectonicPlate<Matter> plate = mock(TectonicPlate.class);
|
||||
MantleChunk<Matter> chunk = mock(MantleChunk.class);
|
||||
Matter matter = mock(Matter.class);
|
||||
MatterSlice<RiverCaveHydrology> hydrologySlice = mock(MatterSlice.class);
|
||||
KMap<Long, TectonicPlate<Matter>> regions = new KMap<>();
|
||||
regions.put(Mantle.key(0, 0), plate);
|
||||
CavePosition position = new CavePosition(0, 20, 0);
|
||||
when(mantle.getLoadedRegions()).thenReturn(regions);
|
||||
when(plate.get(0, 0)).thenReturn(chunk);
|
||||
when(chunk.exists(1)).thenReturn(true);
|
||||
when(chunk.get(1)).thenReturn(matter);
|
||||
when(matter.hasSlice(RiverCaveHydrology.class)).thenReturn(true);
|
||||
doReturn(hydrologySlice).when(matter).getSlice(RiverCaveHydrology.class);
|
||||
when(hydrologySlice.get(0, 4, 0)).thenReturn(RiverCaveHydrology.of(RiverCaveAction.WET_SOURCE));
|
||||
MantleRiverCaveVoxelView view = new MantleRiverCaveVoxelView(
|
||||
mantle,
|
||||
128,
|
||||
(x, z) -> 60,
|
||||
(x, z) -> null
|
||||
);
|
||||
|
||||
assertEquals(CaveVoxel.SOLID, view.voxelAt(position));
|
||||
assertEquals(RiverCaveAction.WET_SOURCE, view.riverActionAt(position));
|
||||
}
|
||||
}
|
||||
+98
-1
@@ -99,6 +99,7 @@ public class MantleRiverHydrologyComponentTest {
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
(minimumX, minimumZ, maximumX, maximumZ) -> riverSample(
|
||||
RiverRouteState.WET,
|
||||
RiverSection.CHANNEL
|
||||
@@ -116,9 +117,10 @@ public class MantleRiverHydrologyComponentTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buriedChannelCapsBeforeAnUnrelatedCave() {
|
||||
public void buriedChannelCutsIntoCaveWhileGuardingItsWaterline() {
|
||||
TestVoxelView view = new TestVoxelView();
|
||||
view.set(new CavePosition(1, 12, 0), CaveVoxel.CAVE_AIR);
|
||||
view.set(new CavePosition(1, 14, 0), CaveVoxel.CAVE_AIR);
|
||||
IrisRiverTunnelSample tunnel = new IrisRiverTunnelSample(
|
||||
riverSample(RiverRouteState.WET, RiverSection.CHANNEL),
|
||||
10,
|
||||
@@ -131,6 +133,72 @@ public class MantleRiverHydrologyComponentTest {
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
(minimumX, minimumZ, maximumX, maximumZ) -> riverSample(
|
||||
RiverRouteState.WET,
|
||||
RiverSection.CHANNEL
|
||||
),
|
||||
(x, z) -> x == 0 && z == 0 ? tunnel : null,
|
||||
(x, z) -> IrisRiverSurfaceSample.none(70D, 63D)
|
||||
);
|
||||
|
||||
assertEquals(RiverCaveAction.WET_SOURCE, plan.actions().get(new CavePosition(0, 12, 0)));
|
||||
assertEquals(RiverCaveAction.SEAL_GUARD, plan.actions().get(new CavePosition(1, 12, 0)));
|
||||
assertFalse(plan.actions().containsKey(new CavePosition(1, 14, 0)));
|
||||
assertEquals(
|
||||
new CaveVoxelPrecondition(CaveVoxel.CAVE_AIR, false),
|
||||
plan.preconditions().get(new CavePosition(1, 14, 0))
|
||||
);
|
||||
assertTrue(MantleRiverHydrologyComponent.preconditionsHold(view, plan.preconditions()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buriedChannelRejectsSurfaceOpenCaveAir() {
|
||||
TestVoxelView view = new TestVoxelView();
|
||||
CavePosition contact = new CavePosition(1, 12, 0);
|
||||
view.set(contact, CaveVoxel.CAVE_AIR);
|
||||
view.open(contact);
|
||||
IrisRiverTunnelSample tunnel = new IrisRiverTunnelSample(
|
||||
riverSample(RiverRouteState.WET, RiverSection.CHANNEL),
|
||||
10,
|
||||
12,
|
||||
14
|
||||
);
|
||||
|
||||
MantleRiverHydrologyComponent.TunnelPlan plan = MantleRiverHydrologyComponent.planTunnels(
|
||||
view,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
(minimumX, minimumZ, maximumX, maximumZ) -> riverSample(
|
||||
RiverRouteState.WET,
|
||||
RiverSection.CHANNEL
|
||||
),
|
||||
(x, z) -> x == 0 && z == 0 ? tunnel : null,
|
||||
(x, z) -> IrisRiverSurfaceSample.none(70D, 63D)
|
||||
);
|
||||
|
||||
assertTrue(plan.actions().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buriedChannelStillRejectsAnUncontainedFluidCave() {
|
||||
TestVoxelView view = new TestVoxelView();
|
||||
view.set(new CavePosition(1, 12, 0), CaveVoxel.COMPATIBLE_FLUID);
|
||||
IrisRiverTunnelSample tunnel = new IrisRiverTunnelSample(
|
||||
riverSample(RiverRouteState.WET, RiverSection.CHANNEL),
|
||||
10,
|
||||
12,
|
||||
14
|
||||
);
|
||||
|
||||
MantleRiverHydrologyComponent.TunnelPlan plan = MantleRiverHydrologyComponent.planTunnels(
|
||||
view,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
(minimumX, minimumZ, maximumX, maximumZ) -> riverSample(
|
||||
RiverRouteState.WET,
|
||||
RiverSection.CHANNEL
|
||||
@@ -148,6 +216,8 @@ public class MantleRiverHydrologyComponentTest {
|
||||
TestVoxelView view = new TestVoxelView();
|
||||
view.set(new CavePosition(1, 11, 0), CaveVoxel.COMPATIBLE_FLUID);
|
||||
view.set(new CavePosition(1, 12, 0), CaveVoxel.COMPATIBLE_FLUID);
|
||||
view.set(new CavePosition(1, 13, 0), CaveVoxel.CAVE_AIR);
|
||||
view.set(new CavePosition(1, 14, 0), CaveVoxel.CAVE_AIR);
|
||||
IrisRiverTunnelSample tunnel = new IrisRiverTunnelSample(
|
||||
riverSample(RiverRouteState.WET, RiverSection.CHANNEL),
|
||||
10,
|
||||
@@ -168,6 +238,7 @@ public class MantleRiverHydrologyComponentTest {
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
(minimumX, minimumZ, maximumX, maximumZ) -> riverSample(
|
||||
RiverRouteState.WET,
|
||||
RiverSection.CHANNEL
|
||||
@@ -177,6 +248,7 @@ public class MantleRiverHydrologyComponentTest {
|
||||
);
|
||||
|
||||
assertEquals(RiverCaveAction.WET_SOURCE, plan.actions().get(new CavePosition(0, 12, 0)));
|
||||
assertEquals(RiverCaveAction.DRY_AIR, plan.actions().get(new CavePosition(0, 14, 0)));
|
||||
assertFalse(plan.preconditions().containsKey(new CavePosition(1, 12, 0)));
|
||||
}
|
||||
|
||||
@@ -199,6 +271,7 @@ public class MantleRiverHydrologyComponentTest {
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
(minimumX, minimumZ, maximumX, maximumZ) -> riverSample(
|
||||
RiverRouteState.WET,
|
||||
RiverSection.CHANNEL
|
||||
@@ -343,6 +416,29 @@ public class MantleRiverHydrologyComponentTest {
|
||||
assertFalse(plan.actions().containsKey(connectedPit));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generatedTargetKeepsItsGrottoShellBelowTheRiverBed() {
|
||||
IrisRiverCaves caves = caves()
|
||||
.setDryHeadroom(4)
|
||||
.setGrottoVerticalRadius(6)
|
||||
.setMaxBoreDepth(64);
|
||||
CavePosition entry = new CavePosition(0, 54, 0);
|
||||
int waterHeadY = 48;
|
||||
|
||||
CavePosition target = MantleRiverHydrologyComponent.findGeneratedTarget(
|
||||
new TestVoxelView(),
|
||||
caves,
|
||||
entry,
|
||||
waterHeadY,
|
||||
0,
|
||||
0
|
||||
);
|
||||
|
||||
assertEquals(new CavePosition(0, 46, 0), target);
|
||||
assertTrue(target.y() + caves.getGrottoVerticalRadius() < entry.y());
|
||||
assertTrue(target.y() + caves.getGrottoVerticalRadius() >= waterHeadY + caves.getDryHeadroom());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fluidPolicyMappingPreservesReplaceSemantics() {
|
||||
assertEquals(
|
||||
@@ -436,6 +532,7 @@ public class MantleRiverHydrologyComponentTest {
|
||||
3,
|
||||
-4,
|
||||
6,
|
||||
2,
|
||||
(minimumX, minimumZ, maximumX, maximumZ) -> {
|
||||
footprintSamples.incrementAndGet();
|
||||
assertEquals(42D, minimumX, 0D);
|
||||
|
||||
+44
@@ -5,6 +5,8 @@ import art.arcane.iris.core.loader.ResourceLoader;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisDimensionCarvingResolver;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveAction;
|
||||
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.project.hunk.Hunk;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
@@ -107,10 +109,52 @@ public class IrisCarveModifierBoundarySupportTest {
|
||||
assertTrue(IrisCarveModifier.hasStableCaveFloorSupport(output, 0, 6, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void riverGuardOnlyAcceptsStableSolidBoundaryLayers() {
|
||||
RiverCaveHydrology guard = RiverCaveHydrology.of(RiverCaveAction.SEAL_GUARD);
|
||||
PlatformBlockState stone = state("minecraft:stone", true);
|
||||
PlatformBlockState sand = state("minecraft:sand", true);
|
||||
PlatformBlockState water = state("minecraft:water", false, true);
|
||||
|
||||
assertTrue(IrisCarveModifier.canReplaceRiverGuard(guard, stone, false));
|
||||
assertTrue(IrisCarveModifier.canReplaceRiverGuard(guard, stone, true));
|
||||
assertTrue(IrisCarveModifier.canReplaceRiverGuard(guard, sand, false));
|
||||
assertFalse(IrisCarveModifier.canReplaceRiverGuard(guard, sand, true));
|
||||
assertFalse(IrisCarveModifier.canReplaceRiverGuard(guard, water, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void riverBiomeInheritanceIsColumnCoherentAndHonorsLimits() {
|
||||
long seed = 7845123L;
|
||||
boolean cell = IrisCarveModifier.selectsParentRiverBiome(seed, 8, 12, 0.5D);
|
||||
for (int x = 8; x < 12; x++) {
|
||||
for (int z = 12; z < 16; z++) {
|
||||
assertTrue(cell == IrisCarveModifier.selectsParentRiverBiome(seed, x, z, 0.5D));
|
||||
}
|
||||
}
|
||||
assertFalse(IrisCarveModifier.selectsParentRiverBiome(seed, 8, 12, 0D));
|
||||
assertTrue(IrisCarveModifier.selectsParentRiverBiome(seed, 8, 12, 1D));
|
||||
|
||||
boolean inherited = false;
|
||||
boolean overridden = false;
|
||||
for (int x = -128; x <= 128; x += 4) {
|
||||
boolean selected = IrisCarveModifier.selectsParentRiverBiome(seed, x, 0, 0.5D);
|
||||
inherited |= selected;
|
||||
overridden |= !selected;
|
||||
}
|
||||
assertTrue(inherited);
|
||||
assertTrue(overridden);
|
||||
}
|
||||
|
||||
private PlatformBlockState state(String key, boolean solid) {
|
||||
return state(key, solid, false);
|
||||
}
|
||||
|
||||
private PlatformBlockState state(String key, boolean solid, boolean fluid) {
|
||||
PlatformBlockState state = mock(PlatformBlockState.class);
|
||||
doReturn(key).when(state).key();
|
||||
doReturn(solid).when(state).isSolid();
|
||||
doReturn(fluid).when(state).isFluid();
|
||||
return state;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -70,7 +70,7 @@ public class IrisCarveModifierInferenceIsolationTest {
|
||||
Map<String, IrisBiome> customBiomes = new HashMap<>();
|
||||
customBiomes.put("shared", biome);
|
||||
CarveWallBuffer walls = new CarveWallBuffer(1);
|
||||
walls.put(0, 1, 0, new MatterCavern(true, "shared", (byte) 0));
|
||||
walls.put(0, 1, 0, new MatterCavern(true, "shared", (byte) 0), false);
|
||||
Method paintBoundaryZone = IrisCarveModifier.class.getDeclaredMethod(
|
||||
"paintBoundaryZone",
|
||||
Hunk.class,
|
||||
|
||||
@@ -22,19 +22,21 @@ public class IrisCarveScratchTest {
|
||||
int y = 20 + index;
|
||||
int z = (index * 3) & 15;
|
||||
MatterCavern cavern = new MatterCavern(true, "cave-" + index, (byte) 0);
|
||||
buffer.put(x, y, z, cavern);
|
||||
buffer.put(x, y, z, cavern, (index & 1) == 0);
|
||||
expected.put(key(x, y, z), cavern);
|
||||
}
|
||||
|
||||
MatterCavern replacement = new MatterCavern(true, "replacement", (byte) 0);
|
||||
buffer.put(5, 25, 15, replacement);
|
||||
buffer.put(5, 25, 15, replacement, true);
|
||||
expected.put(key(5, 25, 15), replacement);
|
||||
|
||||
assertSame(replacement, buffer.get(5, 25, 15));
|
||||
assertTrue(buffer.isRiverBoundary(5, 25, 15));
|
||||
assertNull(buffer.get(5, 26, 15));
|
||||
assertFalse(buffer.isRiverBoundary(5, 26, 15));
|
||||
|
||||
Map<String, MatterCavern> actual = new HashMap<>();
|
||||
buffer.forEach((x, y, z, cavern) -> actual.put(key(x, y, z), cavern));
|
||||
buffer.forEach((x, y, z, cavern, riverBoundary) -> actual.put(key(x, y, z), cavern));
|
||||
assertEquals(expected.keySet(), actual.keySet());
|
||||
for (Map.Entry<String, MatterCavern> entry : expected.entrySet()) {
|
||||
assertSame(entry.getValue(), actual.get(entry.getKey()));
|
||||
@@ -47,7 +49,7 @@ public class IrisCarveScratchTest {
|
||||
MatterCavern cavern = new MatterCavern(true, "cave", (byte) 0);
|
||||
scratch.columnMasks[0].add(12);
|
||||
scratch.boundaryMasks[0].add(13);
|
||||
scratch.walls.put(1, 12, 2, cavern);
|
||||
scratch.walls.put(1, 12, 2, cavern, true);
|
||||
scratch.customBiomeCache.put("cave", null);
|
||||
scratch.customCaveBiomePresent = true;
|
||||
|
||||
@@ -58,7 +60,7 @@ public class IrisCarveScratchTest {
|
||||
assertTrue(scratch.customBiomeCache.isEmpty());
|
||||
assertFalse(scratch.customCaveBiomePresent);
|
||||
int[] wallCount = new int[1];
|
||||
scratch.walls.forEach((x, y, z, value) -> wallCount[0]++);
|
||||
scratch.walls.forEach((x, y, z, value, riverBoundary) -> wallCount[0]++);
|
||||
assertEquals(0, wallCount[0]);
|
||||
}
|
||||
|
||||
|
||||
+73
@@ -2,6 +2,7 @@ package art.arcane.iris.engine.object;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static art.arcane.iris.engine.object.IrisDimensionTypeOptions.TriState.TRUE;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
@@ -79,6 +80,60 @@ public class IrisDimensionRuntimeContractTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hotloadRejectsEnvironmentChanges() {
|
||||
IrisDimension active = hotloadDimension();
|
||||
IrisDimension replacement = hotloadDimension();
|
||||
replacement.setEnvironment(IrisEnvironment.NETHER);
|
||||
|
||||
assertHotloadRejected(active, replacement, "active environment is NORMAL");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hotloadRejectsCustomEnvironmentEvenWhenItsBaseTypeMatchesNormal() {
|
||||
IrisDimension active = hotloadDimension();
|
||||
IrisDimension replacement = hotloadDimension();
|
||||
replacement.setEnvironment(IrisEnvironment.CUSTOM);
|
||||
|
||||
assertHotloadRejected(active, replacement, "replacement environment is CUSTOM");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hotloadRejectsEffectiveDimensionOptionChanges() {
|
||||
IrisDimension active = hotloadDimension();
|
||||
IrisDimension replacement = hotloadDimension();
|
||||
replacement.setDimensionOptions(new IrisDimensionTypeOptions().coordinateScale(2D));
|
||||
|
||||
assertHotloadRejected(active, replacement, "dimensionOptions");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hotloadRejectsFullbrightChangesThatAlterTheGeneratedType() {
|
||||
IrisDimension active = hotloadDimension();
|
||||
IrisDimension replacement = hotloadDimension();
|
||||
replacement.setFullbright(true);
|
||||
|
||||
assertHotloadRejected(active, replacement, "fullbright");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hotloadAllowsExplicitOptionsEquivalentToTheBaseTemplate() {
|
||||
IrisDimension active = hotloadDimension();
|
||||
IrisDimension replacement = hotloadDimension();
|
||||
replacement.setDimensionOptions(new IrisDimensionTypeOptions().skylight(TRUE));
|
||||
|
||||
IrisDimensionRuntimeContract.requireHotloadCompatible("Studio world", active, replacement, "iris");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hotloadAllowsTerrainOnlyDimensionChanges() {
|
||||
IrisDimension active = hotloadDimension();
|
||||
IrisDimension replacement = hotloadDimension();
|
||||
replacement.setFluidHeight(80);
|
||||
|
||||
IrisDimensionRuntimeContract.requireHotloadCompatible("Studio world", active, replacement, "iris");
|
||||
}
|
||||
|
||||
private IrisDimensionRuntimeContract expectedContract() {
|
||||
IrisDimension dimension = new IrisDimension();
|
||||
dimension.setLoadKey("overworld");
|
||||
@@ -86,4 +141,22 @@ public class IrisDimensionRuntimeContractTest {
|
||||
dimension.setLogicalHeight(512);
|
||||
return IrisDimensionRuntimeContract.expected(dimension, "iris");
|
||||
}
|
||||
|
||||
private IrisDimension hotloadDimension() {
|
||||
IrisDimension dimension = new IrisDimension();
|
||||
dimension.setLoadKey("overworld");
|
||||
dimension.setEnvironment(IrisEnvironment.NORMAL);
|
||||
dimension.setDimensionHeight(new IrisRange(-64, 320));
|
||||
dimension.setLogicalHeight(384);
|
||||
return dimension;
|
||||
}
|
||||
|
||||
private void assertHotloadRejected(IrisDimension active, IrisDimension replacement, String messageFragment) {
|
||||
try {
|
||||
IrisDimensionRuntimeContract.requireHotloadCompatible("Studio world", active, replacement, "iris");
|
||||
fail("A hotload cannot replace the generated dimension type contract");
|
||||
} catch (IrisDimensionContractException e) {
|
||||
assertTrue(e.getMessage().contains(messageFragment));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
@@ -20,6 +21,8 @@ import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class RiverNetworkTest {
|
||||
private static final double TWO_PI_FOR_TESTS = StrictMath.PI * 2D;
|
||||
|
||||
@Test
|
||||
public void mapsNegativeWorldCoordinatesWithFloorDivision() {
|
||||
RiverNetwork network = new RiverNetwork(options(1L).build());
|
||||
@@ -32,6 +35,28 @@ public class RiverNetworkTest {
|
||||
assertEquals(0, network.tileXForBlock(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void expandedSamplingAddsOnlyTheRequestedLateralRadius() {
|
||||
RiverNode from = node(0L, 0L, 0D, 0D);
|
||||
RiverNode to = node(1L, 0L, 100D, 0D);
|
||||
RiverTile tile = new RiverTile(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
-64,
|
||||
128,
|
||||
64,
|
||||
List.of(reach(from, to, 4D, 1D))
|
||||
);
|
||||
|
||||
assertFalse(tile.sample(50D, 5D).present());
|
||||
RiverSample expanded = tile.sampleExpanded(50D, 5D, 2D);
|
||||
assertTrue(expanded.present());
|
||||
assertEquals(RiverSection.BANK, expanded.section());
|
||||
assertEquals(5D, expanded.distance(), 0D);
|
||||
assertThrows(IllegalArgumentException.class, () -> tile.sampleExpanded(50D, 5D, -1D));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void graphHasReciprocalCardinalsAndOneStableDiagonalPerSquare() {
|
||||
RiverNetwork network = new RiverNetwork(options(2L).build());
|
||||
@@ -138,6 +163,30 @@ public class RiverNetworkTest {
|
||||
assertEquals(digest(tile), digest(network.buildTile(0, 0, terrain)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void recursiveBranchesApplyTheChildSoftCapAtEveryNode() {
|
||||
RiverNetwork network = new RiverNetwork(options(720L)
|
||||
.requireOcean(false)
|
||||
.routingBasinCells(16)
|
||||
.branchSoftCap(4)
|
||||
.branchChildShrinkFactor(0D)
|
||||
.build());
|
||||
RiverTerrainSampler terrain = flatTerrain(false);
|
||||
HashMap<RiverNodeId, Integer> incoming = new HashMap<>();
|
||||
for (long cellX = -24L; cellX <= 24L; cellX++) {
|
||||
for (long cellZ = -24L; cellZ <= 24L; cellZ++) {
|
||||
RiverNode downstream = network.downstream(new RiverNodeId(cellX, cellZ), terrain);
|
||||
if (downstream != null) {
|
||||
incoming.merge(downstream.id(), 1, Integer::sum);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertFalse(incoming.isEmpty());
|
||||
assertTrue(incoming.values().stream().allMatch(children -> children <= 4));
|
||||
assertTrue(incoming.values().stream().anyMatch(children -> children >= 3));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void candidateRankingAndTracingDoNotMaterializeMeanderGeometry() {
|
||||
RiverNetwork network = new RiverNetwork(options(75L)
|
||||
@@ -167,8 +216,8 @@ public class RiverNetworkTest {
|
||||
|
||||
assertFalse(tile.reaches().isEmpty());
|
||||
assertTrue(meanderSamples[0] > 0);
|
||||
assertEquals(0, meanderSamples[0] % 33);
|
||||
assertTrue(meanderSamples[0] / 33 <= 256);
|
||||
assertEquals(0, meanderSamples[0] % 12);
|
||||
assertTrue(meanderSamples[0] / 12 <= 256);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -330,27 +379,33 @@ public class RiverNetworkTest {
|
||||
assertFalse(wet.edges().isEmpty());
|
||||
|
||||
RiverNetwork dryNetwork = new RiverNetwork(options(4L).maxRouteReaches(6).build());
|
||||
RiverRoute dry = dryNetwork.trace(source, slopedTerrain(false));
|
||||
RiverTerrainSampler physicalTerminal = new TestTerrain(false) {
|
||||
@Override
|
||||
public boolean allowsReach(RiverRoutingContext context) {
|
||||
return context.from().id().equals(source);
|
||||
}
|
||||
};
|
||||
RiverRoute dry = dryNetwork.trace(source, physicalTerminal);
|
||||
assertEquals(RiverRouteState.DRY, dry.state());
|
||||
assertFalse(dry.edges().isEmpty());
|
||||
|
||||
RiverNetwork suppressedNetwork = new RiverNetwork(
|
||||
options(4L).maxRouteReaches(6).dryChannelChance(0.0).build()
|
||||
);
|
||||
RiverRoute suppressed = suppressedNetwork.trace(source, slopedTerrain(false));
|
||||
RiverRoute suppressed = suppressedNetwork.trace(source, physicalTerminal);
|
||||
assertEquals(RiverRouteState.SUPPRESSED, suppressed.state());
|
||||
assertTrue(suppressed.edges().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void oceanOptionalRoutesRemainWetAndExposeTerminalState() {
|
||||
public void oceanOptionalRoutesDoNotTurnTheTraceHorizonIntoATerminal() {
|
||||
RiverNetwork network = new RiverNetwork(
|
||||
options(41L).maxRouteReaches(3).requireOcean(false).build()
|
||||
);
|
||||
RiverRoute route = network.trace(new RiverNodeId(0L, 0L), slopedTerrain(false));
|
||||
assertEquals(RiverRouteState.WET, route.state());
|
||||
assertFalse(route.oceanConnected());
|
||||
assertTrue(route.terminal());
|
||||
assertFalse(route.terminal());
|
||||
assertFalse(route.edges().isEmpty());
|
||||
}
|
||||
|
||||
@@ -359,12 +414,22 @@ public class RiverNetworkTest {
|
||||
RiverNodeId source = new RiverNodeId(0L, 0L);
|
||||
RiverNetwork requiredOcean = new RiverNetwork(options(42L).maxRouteReaches(3).requireOcean(true).build());
|
||||
RiverTerrainSampler wetTerminal = new TestTerrain(false) {
|
||||
@Override
|
||||
public boolean allowsReach(RiverRoutingContext context) {
|
||||
return context.from().id().equals(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RiverTerminalPolicy terminalPolicy(int blockX, int blockZ) {
|
||||
return RiverTerminalPolicy.WET;
|
||||
}
|
||||
};
|
||||
RiverTerrainSampler suppressedTerminal = new TestTerrain(false) {
|
||||
@Override
|
||||
public boolean allowsReach(RiverRoutingContext context) {
|
||||
return context.from().id().equals(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RiverTerminalPolicy terminalPolicy(int blockX, int blockZ) {
|
||||
return RiverTerminalPolicy.SUPPRESS;
|
||||
@@ -376,6 +441,11 @@ public class RiverNetworkTest {
|
||||
|
||||
RiverNetwork optionalOcean = new RiverNetwork(options(43L).maxRouteReaches(3).requireOcean(false).build());
|
||||
RiverTerrainSampler dryTerminal = new TestTerrain(false) {
|
||||
@Override
|
||||
public boolean allowsReach(RiverRoutingContext context) {
|
||||
return context.from().id().equals(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RiverTerminalPolicy terminalPolicy(int blockX, int blockZ) {
|
||||
return RiverTerminalPolicy.DRY;
|
||||
@@ -546,10 +616,10 @@ public class RiverNetworkTest {
|
||||
);
|
||||
|
||||
assertEquals(List.of(
|
||||
-6381557853649488119L,
|
||||
8976828799821705624L,
|
||||
6774369135184562919L,
|
||||
-6208289191927305511L
|
||||
3050685048971830506L,
|
||||
-3861364772874819248L,
|
||||
-8770785170128501517L,
|
||||
6698607697170340377L
|
||||
), actual);
|
||||
}
|
||||
|
||||
@@ -589,6 +659,7 @@ public class RiverNetworkTest {
|
||||
.channelWidth(512D)
|
||||
.maxChannelWidth(512D)
|
||||
.bankWidth(0D)
|
||||
.branchChildShrinkFactor(1D)
|
||||
.meanderStrength(0D)
|
||||
.meanderSubdivisions(1)
|
||||
.build());
|
||||
@@ -716,6 +787,7 @@ public class RiverNetworkTest {
|
||||
1,
|
||||
1,
|
||||
10.0,
|
||||
RiverWidthProfile.constant(10.0),
|
||||
5.0,
|
||||
3.0,
|
||||
false,
|
||||
@@ -795,6 +867,7 @@ public class RiverNetworkTest {
|
||||
.maxChannelWidth(10D)
|
||||
.maxBankWidth(3D)
|
||||
.maxDepth(6D)
|
||||
.meanderStrength(0D)
|
||||
.build());
|
||||
RiverTerrainSampler styled = new TestTerrain(false) {
|
||||
@Override
|
||||
@@ -832,6 +905,78 @@ public class RiverNetworkTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void channelWidthVariesInsideOneReachAndRemainsSpatiallyQueryable() {
|
||||
RiverNode from = node(0L, 0L, 0D, 0D);
|
||||
RiverNode to = node(1L, 0L, 100D, 0D);
|
||||
RiverWidthProfile profile = new RiverWidthProfile(
|
||||
new double[]{0D, 0.5D, 1D},
|
||||
new double[]{1D, 7D, 1D}
|
||||
);
|
||||
RiverReach reach = new RiverReach(
|
||||
RiverEdgeId.of(from.id(), to.id()),
|
||||
from,
|
||||
to,
|
||||
RiverRouteState.WET,
|
||||
1,
|
||||
1,
|
||||
7D,
|
||||
profile,
|
||||
0D,
|
||||
3D,
|
||||
false,
|
||||
false,
|
||||
new RiverPolyline(new double[]{0D, 100D}, new double[]{0D, 0D})
|
||||
);
|
||||
RiverTile tile = new RiverTile(0, 0, 0, -16, 128, 32, List.of(reach));
|
||||
|
||||
RiverSample narrow = tile.sample(10D, 2D);
|
||||
RiverSample wide = tile.sample(50D, 2D);
|
||||
|
||||
assertFalse(narrow.present());
|
||||
assertTrue(wide.present());
|
||||
assertEquals(7D, wide.width(), 0D);
|
||||
assertEquals(RiverSection.CHANNEL, wide.section());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void foldedReachSamplingFindsFartherCoveringWidthEnvelope() {
|
||||
RiverNode from = node(0L, 0L, 0D, 0D);
|
||||
RiverNode to = node(1L, 0L, 0D, 10D);
|
||||
RiverWidthProfile profile = new RiverWidthProfile(
|
||||
new double[]{0D, 0.48D, 0.53D, 1D},
|
||||
new double[]{1D, 1D, 18D, 18D}
|
||||
);
|
||||
RiverReach reach = new RiverReach(
|
||||
RiverEdgeId.of(from.id(), to.id()),
|
||||
from,
|
||||
to,
|
||||
RiverRouteState.WET,
|
||||
1,
|
||||
1,
|
||||
18D,
|
||||
profile,
|
||||
0D,
|
||||
3D,
|
||||
false,
|
||||
false,
|
||||
new RiverPolyline(
|
||||
new double[]{0D, 100D, 100D, 0D},
|
||||
new double[]{0D, 0D, 10D, 10D}
|
||||
)
|
||||
);
|
||||
RiverTile tile = new RiverTile(0, 0, 0, -16, 128, 32, List.of(reach));
|
||||
|
||||
RiverSample point = tile.sample(50D, 2D);
|
||||
RiverSample footprint = tile.sampleFootprint(49D, 1.5D, 51D, 2.5D);
|
||||
|
||||
assertTrue(point.present());
|
||||
assertTrue(point.alongReach() > 0.53D);
|
||||
assertEquals(18D, point.width(), 0.000001D);
|
||||
assertTrue(footprint.present());
|
||||
assertTrue(footprint.alongReach() > 0.53D);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reachFeasibilityReceivesTheFinalPinnedMeanderPolyline() {
|
||||
RiverNetwork network = new RiverNetwork(options(62L).meanderStrength(20D).build());
|
||||
@@ -865,6 +1010,182 @@ public class RiverNetworkTest {
|
||||
assertEquals(null, downstream);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void irisNoiseChangesTheSelectedPersonalityWithoutBreakingItsEnvelope() {
|
||||
double meanderStrength = 20D;
|
||||
RiverNetwork network = new RiverNetwork(options(63L)
|
||||
.siteJitter(0D)
|
||||
.requireOcean(false)
|
||||
.meanderStrength(meanderStrength)
|
||||
.meanderSubdivisions(32)
|
||||
.build());
|
||||
RiverTerrainSampler quietTerrain = new TestTerrain(false) {
|
||||
@Override
|
||||
public double meanderNoise(RiverMeanderContext context) {
|
||||
return 0D;
|
||||
}
|
||||
};
|
||||
RiverTerrainSampler noisyTerrain = new TestTerrain(false) {
|
||||
@Override
|
||||
public double meanderNoise(RiverMeanderContext context) {
|
||||
return StrictMath.sin(context.normalizedPosition() * StrictMath.PI * 7D);
|
||||
}
|
||||
};
|
||||
|
||||
RiverReach quietReach = network.buildTile(0, 0, quietTerrain).reaches().getFirst();
|
||||
RiverReach noisyReach = network.buildTile(0, 0, noisyTerrain).reaches().getFirst();
|
||||
assertEquals(quietReach.id(), noisyReach.id());
|
||||
RiverPolyline quietPolyline = quietReach.polyline();
|
||||
RiverPolyline noisyPolyline = noisyReach.polyline();
|
||||
double deltaX = noisyReach.to().x() - noisyReach.from().x();
|
||||
double deltaZ = noisyReach.to().z() - noisyReach.from().z();
|
||||
double chordLength = StrictMath.hypot(deltaX, deltaZ);
|
||||
boolean changed = false;
|
||||
for (int point = 1; point < noisyPolyline.size() - 1; point++) {
|
||||
double pointX = noisyPolyline.x(point) - noisyReach.from().x();
|
||||
double pointZ = noisyPolyline.z(point) - noisyReach.from().z();
|
||||
double signedOffset = (pointX * deltaZ - pointZ * deltaX) / chordLength;
|
||||
if (Double.doubleToLongBits(noisyPolyline.x(point)) != Double.doubleToLongBits(quietPolyline.x(point))
|
||||
|| Double.doubleToLongBits(noisyPolyline.z(point))
|
||||
!= Double.doubleToLongBits(quietPolyline.z(point))) {
|
||||
changed = true;
|
||||
}
|
||||
assertTrue(StrictMath.abs(signedOffset) <= meanderStrength + 0.0000001D);
|
||||
}
|
||||
|
||||
assertTrue(changed);
|
||||
assertEquals(noisyReach.from().x(), noisyPolyline.x(0), 0D);
|
||||
assertEquals(noisyReach.from().z(), noisyPolyline.z(0), 0D);
|
||||
int last = noisyPolyline.size() - 1;
|
||||
assertEquals(noisyReach.to().x(), noisyPolyline.x(last), 0D);
|
||||
assertEquals(noisyReach.to().z(), noisyPolyline.z(last), 0D);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reachesSpanBroadAsymmetricCompoundAndRestlessShapeFamilies() {
|
||||
double meanderStrength = 20D;
|
||||
RiverNetwork network = new RiverNetwork(options(64L)
|
||||
.siteJitter(0D)
|
||||
.requireOcean(false)
|
||||
.meanderStrength(meanderStrength)
|
||||
.meanderSubdivisions(48)
|
||||
.build());
|
||||
RiverTerrainSampler terrain = new TestTerrain(false) {
|
||||
@Override
|
||||
public double meanderNoise(RiverMeanderContext context) {
|
||||
return 0D;
|
||||
}
|
||||
};
|
||||
|
||||
HashMap<RiverEdgeId, RiverReach> uniqueReaches = new HashMap<>();
|
||||
for (int tileX = -2; tileX <= 2; tileX++) {
|
||||
for (int tileZ = -2; tileZ <= 2; tileZ++) {
|
||||
for (RiverReach reach : network.buildTile(tileX, tileZ, terrain).reaches()) {
|
||||
uniqueReaches.putIfAbsent(reach.id(), reach);
|
||||
}
|
||||
}
|
||||
}
|
||||
ArrayList<ReachShape> shapes = new ArrayList<>(uniqueReaches.size());
|
||||
for (RiverReach reach : uniqueReaches.values()) {
|
||||
shapes.add(shapeOf(reach, meanderStrength));
|
||||
}
|
||||
double minimumAmplitude = shapes.stream()
|
||||
.mapToDouble(ReachShape::amplitudeRatio)
|
||||
.min()
|
||||
.orElseThrow();
|
||||
double maximumAmplitude = shapes.stream()
|
||||
.mapToDouble(ReachShape::amplitudeRatio)
|
||||
.max()
|
||||
.orElseThrow();
|
||||
HashSet<Integer> dominantBands = new HashSet<>();
|
||||
HashSet<String> signatures = new HashSet<>();
|
||||
for (ReachShape shape : shapes) {
|
||||
dominantBands.add(shape.dominantBand());
|
||||
signatures.add(
|
||||
StrictMath.min(6, shape.crossings()) + ":"
|
||||
+ (int) StrictMath.floor(shape.centroid() * 5D) + ":"
|
||||
+ (int) StrictMath.floor(shape.amplitudeRatio() * 5D) + ":"
|
||||
+ shape.dominantBand()
|
||||
);
|
||||
}
|
||||
String diagnostics = shapes.toString();
|
||||
assertTrue(diagnostics, shapes.size() >= 32);
|
||||
assertTrue(diagnostics, shapes.stream().anyMatch(shape -> shape.crossings() <= 1));
|
||||
assertTrue(diagnostics, shapes.stream().anyMatch(shape -> shape.crossings() >= 2 && shape.crossings() <= 4));
|
||||
assertTrue(diagnostics, shapes.stream().anyMatch(shape -> shape.crossings() >= 5));
|
||||
assertTrue(diagnostics, shapes.stream().anyMatch(shape -> shape.centroid() < 0.43D));
|
||||
assertTrue(diagnostics, shapes.stream().anyMatch(shape -> shape.centroid() > 0.57D));
|
||||
assertTrue(diagnostics, maximumAmplitude - minimumAmplitude >= 0.25D);
|
||||
assertTrue(diagnostics, dominantBands.size() >= 3);
|
||||
assertTrue(diagnostics, signatures.size() >= 8);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void drainageDomainWarpChangesRoutesDeterministically() {
|
||||
RiverTerrainSampler terrain = flatTerrain(false);
|
||||
RiverNetwork straight = new RiverNetwork(options(65L)
|
||||
.requireOcean(false)
|
||||
.routingDeviationStrengthCells(0D)
|
||||
.build());
|
||||
RiverNetwork deviated = new RiverNetwork(options(65L)
|
||||
.requireOcean(false)
|
||||
.routingDeviationScaleCells(8)
|
||||
.routingDeviationStrengthCells(4D)
|
||||
.build());
|
||||
boolean changed = false;
|
||||
for (long cellX = -8L; cellX <= 8L; cellX++) {
|
||||
for (long cellZ = -8L; cellZ <= 8L; cellZ++) {
|
||||
RiverNode straightDownstream = straight.downstream(new RiverNodeId(cellX, cellZ), terrain);
|
||||
RiverNode deviatedDownstream = deviated.downstream(new RiverNodeId(cellX, cellZ), terrain);
|
||||
if (!Objects.equals(
|
||||
straightDownstream == null ? null : straightDownstream.id(),
|
||||
deviatedDownstream == null ? null : deviatedDownstream.id()
|
||||
)) {
|
||||
changed = true;
|
||||
}
|
||||
RiverNode repeated = deviated.downstream(new RiverNodeId(cellX, cellZ), terrain);
|
||||
assertEquals(
|
||||
deviatedDownstream == null ? null : deviatedDownstream.id(),
|
||||
repeated == null ? null : repeated.id()
|
||||
);
|
||||
}
|
||||
}
|
||||
assertTrue(changed);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void confluenceAttractionChangesDownstreamChoicesDeterministically() {
|
||||
RiverTerrainSampler terrain = flatTerrain(false);
|
||||
RiverNetwork dispersed = new RiverNetwork(options(67L)
|
||||
.requireOcean(false)
|
||||
.confluenceWeight(0D)
|
||||
.build());
|
||||
RiverNetwork branching = new RiverNetwork(options(67L)
|
||||
.requireOcean(false)
|
||||
.confluenceWeight(512D)
|
||||
.build());
|
||||
boolean changed = false;
|
||||
for (long cellX = -8L; cellX <= 8L; cellX++) {
|
||||
for (long cellZ = -8L; cellZ <= 8L; cellZ++) {
|
||||
RiverNodeId id = new RiverNodeId(cellX, cellZ);
|
||||
RiverNode dispersedDownstream = dispersed.downstream(id, terrain);
|
||||
RiverNode branchingDownstream = branching.downstream(id, terrain);
|
||||
if (!Objects.equals(
|
||||
dispersedDownstream == null ? null : dispersedDownstream.id(),
|
||||
branchingDownstream == null ? null : branchingDownstream.id()
|
||||
)) {
|
||||
changed = true;
|
||||
}
|
||||
RiverNode repeated = branching.downstream(id, terrain);
|
||||
assertEquals(
|
||||
branchingDownstream == null ? null : branchingDownstream.id(),
|
||||
repeated == null ? null : repeated.id()
|
||||
);
|
||||
}
|
||||
}
|
||||
assertTrue(changed);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void optionsRejectNonFiniteOrUnsafeValues() {
|
||||
assertThrows(IllegalArgumentException.class, () -> options(10L).sourceChance(Double.NaN).build());
|
||||
@@ -872,6 +1193,9 @@ public class RiverNetworkTest {
|
||||
assertThrows(IllegalArgumentException.class, () -> options(10L).routingBasinCells(7).build());
|
||||
assertThrows(IllegalArgumentException.class, () -> options(10L).routingPlateauHeight(0D).build());
|
||||
assertThrows(IllegalArgumentException.class, () -> options(10L).hydraulicBaseHeight(Double.NaN).build());
|
||||
assertThrows(IllegalArgumentException.class, () -> options(10L).confluenceWeight(-1D).build());
|
||||
assertThrows(IllegalArgumentException.class, () -> options(10L).branchSoftCap(0).build());
|
||||
assertThrows(IllegalArgumentException.class, () -> options(10L).branchChildShrinkFactor(1.1D).build());
|
||||
assertThrows(IllegalArgumentException.class, () -> options(10L).channelWidth(-1.0).build());
|
||||
assertThrows(IllegalArgumentException.class, () -> options(10L)
|
||||
.tileCells(1)
|
||||
@@ -981,6 +1305,7 @@ public class RiverNetworkTest {
|
||||
1,
|
||||
1,
|
||||
width,
|
||||
RiverWidthProfile.constant(width),
|
||||
bankWidth,
|
||||
3D,
|
||||
false,
|
||||
@@ -1022,6 +1347,57 @@ public class RiverNetworkTest {
|
||||
return hydraulicComparison != 0 ? hydraulicComparison : first.id().compareTo(second.id());
|
||||
}
|
||||
|
||||
private static ReachShape shapeOf(RiverReach reach, double meanderStrength) {
|
||||
RiverPolyline polyline = reach.polyline();
|
||||
double deltaX = reach.to().x() - reach.from().x();
|
||||
double deltaZ = reach.to().z() - reach.from().z();
|
||||
double chordLength = StrictMath.hypot(deltaX, deltaZ);
|
||||
double[] offsets = new double[polyline.size()];
|
||||
double maximumOffset = 0D;
|
||||
double weightedPosition = 0D;
|
||||
double totalWeight = 0D;
|
||||
int crossings = 0;
|
||||
int previousSign = 0;
|
||||
for (int point = 0; point < polyline.size(); point++) {
|
||||
double pointX = polyline.x(point) - reach.from().x();
|
||||
double pointZ = polyline.z(point) - reach.from().z();
|
||||
double offset = (pointX * deltaZ - pointZ * deltaX) / chordLength;
|
||||
offsets[point] = offset;
|
||||
double absoluteOffset = StrictMath.abs(offset);
|
||||
maximumOffset = StrictMath.max(maximumOffset, absoluteOffset);
|
||||
double t = (double) point / (polyline.size() - 1);
|
||||
weightedPosition += t * absoluteOffset;
|
||||
totalWeight += absoluteOffset;
|
||||
int sign = absoluteOffset < meanderStrength * 0.05D ? 0 : offset < 0D ? -1 : 1;
|
||||
if (sign != 0 && previousSign != 0 && sign != previousSign) {
|
||||
crossings++;
|
||||
}
|
||||
if (sign != 0) {
|
||||
previousSign = sign;
|
||||
}
|
||||
assertTrue(absoluteOffset <= meanderStrength + 0.0000001D);
|
||||
}
|
||||
int dominantBand = 1;
|
||||
double dominantEnergy = -1D;
|
||||
int samples = offsets.length - 1;
|
||||
for (int frequency = 1; frequency <= 8; frequency++) {
|
||||
double cosine = 0D;
|
||||
double sine = 0D;
|
||||
for (int point = 0; point < samples; point++) {
|
||||
double phase = TWO_PI_FOR_TESTS * frequency * point / samples;
|
||||
cosine += offsets[point] * StrictMath.cos(phase);
|
||||
sine += offsets[point] * StrictMath.sin(phase);
|
||||
}
|
||||
double energy = cosine * cosine + sine * sine;
|
||||
if (energy > dominantEnergy) {
|
||||
dominantEnergy = energy;
|
||||
dominantBand = frequency;
|
||||
}
|
||||
}
|
||||
double centroid = totalWeight <= 0.0000001D ? 0.5D : weightedPosition / totalWeight;
|
||||
return new ReachShape(crossings, maximumOffset / meanderStrength, centroid, dominantBand);
|
||||
}
|
||||
|
||||
private static long digest(RiverTile tile) {
|
||||
long hash = 0xCBF29CE484222325L;
|
||||
for (RiverReach reach : tile.reaches()) {
|
||||
@@ -1032,6 +1408,10 @@ public class RiverNetworkTest {
|
||||
hash = RiverNetwork.mix(hash ^ reach.order());
|
||||
hash = RiverNetwork.mix(hash ^ reach.state().ordinal());
|
||||
hash = RiverNetwork.mix(hash ^ Double.doubleToLongBits(reach.width()));
|
||||
for (int index = 0; index < reach.widthProfile().size(); index++) {
|
||||
hash = RiverNetwork.mix(hash ^ Double.doubleToLongBits(reach.widthProfile().position(index)));
|
||||
hash = RiverNetwork.mix(hash ^ Double.doubleToLongBits(reach.widthProfile().width(index)));
|
||||
}
|
||||
hash = RiverNetwork.mix(hash ^ Double.doubleToLongBits(reach.bankWidth()));
|
||||
hash = RiverNetwork.mix(hash ^ Double.doubleToLongBits(reach.depth()));
|
||||
hash = RiverNetwork.mix(hash ^ (reach.mouth() ? 1L : 0L));
|
||||
@@ -1062,6 +1442,7 @@ public class RiverNetworkTest {
|
||||
assertEquals(first.flow(), second.flow());
|
||||
assertEquals(first.order(), second.order());
|
||||
assertEquals(first.width(), second.width(), 0.0);
|
||||
assertEquals(first.widthProfile(), second.widthProfile());
|
||||
assertEquals(first.bankWidth(), second.bankWidth(), 0.0);
|
||||
assertEquals(first.depth(), second.depth(), 0.0);
|
||||
assertEquals(first.mouth(), second.mouth());
|
||||
@@ -1092,4 +1473,7 @@ public class RiverNetworkTest {
|
||||
return ocean && blockX >= 224;
|
||||
}
|
||||
}
|
||||
|
||||
private record ReachShape(int crossings, double amplitudeRatio, double centroid, int dominantBand) {
|
||||
}
|
||||
}
|
||||
@@ -295,6 +295,7 @@ public class RiverTileCacheTest {
|
||||
1,
|
||||
1,
|
||||
8.0,
|
||||
RiverWidthProfile.constant(8.0),
|
||||
4.0,
|
||||
3.0,
|
||||
false,
|
||||
|
||||
+41
-3
@@ -160,8 +160,8 @@ public class RiverCaveContainmentPlannerTest {
|
||||
RiverCavePlan rejected = planner.plan(view, source, rejectedSettings);
|
||||
|
||||
assertTrue(accepted.accepted());
|
||||
assertEquals(RiverCaveAction.DRY_AIR, accepted.actions().get(position(0, 11, 0)));
|
||||
assertEquals(RiverCaveAction.DRY_AIR, accepted.actions().get(position(0, 12, 0)));
|
||||
assertEquals(RiverCaveAction.FALLING_WATER, accepted.actions().get(position(0, 11, 0)));
|
||||
assertEquals(RiverCaveAction.FALLING_WATER, accepted.actions().get(position(0, 12, 0)));
|
||||
assertEquals(RiverCaveRejection.DRY_HEADROOM_LIMIT, rejected.rejection());
|
||||
}
|
||||
|
||||
@@ -341,7 +341,7 @@ public class RiverCaveContainmentPlannerTest {
|
||||
RiverCavePlan plan = planner.plan(view, source, settings());
|
||||
|
||||
assertTrue(plan.accepted());
|
||||
assertEquals(RiverCaveAction.DRY_AIR, plan.actions().get(position(0, 12, 0)));
|
||||
assertEquals(RiverCaveAction.FALLING_WATER, plan.actions().get(position(0, 12, 0)));
|
||||
assertEquals(RiverCaveAction.WET_SOURCE, plan.actions().get(position(0, 8, 0)));
|
||||
assertTrue(plan.actions().containsValue(RiverCaveAction.SEAL_GUARD));
|
||||
assertEquals(plan.actions().keySet(), plan.baselinePreconditions().keySet());
|
||||
@@ -349,6 +349,44 @@ public class RiverCaveContainmentPlannerTest {
|
||||
assertMutationConnected(plan, source.entry(), source.target());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generatedGrottoAcceptsItsCompleteSurfaceMouth() {
|
||||
TestVoxelView view = new TestVoxelView();
|
||||
CavePosition[] wetMouth = {
|
||||
position(-1, 12, 0),
|
||||
position(1, 12, 0),
|
||||
position(0, 12, -1),
|
||||
position(0, 12, 1)
|
||||
};
|
||||
CavePosition[] dryMouth = {
|
||||
position(-1, 13, 0),
|
||||
position(1, 13, 0),
|
||||
position(0, 13, -1),
|
||||
position(0, 13, 1)
|
||||
};
|
||||
view.set(CaveVoxel.COMPATIBLE_FLUID, wetMouth);
|
||||
view.set(CaveVoxel.CAVE_AIR, dryMouth);
|
||||
for (CavePosition position : wetMouth) {
|
||||
view.openToSurface(position);
|
||||
}
|
||||
for (CavePosition position : dryMouth) {
|
||||
view.openToSurface(position);
|
||||
}
|
||||
RiverCaveSource source = source(108L, 10, RiverCaveMode.GENERATED_GROTTO, position(0, 8, 0));
|
||||
|
||||
RiverCavePlan plan = planner.plan(view, source, detailedSettings(
|
||||
2,
|
||||
2,
|
||||
RiverCaveGrottoShape.ELLIPSOID,
|
||||
RiverCaveFluidPolicy.ALLOW_COMPATIBLE
|
||||
));
|
||||
|
||||
assertTrue(plan.rejection().toString(), plan.accepted());
|
||||
for (CavePosition position : wetMouth) {
|
||||
assertEquals(RiverCaveAction.FALLING_WATER, plan.actions().get(position));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generatedGrottoRejectsAnOpenShell() {
|
||||
TestVoxelView view = new TestVoxelView();
|
||||
|
||||
@@ -22,7 +22,9 @@ import art.arcane.iris.engine.river.RiverRouteState;
|
||||
import art.arcane.iris.engine.river.RiverAnchor;
|
||||
import art.arcane.iris.engine.river.RiverRoutingContext;
|
||||
import art.arcane.iris.engine.river.RiverTerrainNodeSample;
|
||||
import art.arcane.iris.engine.river.RiverTopologyComplexity;
|
||||
import art.arcane.iris.engine.river.RiverTerrainSourceSample;
|
||||
import art.arcane.iris.engine.river.RiverWidthProfile;
|
||||
import art.arcane.iris.util.project.interpolation.NoiseBounds;
|
||||
import art.arcane.iris.util.project.stream.ProceduralStream;
|
||||
import art.arcane.iris.util.project.stream.interpolation.Interpolated;
|
||||
@@ -40,6 +42,82 @@ import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
public class IrisRiverRuntimeTest {
|
||||
@Test
|
||||
public void tunnelCrossSectionRoundsAndNoiseModulatesBothSurfaces() {
|
||||
assertEquals(62, IrisRiverRuntime.shapedTunnelBedY(70, 64D, 1D, 2D));
|
||||
assertEquals(69, IrisRiverRuntime.shapedTunnelBedY(70, 64D, 0D, 2D));
|
||||
assertEquals(65, IrisRiverRuntime.shapedTunnelBedY(70, 64D, 1D, -1D));
|
||||
assertEquals(77, IrisRiverRuntime.shapedTunnelCeilingY(70, 4D, 1D, 3D));
|
||||
assertEquals(70, IrisRiverRuntime.shapedTunnelCeilingY(70, 4D, 0D, 3D));
|
||||
assertEquals(71, IrisRiverRuntime.shapedTunnelCeilingY(70, 4D, 0.5D, -2D));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void widenedTunnelHaloAccountsForMaximumConfiguredMultiplier() {
|
||||
assertEquals(17, RiverTopologyComplexity.tunnelHalo(7D, 4D, 2D));
|
||||
assertEquals(2500L, RiverTopologyComplexity.tunnelSampleColumns(7D, 4D, 2D));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tunnelWidthMultiplierWidensOnlySubterraneanFootprint() {
|
||||
IrisRiverNetwork narrowConfiguration = configuration(false);
|
||||
narrowConfiguration.getTerrain()
|
||||
.setMaxChannelWidth(4D)
|
||||
.setMaxBankWidth(0D)
|
||||
.setBankWidth(range(0D))
|
||||
.setMaxIncision(10)
|
||||
.setTunnelMouthBlend(0D)
|
||||
.setTunnelWidthMultiplier(range(1D));
|
||||
IrisRiverNetwork wideConfiguration = configuration(false);
|
||||
wideConfiguration.getTerrain()
|
||||
.setMaxChannelWidth(4D)
|
||||
.setMaxBankWidth(0D)
|
||||
.setBankWidth(range(0D))
|
||||
.setMaxIncision(10)
|
||||
.setTunnelMouthBlend(0D)
|
||||
.setTunnelWidthMultiplier(range(4D));
|
||||
|
||||
try (IrisRiverRuntime narrow = runtime(
|
||||
narrowConfiguration,
|
||||
constantHeight(100D),
|
||||
constantLandBiome(),
|
||||
new IrisRegion(),
|
||||
true,
|
||||
false
|
||||
); IrisRiverRuntime wide = runtime(
|
||||
wideConfiguration,
|
||||
constantHeight(100D),
|
||||
constantLandBiome(),
|
||||
new IrisRegion(),
|
||||
true,
|
||||
false
|
||||
)) {
|
||||
int narrowTunnelColumns = 0;
|
||||
int wideTunnelColumns = 0;
|
||||
int narrowSurfaceColumns = 0;
|
||||
int wideSurfaceColumns = 0;
|
||||
for (int x = -128; x < 128; x += 2) {
|
||||
for (int z = -128; z < 128; z += 2) {
|
||||
if (narrow.sampleTunnel(x, z) != null) {
|
||||
narrowTunnelColumns++;
|
||||
}
|
||||
if (wide.sampleTunnel(x, z) != null) {
|
||||
wideTunnelColumns++;
|
||||
}
|
||||
if (narrow.sample(x, z).river().present()) {
|
||||
narrowSurfaceColumns++;
|
||||
}
|
||||
if (wide.sample(x, z).river().present()) {
|
||||
wideSurfaceColumns++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertTrue(wideTunnelColumns > narrowTunnelColumns * 2);
|
||||
assertEquals(narrowSurfaceColumns, wideSurfaceColumns);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void terminalTaperUsesMeasuredReachLength() {
|
||||
assertEquals(1D, IrisRiverRuntime.terminalWeight(40, 200D, 0.8D), 0D);
|
||||
@@ -185,6 +263,50 @@ public class IrisRiverRuntimeTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void oceanIntentOnlyTerminatesRiversWhereTheNaturalSurfaceIsSubmerged() {
|
||||
IrisRiverNetwork configuration = configuration(false);
|
||||
configuration.getTopology().setTerrainHeightWeight(0D);
|
||||
IrisBiome oceanBiome = new IrisBiome().setInferredType(InferredType.SEA);
|
||||
IrisRegion region = new IrisRegion();
|
||||
ProceduralStream<Double> height = ProceduralStream.ofDouble((x, z) -> x < 0D ? 50D : 200D);
|
||||
ProceduralStream<Boolean> oceanIntent = ProceduralStream.of(
|
||||
(x, z) -> true,
|
||||
Interpolated.BOOLEAN
|
||||
);
|
||||
ProceduralStream<IrisBiome> biomes = ProceduralStream.of(
|
||||
(x, z) -> oceanBiome,
|
||||
Interpolated.of(value -> 0D, value -> oceanBiome)
|
||||
);
|
||||
ProceduralStream<IrisRegion> regions = ProceduralStream.of(
|
||||
(x, z) -> region,
|
||||
Interpolated.of(value -> 0D, value -> region)
|
||||
);
|
||||
|
||||
try (IrisRiverRuntime runtime = new IrisRiverRuntime(new IrisRiverRuntimeContext(
|
||||
4829759234L,
|
||||
configuration,
|
||||
mock(IrisData.class),
|
||||
63,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
(x, z) -> new NoiseBounds(0D, 512D),
|
||||
height,
|
||||
ProceduralStream.ofDouble((x, z) -> 0D),
|
||||
oceanIntent,
|
||||
biomes,
|
||||
regions
|
||||
))) {
|
||||
assertTrue(runtime.sampleNode(-1, 0).ocean());
|
||||
assertFalse(runtime.sampleNode(1, 0).ocean());
|
||||
assertTrue(runtime.sampleSource(-1, 0).ocean());
|
||||
assertFalse(runtime.sampleSource(1, 0).ocean());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void wetTerminalRiverCarvesDownAndPublishesAFluidHead() {
|
||||
IrisRiverNetwork configuration = configuration(false);
|
||||
@@ -568,6 +690,42 @@ public class IrisRiverRuntimeTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cliffTransitionClassifiesOpenAndSolidColumnsIndependently() {
|
||||
IrisRiverNetwork configuration = configuration(false);
|
||||
configuration.getTerrain()
|
||||
.setMaxIncision(10)
|
||||
.setTunnelMouthBlend(2D);
|
||||
ProceduralStream<Double> height = ProceduralStream.ofDouble((x, z) -> x < 0D ? 50D : 100D);
|
||||
|
||||
try (IrisRiverRuntime runtime = runtime(
|
||||
configuration,
|
||||
height,
|
||||
constantLandBiome(),
|
||||
new IrisRegion(),
|
||||
true,
|
||||
false
|
||||
)) {
|
||||
boolean transitionFound = false;
|
||||
for (int z = -2048; z <= 2048 && !transitionFound; z++) {
|
||||
IrisRiverSurfaceSample open = runtime.sample(-1, z);
|
||||
IrisRiverSurfaceSample solid = runtime.sample(0, z);
|
||||
IrisRiverTunnelSample tunnel = runtime.sampleTunnel(0, z);
|
||||
if (open.river().present()
|
||||
&& solid.river().present()
|
||||
&& open.river().reachId().equals(solid.river().reachId())
|
||||
&& open.surfaceFluid()
|
||||
&& tunnel != null) {
|
||||
assertFalse(open.subterranean());
|
||||
assertTrue(solid.subterranean());
|
||||
transitionFound = true;
|
||||
}
|
||||
}
|
||||
|
||||
assertTrue(transitionFound);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void terracedWaterUsesFixedInteriorPoolsAndPreservesNodeHeads() {
|
||||
IrisRiverNetwork configuration = configuration(false);
|
||||
@@ -635,6 +793,7 @@ public class IrisRiverRuntimeTest {
|
||||
1,
|
||||
1,
|
||||
4D,
|
||||
RiverWidthProfile.constant(4D),
|
||||
2D,
|
||||
4D,
|
||||
true,
|
||||
|
||||
@@ -13,8 +13,10 @@ import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Severity is stated by the caller and honoured by every adapter, so the levels {@link IrisLogging} offers
|
||||
@@ -24,6 +26,7 @@ import static org.mockito.Mockito.mock;
|
||||
public class IrisLoggingTest {
|
||||
private final List<LogLevel> levels = new ArrayList<>();
|
||||
private final List<String> messages = new ArrayList<>();
|
||||
private IrisPlatform capturingPlatform;
|
||||
private IrisPlatform previousPlatform;
|
||||
|
||||
@Before
|
||||
@@ -55,6 +58,34 @@ public class IrisLoggingTest {
|
||||
assertEquals("Engine init: world", messages.getFirst());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void debugFormatsArgumentsAtDebugLevel() {
|
||||
IrisLogging.debug("chunk=%d,%d", 3, 7);
|
||||
|
||||
assertEquals(List.of(LogLevel.DEBUG), levels);
|
||||
assertEquals("chunk=3,7", messages.getFirst());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void contextualReportUsesThePlatformReporterWithoutADuplicateLogLine() {
|
||||
RuntimeException failure = new RuntimeException("broken");
|
||||
|
||||
IrisLogging.reportError("Generation failed.", failure);
|
||||
|
||||
verify(capturingPlatform).reportError("Generation failed.", failure);
|
||||
assertTrue(levels.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultContextualReporterLogsTheFailureContext() {
|
||||
IrisPlatform platform = mock(IrisPlatform.class, CALLS_REAL_METHODS);
|
||||
|
||||
platform.reportError("Generation failed.", null);
|
||||
|
||||
verify(platform).log(LogLevel.ERROR, "Generation failed.");
|
||||
verify(platform).reportError((Throwable) null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void warnOnceStatesTheConditionOnceAndDropsRepeatsToDebug() {
|
||||
String key = uniqueKey();
|
||||
@@ -104,6 +135,7 @@ public class IrisLoggingTest {
|
||||
|
||||
private void bindCapturingPlatform() {
|
||||
IrisPlatform platform = mock(IrisPlatform.class);
|
||||
capturingPlatform = platform;
|
||||
doAnswer(invocation -> {
|
||||
levels.add(invocation.getArgument(0, LogLevel.class));
|
||||
messages.add(invocation.getArgument(1, String.class));
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package art.arcane.iris.util.common.scheduling;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class JRepeatingTaskLifecycleTest {
|
||||
@After
|
||||
public void clearTrackedTasks() {
|
||||
trackedTasks().clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pluginCancellationDrainsEveryTrackedRepeaterOnce() {
|
||||
AtomicInteger cancellations = new AtomicInteger();
|
||||
Map<Integer, Runnable> tracked = trackedTasks();
|
||||
tracked.put(-1, cancellations::incrementAndGet);
|
||||
tracked.put(-2, cancellations::incrementAndGet);
|
||||
|
||||
J.cancelPluginTasks();
|
||||
J.cancelPluginTasks();
|
||||
|
||||
assertEquals(2, cancellations.get());
|
||||
assertTrue(tracked.isEmpty());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Map<Integer, Runnable> trackedTasks() {
|
||||
try {
|
||||
Field field = J.class.getDeclaredField("REPEATING_CANCELLERS");
|
||||
field.setAccessible(true);
|
||||
return (Map<Integer, Runnable>) field.get(null);
|
||||
} catch (ReflectiveOperationException failure) {
|
||||
throw new AssertionError("Unable to inspect tracked Iris repeaters", failure);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user