mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-25 13:30:08 +00:00
Fixed Folia spawn initialization
- Check existing spawn chunks asynchronously during world initialization - Guard delayed hydrology preparation with runtime lifecycle ownership - Preserve prefetch failure reporting during concurrent shutdown
This commit is contained in:
commit
d093ff0e41
3 files changed
+320
-35
No files matched your search
@@ -37,6 +37,7 @@ import art.arcane.iris.core.runtime.jigsaw.JigsawStudioActivation;
|
|||||||
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioSession;
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioSession;
|
||||||
import art.arcane.iris.core.service.StudioSVC;
|
import art.arcane.iris.core.service.StudioSVC;
|
||||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||||
|
import art.arcane.iris.engine.IrisComplex;
|
||||||
import art.arcane.iris.engine.IrisEngine;
|
import art.arcane.iris.engine.IrisEngine;
|
||||||
import art.arcane.iris.engine.data.cache.AtomicCache;
|
import art.arcane.iris.engine.data.cache.AtomicCache;
|
||||||
import art.arcane.iris.engine.data.chunk.TerrainChunk;
|
import art.arcane.iris.engine.data.chunk.TerrainChunk;
|
||||||
@@ -52,6 +53,7 @@ import art.arcane.iris.engine.history.GenerationAdmission;
|
|||||||
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
|
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
|
||||||
import art.arcane.iris.engine.history.IrisBoundarySignatureSampler;
|
import art.arcane.iris.engine.history.IrisBoundarySignatureSampler;
|
||||||
import art.arcane.iris.engine.history.TransitionGenerationPlan;
|
import art.arcane.iris.engine.history.TransitionGenerationPlan;
|
||||||
|
import art.arcane.iris.engine.hydrology.runtime.IrisHydrologyRuntime;
|
||||||
import art.arcane.iris.engine.object.IrisDimensionContractException;
|
import art.arcane.iris.engine.object.IrisDimensionContractException;
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
import art.arcane.iris.engine.object.IrisDimensionRuntimeContract;
|
import art.arcane.iris.engine.object.IrisDimensionRuntimeContract;
|
||||||
@@ -101,6 +103,7 @@ import java.util.Locale;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.CompletionException;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
@@ -258,41 +261,73 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Starts nearest-first hydrology planning around a new world's initial spawn. */
|
/** Starts nearest-first hydrology planning around a new world's initial spawn. */
|
||||||
private void prefetchSpawnHydrology(Engine engine, World world) {
|
private CompletableFuture<Void> prefetchSpawnHydrology(Engine engine, World world) {
|
||||||
if (usesFlatStudioTerrain()) {
|
if (usesFlatStudioTerrain()) {
|
||||||
return;
|
return CompletableFuture.completedFuture(null);
|
||||||
}
|
}
|
||||||
if (engine.getComplex() == null || engine.getComplex().getHydrologyRuntime() == null) {
|
IrisComplex complex = engine.getComplex();
|
||||||
return;
|
IrisHydrologyRuntime hydrology = complex == null ? null : complex.getHydrologyRuntime();
|
||||||
|
if (hydrology == null) {
|
||||||
|
return CompletableFuture.completedFuture(null);
|
||||||
}
|
}
|
||||||
Location spawn = getInitialSpawnLocation(world);
|
Location spawn = getInitialSpawnLocation(world);
|
||||||
int spawnX = spawn.getBlockX();
|
return requestChunkAsync(world, spawn.getBlockX() >> 4, spawn.getBlockZ() >> 4, false)
|
||||||
int spawnZ = spawn.getBlockZ();
|
.exceptionally(failure -> {
|
||||||
if (world.isChunkGenerated(spawnX >> 4, spawnZ >> 4)) {
|
if (closing || this.engine != engine || engine.isClosing() || engine.isClosed()) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
if (studio && engine.getDimension().getStudioMode() == StudioMode.NORMAL
|
throw new CompletionException(failure);
|
||||||
&& engine instanceof IrisEngine irisEngine) {
|
})
|
||||||
irisEngine.startStudioEntryHydrology(spawnX, spawnZ);
|
.thenAcceptAsync(chunk -> {
|
||||||
return;
|
if (chunk == null) {
|
||||||
}
|
startSpawnHydrology(engine, hydrology, spawn);
|
||||||
int reach = Math.max(16, hydrologyTileSize() / 2);
|
}
|
||||||
engine.getComplex().getHydrologyRuntime().prefetchArea(
|
})
|
||||||
spawnX - reach,
|
.whenComplete((ignored, failure) -> {
|
||||||
spawnZ - reach,
|
if (failure != null) {
|
||||||
spawnX + reach - 1,
|
IrisLogging.reportError("Failed to prefetch spawn hydrology for " + world.getName(), failure);
|
||||||
spawnZ + reach - 1,
|
}
|
||||||
spawnX,
|
});
|
||||||
spawnZ
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private int hydrologyTileSize() {
|
private void startSpawnHydrology(Engine expectedEngine, IrisHydrologyRuntime hydrology, Location spawn) {
|
||||||
Engine activeEngine = engine;
|
if (closing) {
|
||||||
if (activeEngine == null || activeEngine.getComplex() == null || activeEngine.getComplex().getHydrologyRuntime() == null) {
|
return;
|
||||||
return 0;
|
}
|
||||||
|
GenerationStagePermit permit;
|
||||||
|
try {
|
||||||
|
permit = acquireGenerationStage("spawn_hydrology_prefetch");
|
||||||
|
} catch (IllegalStateException failure) {
|
||||||
|
if (closing && failure.getCause() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw failure;
|
||||||
|
}
|
||||||
|
try (permit) {
|
||||||
|
if (engine != expectedEngine || expectedEngine.isClosing() || expectedEngine.isClosed() || usesFlatStudioTerrain()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
IrisComplex complex = expectedEngine.getComplex();
|
||||||
|
if (complex == null || complex.getHydrologyRuntime() != hydrology) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int spawnX = spawn.getBlockX();
|
||||||
|
int spawnZ = spawn.getBlockZ();
|
||||||
|
if (studio && expectedEngine.getDimension().getStudioMode() == StudioMode.NORMAL
|
||||||
|
&& expectedEngine instanceof IrisEngine irisEngine) {
|
||||||
|
irisEngine.startStudioEntryHydrology(spawnX, spawnZ);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int reach = Math.max(16, hydrology.settings().routing().tileSize() / 2);
|
||||||
|
hydrology.prefetchArea(
|
||||||
|
spawnX - reach,
|
||||||
|
spawnZ - reach,
|
||||||
|
spawnX + reach - 1,
|
||||||
|
spawnZ + reach - 1,
|
||||||
|
spawnX,
|
||||||
|
spawnZ
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return activeEngine.getComplex().getHydrologyRuntime().settings().routing().tileSize();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateSpawnLocation(World world) {
|
private void updateSpawnLocation(World world) {
|
||||||
|
|||||||
+20
-7
@@ -3,7 +3,9 @@ package art.arcane.iris.engine.platform;
|
|||||||
import art.arcane.iris.engine.IrisEngine;
|
import art.arcane.iris.engine.IrisEngine;
|
||||||
import art.arcane.iris.engine.IrisComplex;
|
import art.arcane.iris.engine.IrisComplex;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
|
import art.arcane.iris.engine.hydrology.HydrologyPlannerSettings;
|
||||||
import art.arcane.iris.engine.hydrology.runtime.IrisHydrologyRuntime;
|
import art.arcane.iris.engine.hydrology.runtime.IrisHydrologyRuntime;
|
||||||
|
import org.bukkit.Chunk;
|
||||||
import org.bukkit.World;
|
import org.bukkit.World;
|
||||||
import art.arcane.iris.engine.object.StudioMode;
|
import art.arcane.iris.engine.object.StudioMode;
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
@@ -14,11 +16,14 @@ import java.lang.reflect.Field;
|
|||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||||
|
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
|
||||||
import static org.mockito.Mockito.doReturn;
|
import static org.mockito.Mockito.doReturn;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
@@ -35,23 +40,31 @@ public class BukkitChunkGeneratorInitializationModeTest {
|
|||||||
Field studio = BukkitChunkGenerator.class.getDeclaredField("studio");
|
Field studio = BukkitChunkGenerator.class.getDeclaredField("studio");
|
||||||
studio.setAccessible(true);
|
studio.setAccessible(true);
|
||||||
studio.setBoolean(generator, true);
|
studio.setBoolean(generator, true);
|
||||||
|
Field loadLock = BukkitChunkGenerator.class.getDeclaredField("loadLock");
|
||||||
|
loadLock.setAccessible(true);
|
||||||
|
loadLock.set(generator, new BukkitChunkGenerator.GenerationStageGate(2, generator::isClosing));
|
||||||
doReturn(false).when(generator).usesFlatStudioTerrain();
|
doReturn(false).when(generator).usesFlatStudioTerrain();
|
||||||
World world = mock(World.class);
|
World world = mock(World.class);
|
||||||
when(world.getMinHeight()).thenReturn(-64);
|
when(world.getMinHeight()).thenReturn(-64);
|
||||||
when(world.getMaxHeight()).thenReturn(320);
|
when(world.getMaxHeight()).thenReturn(320);
|
||||||
|
when(world.getChunkAtAsync(0, 0, false)).thenReturn(CompletableFuture.completedFuture(null));
|
||||||
IrisEngine engine = mock(IrisEngine.class);
|
IrisEngine engine = mock(IrisEngine.class);
|
||||||
|
generator.setEngine(engine);
|
||||||
IrisDimension dimension = mock(IrisDimension.class);
|
IrisDimension dimension = mock(IrisDimension.class);
|
||||||
when(engine.getDimension()).thenReturn(dimension);
|
when(engine.getDimension()).thenReturn(dimension);
|
||||||
when(dimension.getStudioMode()).thenReturn(StudioMode.NORMAL);
|
when(dimension.getStudioMode()).thenReturn(StudioMode.NORMAL);
|
||||||
IrisComplex complex = mock(IrisComplex.class);
|
IrisComplex complex = mock(IrisComplex.class);
|
||||||
IrisHydrologyRuntime hydrology = mock(IrisHydrologyRuntime.class);
|
IrisHydrologyRuntime hydrology = mock(IrisHydrologyRuntime.class);
|
||||||
|
HydrologyPlannerSettings settings = mock(HydrologyPlannerSettings.class, RETURNS_DEEP_STUBS);
|
||||||
|
when(settings.routing().tileSize()).thenReturn(512);
|
||||||
|
when(hydrology.settings()).thenReturn(settings);
|
||||||
when(engine.getComplex()).thenReturn(complex);
|
when(engine.getComplex()).thenReturn(complex);
|
||||||
when(complex.getHydrologyRuntime()).thenReturn(hydrology);
|
when(complex.getHydrologyRuntime()).thenReturn(hydrology);
|
||||||
Method prefetch = BukkitChunkGenerator.class.getDeclaredMethod(
|
Method prefetch = BukkitChunkGenerator.class.getDeclaredMethod(
|
||||||
"prefetchSpawnHydrology", Engine.class, World.class);
|
"prefetchSpawnHydrology", Engine.class, World.class);
|
||||||
prefetch.setAccessible(true);
|
prefetch.setAccessible(true);
|
||||||
|
|
||||||
prefetch.invoke(generator, engine, world);
|
((CompletableFuture<?>) prefetch.invoke(generator, engine, world)).get(5L, TimeUnit.SECONDS);
|
||||||
|
|
||||||
verify(engine).startStudioEntryHydrology(0, 0);
|
verify(engine).startStudioEntryHydrology(0, 0);
|
||||||
verifyNoInteractions(hydrology);
|
verifyNoInteractions(hydrology);
|
||||||
@@ -60,21 +73,21 @@ public class BukkitChunkGeneratorInitializationModeTest {
|
|||||||
assertEquals(0.5D, generator.getInitialSpawnLocation(world).getZ(), 0D);
|
assertEquals(0.5D, generator.getInitialSpawnLocation(world).getZ(), 0D);
|
||||||
|
|
||||||
when(dimension.getStudioMode()).thenReturn(StudioMode.BIOME_BUFFET_1x1);
|
when(dimension.getStudioMode()).thenReturn(StudioMode.BIOME_BUFFET_1x1);
|
||||||
prefetch.invoke(generator, engine, world);
|
((CompletableFuture<?>) prefetch.invoke(generator, engine, world)).get(5L, TimeUnit.SECONDS);
|
||||||
verify(engine, times(1)).startStudioEntryHydrology(0, 0);
|
verify(engine, times(1)).startStudioEntryHydrology(0, 0);
|
||||||
verify(hydrology).prefetchArea(anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt());
|
verify(hydrology).prefetchArea(anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt());
|
||||||
when(dimension.getStudioMode()).thenReturn(StudioMode.NORMAL);
|
when(dimension.getStudioMode()).thenReturn(StudioMode.NORMAL);
|
||||||
|
|
||||||
IrisEngine authoring = mock(IrisEngine.class);
|
IrisEngine authoring = mock(IrisEngine.class);
|
||||||
doReturn(true).when(generator).usesFlatStudioTerrain();
|
doReturn(true).when(generator).usesFlatStudioTerrain();
|
||||||
prefetch.invoke(generator, authoring, world);
|
((CompletableFuture<?>) prefetch.invoke(generator, authoring, world)).get(5L, TimeUnit.SECONDS);
|
||||||
verifyNoInteractions(authoring);
|
verifyNoInteractions(authoring);
|
||||||
|
|
||||||
IrisEngine generated = mock(IrisEngine.class);
|
IrisEngine generated = mock(IrisEngine.class);
|
||||||
when(generated.getComplex()).thenReturn(complex);
|
when(generated.getComplex()).thenReturn(complex);
|
||||||
doReturn(false).when(generator).usesFlatStudioTerrain();
|
doReturn(false).when(generator).usesFlatStudioTerrain();
|
||||||
when(world.isChunkGenerated(0, 0)).thenReturn(true);
|
when(world.getChunkAtAsync(0, 0, false)).thenReturn(CompletableFuture.completedFuture(mock(Chunk.class)));
|
||||||
prefetch.invoke(generator, generated, world);
|
((CompletableFuture<?>) prefetch.invoke(generator, generated, world)).get(5L, TimeUnit.SECONDS);
|
||||||
verify(generated, never()).startStudioEntryHydrology(anyInt(), anyInt());
|
verify(generated, never()).startStudioEntryHydrology(anyInt(), anyInt());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,8 +112,8 @@ public class BukkitChunkGeneratorInitializationModeTest {
|
|||||||
public void authoringStartupDoesNotPrefetchTerrainHydrology() throws IOException {
|
public void authoringStartupDoesNotPrefetchTerrainHydrology() throws IOException {
|
||||||
String source = Files.readString(Path.of(
|
String source = Files.readString(Path.of(
|
||||||
"src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java"));
|
"src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java"));
|
||||||
int start = source.indexOf("private void prefetchSpawnHydrology(");
|
int start = source.indexOf("private CompletableFuture<Void> prefetchSpawnHydrology(");
|
||||||
int end = source.indexOf("private int hydrologyTileSize()", start);
|
int end = source.indexOf("private void updateSpawnLocation(", start);
|
||||||
String prefetch = source.substring(start, end);
|
String prefetch = source.substring(start, end);
|
||||||
|
|
||||||
assertTrue(prefetch.indexOf("if (usesFlatStudioTerrain())") >= 0);
|
assertTrue(prefetch.indexOf("if (usesFlatStudioTerrain())") >= 0);
|
||||||
|
|||||||
+237
@@ -0,0 +1,237 @@
|
|||||||
|
package art.arcane.iris.engine.platform;
|
||||||
|
|
||||||
|
import art.arcane.iris.engine.IrisComplex;
|
||||||
|
import art.arcane.iris.engine.IrisEngine;
|
||||||
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
|
import art.arcane.iris.engine.hydrology.HydrologyPlannerSettings;
|
||||||
|
import art.arcane.iris.engine.hydrology.runtime.IrisHydrologyRuntime;
|
||||||
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
|
import art.arcane.iris.engine.object.StudioMode;
|
||||||
|
import org.bukkit.Chunk;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||||
|
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
|
||||||
|
import static org.mockito.Mockito.doAnswer;
|
||||||
|
import static org.mockito.Mockito.doReturn;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.verifyNoInteractions;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class BukkitChunkGeneratorSpawnHydrologyTest {
|
||||||
|
@Test
|
||||||
|
public void startupWaitsAsynchronouslyWithoutHoldingGenerationAdmission() throws Exception {
|
||||||
|
Fixture fixture = new Fixture();
|
||||||
|
CompletableFuture<Void> prefetch = fixture.prefetch();
|
||||||
|
|
||||||
|
assertFalse(prefetch.isDone());
|
||||||
|
assertEquals(2, fixture.gate.availablePermits());
|
||||||
|
verifyNoInteractions(fixture.hydrology);
|
||||||
|
verify(fixture.world).getChunkAtAsync(0, 0, false);
|
||||||
|
verify(fixture.world, never()).isChunkGenerated(anyInt(), anyInt());
|
||||||
|
verify(fixture.world, never()).getChunkAtAsync(anyInt(), anyInt(), eq(true));
|
||||||
|
|
||||||
|
fixture.lookup.complete(null);
|
||||||
|
prefetch.get(5L, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
verify(fixture.hydrology).prefetchArea(-256, -256, 255, 255, 0, 0);
|
||||||
|
assertEquals(2, fixture.gate.availablePermits());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void existingSpawnDoesNotPrefetchHydrology() throws Exception {
|
||||||
|
Fixture fixture = new Fixture();
|
||||||
|
CompletableFuture<Void> prefetch = fixture.prefetch();
|
||||||
|
fixture.lookup.complete(mock(Chunk.class));
|
||||||
|
prefetch.get(5L, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
verifyNoInteractions(fixture.hydrology);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void lateLookupDoesNotEnterClosingGenerator() throws Exception {
|
||||||
|
Fixture fixture = new Fixture();
|
||||||
|
CompletableFuture<Void> prefetch = fixture.prefetch();
|
||||||
|
setField(fixture.generator, "closing", true);
|
||||||
|
fixture.lookup.complete(null);
|
||||||
|
prefetch.get(5L, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
verifyNoInteractions(fixture.hydrology);
|
||||||
|
assertEquals(2, fixture.gate.availablePermits());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void closeRejectsCompletionAlreadyWaitingForAdmission() throws Exception {
|
||||||
|
Fixture fixture = new Fixture();
|
||||||
|
CountDownLatch acquiring = new CountDownLatch(1);
|
||||||
|
doAnswer(invocation -> {
|
||||||
|
acquiring.countDown();
|
||||||
|
return invocation.callRealMethod();
|
||||||
|
}).when(fixture.generator).acquireGenerationStage(anyString());
|
||||||
|
CompletableFuture<Void> prefetch = fixture.prefetch();
|
||||||
|
fixture.gate.acquireExclusive();
|
||||||
|
try {
|
||||||
|
fixture.lookup.complete(null);
|
||||||
|
assertTrue(acquiring.await(5L, TimeUnit.SECONDS));
|
||||||
|
setField(fixture.generator, "closing", true);
|
||||||
|
} finally {
|
||||||
|
fixture.gate.releaseExclusive();
|
||||||
|
}
|
||||||
|
prefetch.get(5L, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
verifyNoInteractions(fixture.hydrology);
|
||||||
|
assertEquals(2, fixture.gate.availablePermits());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void lateLookupDoesNotEnterReplacementEngine() throws Exception {
|
||||||
|
Fixture fixture = new Fixture();
|
||||||
|
CompletableFuture<Void> prefetch = fixture.prefetch();
|
||||||
|
fixture.generator.setEngine(mock(Engine.class));
|
||||||
|
fixture.lookup.complete(null);
|
||||||
|
prefetch.get(5L, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
verifyNoInteractions(fixture.hydrology);
|
||||||
|
assertEquals(2, fixture.gate.availablePermits());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void lateLookupDoesNotEnterSealedEngine() throws Exception {
|
||||||
|
Fixture fixture = new Fixture();
|
||||||
|
CompletableFuture<Void> prefetch = fixture.prefetch();
|
||||||
|
when(fixture.engine.isClosing()).thenReturn(true);
|
||||||
|
fixture.lookup.complete(null);
|
||||||
|
prefetch.get(5L, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
verifyNoInteractions(fixture.hydrology);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void lateLookupDoesNotEnterReplacementHydrology() throws Exception {
|
||||||
|
Fixture fixture = new Fixture();
|
||||||
|
CompletableFuture<Void> prefetch = fixture.prefetch();
|
||||||
|
IrisHydrologyRuntime replacement = mock(IrisHydrologyRuntime.class);
|
||||||
|
when(fixture.complex.getHydrologyRuntime()).thenReturn(replacement);
|
||||||
|
fixture.lookup.complete(null);
|
||||||
|
prefetch.get(5L, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
verifyNoInteractions(fixture.hydrology, replacement);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void normalStudioUsesTrackedEntryPreparation() throws Exception {
|
||||||
|
Fixture fixture = new Fixture();
|
||||||
|
setField(fixture.generator, "studio", true);
|
||||||
|
CompletableFuture<Void> prefetch = fixture.prefetch();
|
||||||
|
fixture.lookup.complete(null);
|
||||||
|
prefetch.get(5L, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
verify(fixture.engine).startStudioEntryHydrology(0, 0);
|
||||||
|
verifyNoInteractions(fixture.hydrology);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void failedLookupDoesNotStartHydrology() throws Exception {
|
||||||
|
Fixture fixture = new Fixture();
|
||||||
|
CompletableFuture<Void> prefetch = fixture.prefetch();
|
||||||
|
IllegalStateException failure = new IllegalStateException("Native chunk lookup failed");
|
||||||
|
fixture.lookup.completeExceptionally(failure);
|
||||||
|
|
||||||
|
ExecutionException result = assertThrows(ExecutionException.class,
|
||||||
|
() -> prefetch.get(5L, TimeUnit.SECONDS));
|
||||||
|
assertSame(failure, result.getCause());
|
||||||
|
verifyNoInteractions(fixture.hydrology);
|
||||||
|
assertEquals(2, fixture.gate.availablePermits());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void lateLookupFailureDoesNotFailClosedGenerator() throws Exception {
|
||||||
|
Fixture fixture = new Fixture();
|
||||||
|
CompletableFuture<Void> prefetch = fixture.prefetch();
|
||||||
|
setField(fixture.generator, "closing", true);
|
||||||
|
fixture.lookup.completeExceptionally(new IllegalStateException("World unloaded"));
|
||||||
|
prefetch.get(5L, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
verifyNoInteractions(fixture.hydrology);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void admittedPlanningFailureSurvivesConcurrentClose() throws Exception {
|
||||||
|
Fixture fixture = new Fixture();
|
||||||
|
IllegalStateException failure = new IllegalStateException("Planner scheduling failed");
|
||||||
|
doAnswer(invocation -> {
|
||||||
|
setField(fixture.generator, "closing", true);
|
||||||
|
throw failure;
|
||||||
|
}).when(fixture.hydrology).prefetchArea(anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt());
|
||||||
|
CompletableFuture<Void> prefetch = fixture.prefetch();
|
||||||
|
fixture.lookup.complete(null);
|
||||||
|
|
||||||
|
ExecutionException result = assertThrows(ExecutionException.class,
|
||||||
|
() -> prefetch.get(5L, TimeUnit.SECONDS));
|
||||||
|
assertSame(failure, result.getCause());
|
||||||
|
assertEquals(2, fixture.gate.availablePermits());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setField(BukkitChunkGenerator generator, String name, Object value)
|
||||||
|
throws ReflectiveOperationException {
|
||||||
|
Field field = BukkitChunkGenerator.class.getDeclaredField(name);
|
||||||
|
field.setAccessible(true);
|
||||||
|
field.set(generator, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Fixture {
|
||||||
|
private final BukkitChunkGenerator generator = mock(BukkitChunkGenerator.class, CALLS_REAL_METHODS);
|
||||||
|
private final BukkitChunkGenerator.GenerationStageGate gate =
|
||||||
|
new BukkitChunkGenerator.GenerationStageGate(2, generator::isClosing);
|
||||||
|
private final IrisEngine engine = mock(IrisEngine.class);
|
||||||
|
private final IrisComplex complex = mock(IrisComplex.class);
|
||||||
|
private final IrisHydrologyRuntime hydrology = mock(IrisHydrologyRuntime.class);
|
||||||
|
private final World world = mock(World.class);
|
||||||
|
private final CompletableFuture<Chunk> lookup = new CompletableFuture<>();
|
||||||
|
|
||||||
|
private Fixture() throws ReflectiveOperationException {
|
||||||
|
setField(generator, "loadLock", gate);
|
||||||
|
generator.setEngine(engine);
|
||||||
|
doReturn(false).when(generator).usesFlatStudioTerrain();
|
||||||
|
when(engine.getComplex()).thenReturn(complex);
|
||||||
|
when(complex.getHydrologyRuntime()).thenReturn(hydrology);
|
||||||
|
HydrologyPlannerSettings settings = mock(HydrologyPlannerSettings.class, RETURNS_DEEP_STUBS);
|
||||||
|
when(settings.routing().tileSize()).thenReturn(512);
|
||||||
|
when(hydrology.settings()).thenReturn(settings);
|
||||||
|
IrisDimension dimension = mock(IrisDimension.class);
|
||||||
|
when(engine.getDimension()).thenReturn(dimension);
|
||||||
|
when(dimension.getStudioMode()).thenReturn(StudioMode.NORMAL);
|
||||||
|
when(world.getMinHeight()).thenReturn(-64);
|
||||||
|
when(world.getMaxHeight()).thenReturn(320);
|
||||||
|
when(world.getChunkAtAsync(0, 0, false)).thenReturn(lookup);
|
||||||
|
when(world.isChunkGenerated(anyInt(), anyInt()))
|
||||||
|
.thenThrow(new AssertionError("World initialization must not query chunk status synchronously"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private CompletableFuture<Void> prefetch() throws ReflectiveOperationException {
|
||||||
|
Method prefetch = BukkitChunkGenerator.class.getDeclaredMethod(
|
||||||
|
"prefetchSpawnHydrology", Engine.class, World.class);
|
||||||
|
prefetch.setAccessible(true);
|
||||||
|
return (CompletableFuture<Void>) prefetch.invoke(generator, engine, world);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user