mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 05:50:39 +00:00
Fail-closed modded decoration
- A generator with a refused or stopping engine refuses imported feature settings - Those settings no longer fall back to vanilla ones - Decoration fails when a sealed runtime has no settled feature table - Decoration fails when the feature table was retired before placement - Stacked initial spawns fail when the surface spawn biome lookup is rejected - Those spawns no longer fall back to the chunk's top biome - Bind failures from a stopping runtime stay out of the refusal memo and banner - The next bind after such a failure retries
This commit is contained in:
1 parent
0cb3417755
commit
96a400b585
8 files changed
+258
-51
No files matched your search
+13
-16
@@ -73,6 +73,7 @@ import art.arcane.iris.pack.loading.IrisData;
|
|||||||
import art.arcane.iris.pack.PackValidationRegistry;
|
import art.arcane.iris.pack.PackValidationRegistry;
|
||||||
import art.arcane.iris.generation.runtime.IrisEngine;
|
import art.arcane.iris.generation.runtime.IrisEngine;
|
||||||
import art.arcane.iris.generation.runtime.Engine;
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.generation.runtime.GenerationFailures;
|
||||||
import art.arcane.iris.generation.runtime.GenerationSessionException;
|
import art.arcane.iris.generation.runtime.GenerationSessionException;
|
||||||
import art.arcane.iris.generation.runtime.GenerationSessionLease;
|
import art.arcane.iris.generation.runtime.GenerationSessionLease;
|
||||||
import art.arcane.iris.structure.nativegen.NativeFeatureGenerationPolicy;
|
import art.arcane.iris.structure.nativegen.NativeFeatureGenerationPolicy;
|
||||||
@@ -517,6 +518,12 @@ public final class IrisModdedChunkGenerator implements NativeGeneratorOwner, Nat
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void recordBindFailure(NativeWorld level, Throwable error) {
|
private void recordBindFailure(NativeWorld level, Throwable error) {
|
||||||
|
// A runtime that is stopping says nothing about the world: fail this call, let the next bind try again.
|
||||||
|
if (GenerationFailures.isShutdownFailure(error)) {
|
||||||
|
ModdedIrisLog.warn("Iris bind of {} ({}) stopped with its runtime: {}", level.name(), dimensionKey,
|
||||||
|
error.toString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
engineBinding.fail(error);
|
engineBinding.fail(error);
|
||||||
for (String line : bindFailureNotice(level.name(), dimensionKey, error)) {
|
for (String line : bindFailureNotice(level.name(), dimensionKey, error)) {
|
||||||
ModdedIrisLog.error(line);
|
ModdedIrisLog.error(line);
|
||||||
@@ -588,21 +595,11 @@ public final class IrisModdedChunkGenerator implements NativeGeneratorOwner, Nat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Engine engineOrNull() {
|
/**
|
||||||
requireBindingAllowed();
|
* Null only while no level uses this generator (world creation screens, codec validation). A bound level whose
|
||||||
Engine cached = engine;
|
* engine is refused, unloading or stopping throws, so no stage can mistake it for an unbound generator.
|
||||||
requireCompletedShutdown(cached);
|
*/
|
||||||
if (cached != null && !cached.isClosed()) {
|
Engine boundEngineOrNull() {
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return engine();
|
|
||||||
} catch (Throwable ignored) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Engine structureEngineOrNull() {
|
|
||||||
requireBindingAllowed();
|
requireBindingAllowed();
|
||||||
engineBinding.throwIfFailed(dimensionKey);
|
engineBinding.throwIfFailed(dimensionKey);
|
||||||
Engine cached = engine;
|
Engine cached = engine;
|
||||||
@@ -1007,7 +1004,7 @@ public final class IrisModdedChunkGenerator implements NativeGeneratorOwner, Nat
|
|||||||
@Override
|
@Override
|
||||||
public <H, S> NativeModdedBiomePolicy<H, S> biomePolicy(NativeBiomeSourceAccess<H, S> source) {
|
public <H, S> NativeModdedBiomePolicy<H, S> biomePolicy(NativeBiomeSourceAccess<H, S> source) {
|
||||||
ModdedBiomePolicy<H, S> policy = new ModdedBiomePolicy<>(source);
|
ModdedBiomePolicy<H, S> policy = new ModdedBiomePolicy<>(source);
|
||||||
policy.bind(new ModdedBiomePolicy.RuntimeCallbacks(this::structureEngineOrNull, this::awaitStructureEngine,
|
policy.bind(new ModdedBiomePolicy.RuntimeCallbacks(this::boundEngineOrNull, this::awaitStructureEngine,
|
||||||
this::allowsGenerationHistoryBypass, this::configuredStructureBiomeKeys,
|
this::allowsGenerationHistoryBypass, this::configuredStructureBiomeKeys,
|
||||||
IrisModdedChunkGenerator::retainedBiomeKeys, this::visibleBiomeSeed));
|
IrisModdedChunkGenerator::retainedBiomeKeys, this::visibleBiomeSeed));
|
||||||
return policy;
|
return policy;
|
||||||
|
|||||||
+10
-10
@@ -266,7 +266,7 @@ final class ModdedBiomePolicy<H, S> implements NativeModdedBiomePolicy<H, S> {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public H getNoiseBiome(int quartX, int quartY, int quartZ, S sampler) {
|
public H getNoiseBiome(int quartX, int quartY, int quartZ, S sampler) {
|
||||||
Engine engine = engineOrNull();
|
Engine engine = boundEngineOrNull();
|
||||||
if (engine == null) {
|
if (engine == null) {
|
||||||
return access.serializedNoise(quartX, quartY, quartZ, sampler);
|
return access.serializedNoise(quartX, quartY, quartZ, sampler);
|
||||||
}
|
}
|
||||||
@@ -288,7 +288,7 @@ final class ModdedBiomePolicy<H, S> implements NativeModdedBiomePolicy<H, S> {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public NativeBiomeSourcePolicy.BiomeLocation<H> findClosestBiome3d(ClosestQuery<H, S> query) {
|
public NativeBiomeSourcePolicy.BiomeLocation<H> findClosestBiome3d(ClosestQuery<H, S> query) {
|
||||||
Engine engine = engineOrNull();
|
Engine engine = boundEngineOrNull();
|
||||||
if (engine == null) {
|
if (engine == null) {
|
||||||
return query.unboundFallback().get();
|
return query.unboundFallback().get();
|
||||||
}
|
}
|
||||||
@@ -355,7 +355,7 @@ final class ModdedBiomePolicy<H, S> implements NativeModdedBiomePolicy<H, S> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public H getVisibleNoiseBiome(int quartX, int quartY, int quartZ, S sampler) {
|
public H getVisibleNoiseBiome(int quartX, int quartY, int quartZ, S sampler) {
|
||||||
Engine engine = engineOrNull();
|
Engine engine = boundEngineOrNull();
|
||||||
if (engine == null) {
|
if (engine == null) {
|
||||||
return access.serializedNoise(quartX, quartY, quartZ, sampler);
|
return access.serializedNoise(quartX, quartY, quartZ, sampler);
|
||||||
}
|
}
|
||||||
@@ -411,7 +411,7 @@ final class ModdedBiomePolicy<H, S> implements NativeModdedBiomePolicy<H, S> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public H getVisibleSurfaceBiome(int blockX, int blockZ) {
|
public H getVisibleSurfaceBiome(int blockX, int blockZ) {
|
||||||
Engine engine = engineOrNull();
|
Engine engine = boundEngineOrNull();
|
||||||
if (engine == null) {
|
if (engine == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -420,15 +420,15 @@ final class ModdedBiomePolicy<H, S> implements NativeModdedBiomePolicy<H, S> {
|
|||||||
try (historyScope) {
|
try (historyScope) {
|
||||||
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_surface_spawn_biome");
|
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_surface_spawn_biome");
|
||||||
if (lease == null) {
|
if (lease == null) {
|
||||||
return null;
|
throw new IllegalStateException("Iris surface spawn biome lookup was rejected during an engine transition");
|
||||||
}
|
}
|
||||||
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
if (!isReady(engine)) {
|
if (!isReady(engine)) {
|
||||||
return null;
|
throw new IllegalStateException("Iris surface spawn biome lookup has no active engine runtime");
|
||||||
}
|
}
|
||||||
NativeBiomeSourceAccess.RegistryView<H> registry = biomeRegistry();
|
NativeBiomeSourceAccess.RegistryView<H> registry = biomeRegistry();
|
||||||
if (registry == null) {
|
if (registry == null) {
|
||||||
return null;
|
throw new IllegalStateException("Iris surface spawn biome lookup has no biome registry");
|
||||||
}
|
}
|
||||||
int quartX = ((blockX) >> 2);
|
int quartX = ((blockX) >> 2);
|
||||||
int quartZ = ((blockZ) >> 2);
|
int quartZ = ((blockZ) >> 2);
|
||||||
@@ -463,7 +463,7 @@ final class ModdedBiomePolicy<H, S> implements NativeModdedBiomePolicy<H, S> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isStructureReachable(Iterable<H> biomes) {
|
public boolean isStructureReachable(Iterable<H> biomes) {
|
||||||
Engine engine = engineOrNull();
|
Engine engine = boundEngineOrNull();
|
||||||
if (engine == null) {
|
if (engine == null) {
|
||||||
return isStructureReachable(biomes, possibleStructureBiomeKeys());
|
return isStructureReachable(biomes, possibleStructureBiomeKeys());
|
||||||
}
|
}
|
||||||
@@ -494,7 +494,7 @@ final class ModdedBiomePolicy<H, S> implements NativeModdedBiomePolicy<H, S> {
|
|||||||
int radius = query.radius();
|
int radius = query.radius();
|
||||||
S sampler = query.sampler();
|
S sampler = query.sampler();
|
||||||
int minQuartY = ((y - radius) >> 2);
|
int minQuartY = ((y - radius) >> 2);
|
||||||
Engine engine = engineOrNull();
|
Engine engine = boundEngineOrNull();
|
||||||
if (engine == null) {
|
if (engine == null) {
|
||||||
return query.fallback().get();
|
return query.fallback().get();
|
||||||
}
|
}
|
||||||
@@ -874,7 +874,7 @@ final class ModdedBiomePolicy<H, S> implements NativeModdedBiomePolicy<H, S> {
|
|||||||
return engine != null && !engine.isClosed() && engine.getComplex() != null;
|
return engine != null && !engine.isClosed() && engine.getComplex() != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Engine engineOrNull() {
|
private Engine boundEngineOrNull() {
|
||||||
RuntimeCallbacks current = runtime;
|
RuntimeCallbacks current = runtime;
|
||||||
return current == null ? null : current.currentEngine().get();
|
return current == null ? null : current.currentEngine().get();
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-8
@@ -25,6 +25,7 @@ import art.arcane.volmlib.nativelib.terrain.feature.NativeFeatureTable;
|
|||||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedImportedFeatures;
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedImportedFeatures;
|
||||||
|
|
||||||
import art.arcane.iris.generation.runtime.Engine;
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.generation.runtime.GenerationClosedException;
|
||||||
import art.arcane.iris.generation.runtime.GenerationFailures;
|
import art.arcane.iris.generation.runtime.GenerationFailures;
|
||||||
import art.arcane.iris.generation.runtime.GenerationSessionLease;
|
import art.arcane.iris.generation.runtime.GenerationSessionLease;
|
||||||
import art.arcane.iris.structure.nativegen.NativeFeatureGenerationPolicy;
|
import art.arcane.iris.structure.nativegen.NativeFeatureGenerationPolicy;
|
||||||
@@ -96,10 +97,12 @@ final class ModdedImportedFeatureStage implements NativeModdedGeneratorPolicy.Fe
|
|||||||
* the per-step feature lists and {@code BiomeFilter}'s hasFeature gate both see real features for a biome
|
* the per-step feature lists and {@code BiomeFilter}'s hasFeature gate both see real features for a biome
|
||||||
* whose datapack JSON declares none by design. Real registry biomes pass straight through.
|
* whose datapack JSON declares none by design. Real registry biomes pass straight through.
|
||||||
*
|
*
|
||||||
* <p>With {@code importedFeatures} disabled there is no table and this is vanilla's default getter.
|
* <p>With {@code importedFeatures} disabled there is no table and this is vanilla's default getter. So is an
|
||||||
|
* unbound generator, which only world creation and codec validation ask. A bound generator whose engine is
|
||||||
|
* refused or stopping throws: BiomeFilter would otherwise drop every imported feature of a custom biome.
|
||||||
*/
|
*/
|
||||||
public NativeFeatureTable currentTable() {
|
public NativeFeatureTable currentTable() {
|
||||||
Engine engine = generator == null ? null : generator.engineOrNull();
|
Engine engine = generator == null ? null : generator.boundEngineOrNull();
|
||||||
return engine == null ? null : featureTables.get(engine.getCacheID());
|
return engine == null ? null : featureTables.get(engine.getCacheID());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,8 +115,15 @@ final class ModdedImportedFeatureStage implements NativeModdedGeneratorPolicy.Fe
|
|||||||
* the build itself is serialized because {@code applyBiomeDecoration} calls this from every worldgen
|
* the build itself is serialized because {@code applyBiomeDecoration} calls this from every worldgen
|
||||||
* thread, and two threads that both found the stage unprepared would each run {@code FeatureSorter}, whose
|
* thread, and two threads that both found the stage unprepared would each run {@code FeatureSorter}, whose
|
||||||
* cycle detection is the expensive part. Waiting here is safe: this caller holds no generator monitor.
|
* cycle detection is the expensive part. Waiting here is safe: this caller holds no generator monitor.
|
||||||
|
* A sealed runtime keeps the table it already settled but cannot build one, and decorating without it
|
||||||
|
* would persist a chunk with no imported features, so it fails the chunk instead.
|
||||||
*/
|
*/
|
||||||
public void prepare(Engine engine) {
|
public void prepare(Engine engine) {
|
||||||
|
if ((engine.isClosed() || engine.isClosing())
|
||||||
|
&& !settled(engine.getCacheID(), biomeSource.packGeneration())) {
|
||||||
|
throw new GenerationClosedException("Iris imported features cannot prepare runtime "
|
||||||
|
+ engine.getCacheID() + " while its engine is closing.");
|
||||||
|
}
|
||||||
prepare(engine, true);
|
prepare(engine, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +138,7 @@ final class ModdedImportedFeatureStage implements NativeModdedGeneratorPolicy.Fe
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void prepare(Engine engine, boolean waitForBuild) {
|
private void prepare(Engine engine, boolean waitForBuild) {
|
||||||
if (engine == null || engine.isClosed() || engine.isClosing()) {
|
if (engine.isClosed() || engine.isClosing()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int runtimeIdentity = engine.getCacheID();
|
int runtimeIdentity = engine.getCacheID();
|
||||||
@@ -204,17 +214,24 @@ final class ModdedImportedFeatureStage implements NativeModdedGeneratorPolicy.Fe
|
|||||||
inertGenerations.put(runtimeIdentity, generation);
|
inertGenerations.put(runtimeIdentity, generation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Null only for a runtime whose features are settled off. A table retired or superseded since this chunk's
|
||||||
|
* prepare fails the chunk: skipping the pass would persist it without imported features.
|
||||||
|
*/
|
||||||
public NativeFeatureTable placementTable(Engine engine) {
|
public NativeFeatureTable placementTable(Engine engine) {
|
||||||
int runtimeIdentity = engine.getCacheID();
|
int runtimeIdentity = engine.getCacheID();
|
||||||
|
long generation = biomeSource.packGeneration();
|
||||||
NativeFeatureTable table = featureTables.get(runtimeIdentity);
|
NativeFeatureTable table = featureTables.get(runtimeIdentity);
|
||||||
if (table == null) {
|
if (table == null && Long.valueOf(generation).equals(inertGenerations.get(runtimeIdentity))) {
|
||||||
WorldCheckFeaturePlacement.recordFeaturesOff();
|
WorldCheckFeaturePlacement.recordFeaturesOff();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (table.generation() != biomeSource.packGeneration()) {
|
if (table == null || table.generation() != generation) {
|
||||||
featureTables.remove(runtimeIdentity, table);
|
if (table != null) {
|
||||||
inertGenerations.remove(runtimeIdentity);
|
featureTables.remove(runtimeIdentity, table);
|
||||||
return null;
|
}
|
||||||
|
throw new GenerationClosedException("Iris imported feature table for runtime " + runtimeIdentity
|
||||||
|
+ " was retired before decoration.");
|
||||||
}
|
}
|
||||||
return generator == null ? null : table;
|
return generator == null ? null : table;
|
||||||
}
|
}
|
||||||
|
|||||||
+65
@@ -4,6 +4,7 @@ import art.arcane.iris.generation.runtime.Engine;
|
|||||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedChunkGenerator;
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedChunkGenerator;
|
||||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
|
||||||
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
||||||
|
import art.arcane.volmlib.util.mantle.MantleClosedException;
|
||||||
import net.minecraft.SharedConstants;
|
import net.minecraft.SharedConstants;
|
||||||
import net.minecraft.server.Bootstrap;
|
import net.minecraft.server.Bootstrap;
|
||||||
import net.minecraft.server.level.ServerChunkCache;
|
import net.minecraft.server.level.ServerChunkCache;
|
||||||
@@ -16,14 +17,22 @@ import org.mockito.MockedStatic;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertNotSame;
|
||||||
import static org.junit.Assert.assertSame;
|
import static org.junit.Assert.assertSame;
|
||||||
import static org.junit.Assert.assertThrows;
|
import static org.junit.Assert.assertThrows;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyLong;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
import static org.mockito.ArgumentMatchers.argThat;
|
import static org.mockito.ArgumentMatchers.argThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||||
|
import static org.mockito.Mockito.doNothing;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.mockStatic;
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
@@ -63,6 +72,62 @@ public class ModdedBindFailureTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A bind that fails because the runtime is stopping (level unload, quit to title, server stop) says nothing about
|
||||||
|
* the world: it fails the chunk that asked, but the next bind tries again and no refusal is announced.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void aShutdownFailureDuringBindIsNeitherRememberedNorAnnouncedAsARefusal() throws Exception {
|
||||||
|
IrisModdedChunkGenerator generator = generator("overworld:overworld");
|
||||||
|
NativeWorld level = level(generator, "minecraft:overworld");
|
||||||
|
field("boundLevel").set(generator, level);
|
||||||
|
NativeModdedServer server = mock(NativeModdedServer.class);
|
||||||
|
when(server.generateStructures()).thenReturn(true);
|
||||||
|
MantleClosedException closed = new MantleClosedException("Tectonic Plate is closed!");
|
||||||
|
|
||||||
|
try (MockedStatic<NativeModdedServer> servers = mockStatic(NativeModdedServer.class);
|
||||||
|
MockedStatic<ModdedWorldEngines> engines = mockStatic(ModdedWorldEngines.class);
|
||||||
|
MockedStatic<ModdedIrisLog> log = mockStatic(ModdedIrisLog.class)) {
|
||||||
|
servers.when(() -> NativeModdedServer.forWorld(level)).thenReturn(server);
|
||||||
|
engines.when(() -> ModdedWorldEngines.get(eq(level), any(), any(), anyLong(), any())).thenThrow(closed);
|
||||||
|
|
||||||
|
assertSame(closed, assertThrows(MantleClosedException.class, generator::engine));
|
||||||
|
assertSame(closed, assertThrows(MantleClosedException.class, generator::engine));
|
||||||
|
|
||||||
|
engines.verify(() -> ModdedWorldEngines.get(eq(level), any(), any(), anyLong(), any()), times(2));
|
||||||
|
log.verify(() -> ModdedIrisLog.error(anyString()), never());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unloading a level resets its binding, so a refusal recorded in one load never carries into the next load of the
|
||||||
|
* same generator (a runtime dimension removal that rolls back, or a level loaded again in the same JVM).
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void aRecordedRefusalDoesNotOutliveTheUnloadOfItsLevel() throws Exception {
|
||||||
|
IrisModdedChunkGenerator generator = generator("overworld:overworld");
|
||||||
|
NativeWorld level = level(generator, "minecraft:overworld");
|
||||||
|
doNothing().when((NativeModdedChunkGenerator<?, ?, ?>) field("nativeGenerator").get(generator)).clearCaches();
|
||||||
|
field("importedFeatures").set(generator, mock(ModdedImportedFeatureStage.class));
|
||||||
|
field("announced").set(generator, new AtomicBoolean());
|
||||||
|
NativeModdedServer server = mock(NativeModdedServer.class);
|
||||||
|
when(server.generateStructures()).thenReturn(false);
|
||||||
|
|
||||||
|
try (MockedStatic<NativeModdedServer> servers = mockStatic(NativeModdedServer.class);
|
||||||
|
MockedStatic<ModdedWorldEngines> engines = mockStatic(ModdedWorldEngines.class);
|
||||||
|
MockedStatic<ModdedIrisLog> log = mockStatic(ModdedIrisLog.class)) {
|
||||||
|
servers.when(() -> NativeModdedServer.forWorld(level)).thenReturn(server);
|
||||||
|
|
||||||
|
IllegalStateException first = assertThrows(IllegalStateException.class, () -> generator.bindLevel(level));
|
||||||
|
generator.unbindEngine(level);
|
||||||
|
IllegalStateException second = assertThrows(IllegalStateException.class, () -> generator.bindLevel(level));
|
||||||
|
|
||||||
|
assertNotSame(first, second.getCause());
|
||||||
|
servers.verify(() -> NativeModdedServer.forWorld(level), times(2));
|
||||||
|
engines.verify(() -> ModdedWorldEngines.evictOrThrow(level));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void bindFailureNoticeNamesTheLevelTheCauseAndThatNothingGenerates() {
|
public void bindFailureNoticeNamesTheLevelTheCauseAndThatNothingGenerates() {
|
||||||
String notice = String.join("\n", IrisModdedChunkGenerator.bindFailureNotice(
|
String notice = String.join("\n", IrisModdedChunkGenerator.bindFailureNotice(
|
||||||
|
|||||||
+24
@@ -2,6 +2,7 @@ package art.arcane.iris.modded;
|
|||||||
|
|
||||||
import art.arcane.iris.generation.biome.IrisBiome;
|
import art.arcane.iris.generation.biome.IrisBiome;
|
||||||
import art.arcane.iris.generation.runtime.Engine;
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.generation.runtime.GenerationSessionException;
|
||||||
import art.arcane.iris.generation.runtime.GenerationSessionLease;
|
import art.arcane.iris.generation.runtime.GenerationSessionLease;
|
||||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||||
import art.arcane.volmlib.nativelib.terrain.NativeBiomeSourceAccess;
|
import art.arcane.volmlib.nativelib.terrain.NativeBiomeSourceAccess;
|
||||||
@@ -84,6 +85,29 @@ public class ModdedBiomePolicyTest {
|
|||||||
assertSame(forest, policy.requiredStructureBiome(6000, -30, -6000, null));
|
assertSame(forest, policy.requiredStructureBiome(6000, -30, -6000, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stacked initial mob spawning asks for the visible surface biome. A null answer made it spawn from the chunk's
|
||||||
|
* top biome instead, so a sealed or unready engine has to fail the chunk.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void surfaceSpawnBiomeRefusesAnEngineInTransitionInsteadOfAnsweringNothing() throws Exception {
|
||||||
|
Engine sealed = mock(Engine.class);
|
||||||
|
when(sealed.isClosing()).thenReturn(true);
|
||||||
|
when(sealed.acquireGenerationLease(anyString())).thenThrow(new GenerationSessionException("sealed", true));
|
||||||
|
Engine unready = mock(Engine.class);
|
||||||
|
when(unready.acquireGenerationLease(anyString())).thenReturn(GenerationSessionLease.noop());
|
||||||
|
|
||||||
|
assertThrows(IllegalStateException.class, () -> boundTo(sealed).getVisibleSurfaceBiome(8, 8));
|
||||||
|
assertThrows(IllegalStateException.class, () -> boundTo(unready).getVisibleSurfaceBiome(8, 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ModdedBiomePolicy<BiomeToken, Object> boundTo(Engine engine) {
|
||||||
|
ModdedBiomePolicy<BiomeToken, Object> policy = new ModdedBiomePolicy<>(new Access(new ArrayList<>()));
|
||||||
|
policy.bind(new ModdedBiomePolicy.RuntimeCallbacks(() -> engine, () -> engine, bound -> true,
|
||||||
|
Set::of, bound -> Set.of(), () -> 0L));
|
||||||
|
return policy;
|
||||||
|
}
|
||||||
|
|
||||||
private static ModdedBiomePolicy<BiomeToken, Object> policy(List<BiomeToken> entries,
|
private static ModdedBiomePolicy<BiomeToken, Object> policy(List<BiomeToken> entries,
|
||||||
AtomicReference<Set<String>> configured) {
|
AtomicReference<Set<String>> configured) {
|
||||||
ModdedBiomePolicy<BiomeToken, Object> policy = new ModdedBiomePolicy<>(new Access(entries));
|
ModdedBiomePolicy<BiomeToken, Object> policy = new ModdedBiomePolicy<>(new Access(entries));
|
||||||
|
|||||||
+68
-3
@@ -3,6 +3,7 @@ package art.arcane.iris.modded;
|
|||||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeFeatureBiomeSource;
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeFeatureBiomeSource;
|
||||||
|
|
||||||
import art.arcane.iris.generation.runtime.Engine;
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.generation.runtime.GenerationClosedException;
|
||||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||||
import art.arcane.iris.structure.nativegen.IrisImportedFeatureControl;
|
import art.arcane.iris.structure.nativegen.IrisImportedFeatureControl;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@@ -11,6 +12,9 @@ import net.minecraft.SharedConstants;
|
|||||||
import net.minecraft.server.Bootstrap;
|
import net.minecraft.server.Bootstrap;
|
||||||
|
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.spy;
|
import static org.mockito.Mockito.spy;
|
||||||
@@ -56,17 +60,78 @@ public class ModdedImportedFeatureStageTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void closingRuntimeDoesNotBuildFeatureTables() {
|
public void aSealedRuntimeWithoutASettledTableRefusesDecorationInsteadOfSkippingFeatures() {
|
||||||
NativeFeatureBiomeSource source = mock(NativeFeatureBiomeSource.class);
|
NativeFeatureBiomeSource source = mock(NativeFeatureBiomeSource.class);
|
||||||
|
when(source.packGeneration()).thenReturn(1L);
|
||||||
Engine engine = mock(Engine.class);
|
Engine engine = mock(Engine.class);
|
||||||
|
when(engine.getCacheID()).thenReturn(7);
|
||||||
when(engine.isClosing()).thenReturn(true);
|
when(engine.isClosing()).thenReturn(true);
|
||||||
ModdedImportedFeatureStage stage = new ModdedImportedFeatureStage(source);
|
ModdedImportedFeatureStage stage = new ModdedImportedFeatureStage(source);
|
||||||
|
|
||||||
stage.prepare(engine);
|
|
||||||
stage.prepareWithoutWaiting(engine);
|
stage.prepareWithoutWaiting(engine);
|
||||||
|
assertThrows(GenerationClosedException.class, () -> stage.prepare(engine));
|
||||||
|
|
||||||
verify(source, never()).packGeneration();
|
|
||||||
verify(engine, never()).getDimension();
|
verify(engine, never()).getDimension();
|
||||||
assertFalse(stage.active());
|
assertFalse(stage.active());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void aSealedRuntimeKeepsDecoratingWithTheTableItAlreadySettled() {
|
||||||
|
NativeFeatureBiomeSource source = mock(NativeFeatureBiomeSource.class);
|
||||||
|
when(source.packGeneration()).thenReturn(1L);
|
||||||
|
Engine engine = disabledFeatures(7);
|
||||||
|
ModdedImportedFeatureStage stage = new ModdedImportedFeatureStage(source);
|
||||||
|
stage.bind(mock(IrisModdedChunkGenerator.class));
|
||||||
|
stage.prepare(engine);
|
||||||
|
|
||||||
|
when(engine.isClosing()).thenReturn(true);
|
||||||
|
stage.prepare(engine);
|
||||||
|
|
||||||
|
assertNull(stage.placementTable(engine));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void aTableRetiredBetweenPrepareAndPlacementFailsTheChunk() {
|
||||||
|
NativeFeatureBiomeSource source = mock(NativeFeatureBiomeSource.class);
|
||||||
|
when(source.packGeneration()).thenReturn(1L);
|
||||||
|
Engine engine = disabledFeatures(7);
|
||||||
|
ModdedImportedFeatureStage stage = new ModdedImportedFeatureStage(source);
|
||||||
|
stage.bind(mock(IrisModdedChunkGenerator.class));
|
||||||
|
|
||||||
|
stage.prepare(engine);
|
||||||
|
stage.evictRuntime(7);
|
||||||
|
assertThrows(GenerationClosedException.class, () -> stage.placementTable(engine));
|
||||||
|
|
||||||
|
stage.prepare(engine);
|
||||||
|
when(source.packGeneration()).thenReturn(2L);
|
||||||
|
assertThrows(GenerationClosedException.class, () -> stage.placementTable(engine));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void anUnboundGeneratorAnswersVanillaGenerationSettings() {
|
||||||
|
IrisModdedChunkGenerator generator = mock(IrisModdedChunkGenerator.class);
|
||||||
|
when(generator.boundEngineOrNull()).thenReturn(null);
|
||||||
|
ModdedImportedFeatureStage stage = new ModdedImportedFeatureStage(mock(NativeFeatureBiomeSource.class));
|
||||||
|
stage.bind(generator);
|
||||||
|
|
||||||
|
assertNull(stage.currentTable());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void aFailedBindFailsTheGenerationSettingsInsteadOfDroppingImportedFeatures() {
|
||||||
|
IllegalStateException refused = new IllegalStateException("Iris generator 'overworld:overworld' failed to bind");
|
||||||
|
IrisModdedChunkGenerator generator = mock(IrisModdedChunkGenerator.class);
|
||||||
|
when(generator.boundEngineOrNull()).thenThrow(refused);
|
||||||
|
ModdedImportedFeatureStage stage = new ModdedImportedFeatureStage(mock(NativeFeatureBiomeSource.class));
|
||||||
|
stage.bind(generator);
|
||||||
|
|
||||||
|
assertSame(refused, assertThrows(IllegalStateException.class, stage::currentTable));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Engine disabledFeatures(int runtimeIdentity) {
|
||||||
|
Engine engine = mock(Engine.class);
|
||||||
|
when(engine.getDimension()).thenReturn(new IrisDimension().setImportedFeatures(new IrisImportedFeatureControl()));
|
||||||
|
when(engine.getCacheID()).thenReturn(runtimeIdentity);
|
||||||
|
return engine;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,7 @@ import java.nio.channels.ClosedChannelException;
|
|||||||
import java.util.concurrent.CancellationException;
|
import java.util.concurrent.CancellationException;
|
||||||
import java.util.concurrent.RejectedExecutionException;
|
import java.util.concurrent.RejectedExecutionException;
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Separates failures of the engine, its mantle, generation history, executors or the JVM from failures of one
|
* Separates failures of the engine, its mantle, generation history, executors or the JVM from failures of one
|
||||||
@@ -22,22 +23,32 @@ public final class GenerationFailures {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isEngineFailure(Throwable failure) {
|
public static boolean isEngineFailure(Throwable failure) {
|
||||||
if (Thread.currentThread().isInterrupted()) {
|
return isShutdownFailure(failure) || hasCause(failure, (Throwable cause) -> cause instanceof Error
|
||||||
return true;
|
|| cause instanceof WrongEngineBroException
|
||||||
}
|
|| cause instanceof SavedBiomeUnavailableException unavailable && unavailable.isLoading()
|
||||||
|
|| cause instanceof TimeoutException);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The engine failures that only mean a runtime, pool or thread is stopping. They say nothing about the world, so
|
||||||
|
* nothing may remember them against it once the next runtime is up.
|
||||||
|
*/
|
||||||
|
public static boolean isShutdownFailure(Throwable failure) {
|
||||||
|
return Thread.currentThread().isInterrupted() || hasCause(failure, (Throwable cause) ->
|
||||||
|
cause instanceof GenerationClosedException
|
||||||
|
|| cause instanceof MantleClosedException
|
||||||
|
|| cause instanceof GenerationSessionException session && session.isExpectedTeardown()
|
||||||
|
|| cause instanceof InterruptedException
|
||||||
|
|| cause instanceof InterruptedIOException
|
||||||
|
|| cause instanceof ClosedChannelException
|
||||||
|
|| cause instanceof RejectedExecutionException
|
||||||
|
|| cause instanceof CancellationException);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean hasCause(Throwable failure, Predicate<Throwable> match) {
|
||||||
Throwable current = failure;
|
Throwable current = failure;
|
||||||
for (int depth = 0; current != null && depth < MAXIMUM_CAUSE_DEPTH; depth++, current = current.getCause()) {
|
for (int depth = 0; current != null && depth < MAXIMUM_CAUSE_DEPTH; depth++, current = current.getCause()) {
|
||||||
if (current instanceof Error
|
if (match.test(current)) {
|
||||||
|| current instanceof GenerationClosedException
|
|
||||||
|| current instanceof MantleClosedException
|
|
||||||
|| current instanceof WrongEngineBroException
|
|
||||||
|| current instanceof SavedBiomeUnavailableException unavailable && unavailable.isLoading()
|
|
||||||
|| current instanceof InterruptedException
|
|
||||||
|| current instanceof InterruptedIOException
|
|
||||||
|| current instanceof ClosedChannelException
|
|
||||||
|| current instanceof RejectedExecutionException
|
|
||||||
|| current instanceof CancellationException
|
|
||||||
|| current instanceof TimeoutException) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import art.arcane.iris.world.history.SavedBiomeUnavailableException;
|
|||||||
import art.arcane.volmlib.util.mantle.MantleClosedException;
|
import art.arcane.volmlib.util.mantle.MantleClosedException;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.nio.channels.ClosedByInterruptException;
|
import java.nio.channels.ClosedByInterruptException;
|
||||||
import java.util.concurrent.CompletionException;
|
import java.util.concurrent.CompletionException;
|
||||||
import java.util.concurrent.RejectedExecutionException;
|
import java.util.concurrent.RejectedExecutionException;
|
||||||
@@ -52,6 +53,33 @@ public class GenerationFailuresTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shutdownFailuresAreTheLifecycleSubsetOfEngineFailures() {
|
||||||
|
assertTrue(GenerationFailures.isShutdownFailure(new GenerationClosedException("router closed")));
|
||||||
|
assertTrue(GenerationFailures.isShutdownFailure(new MantleClosedException("Tectonic Plate is closed!")));
|
||||||
|
assertTrue(GenerationFailures.isShutdownFailure(new RejectedExecutionException("pool shut down")));
|
||||||
|
assertTrue(GenerationFailures.isShutdownFailure(new ClosedByInterruptException()));
|
||||||
|
assertTrue(GenerationFailures.isShutdownFailure(new GenerationSessionException("sealed", true)));
|
||||||
|
assertTrue(GenerationFailures.isShutdownFailure(new IllegalStateException("bind failed",
|
||||||
|
new CompletionException(new InterruptedException()))));
|
||||||
|
|
||||||
|
assertFalse(GenerationFailures.isShutdownFailure(new OutOfMemoryError()));
|
||||||
|
assertFalse(GenerationFailures.isShutdownFailure(new NoClassDefFoundError("art/arcane/Missing")));
|
||||||
|
assertFalse(GenerationFailures.isShutdownFailure(new GenerationSessionException("wrong engine")));
|
||||||
|
assertFalse(GenerationFailures.isShutdownFailure(new IllegalStateException(
|
||||||
|
"Iris generation history is unusable", new IOException("registry definition changed"))));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void anyFailureOnAnInterruptedThreadIsAShutdownFailure() {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
try {
|
||||||
|
assertTrue(GenerationFailures.isShutdownFailure(new IllegalStateException("history unusable")));
|
||||||
|
} finally {
|
||||||
|
Thread.interrupted();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void rethrowPropagatesEngineFailuresAndReturnsForContentFailures() {
|
public void rethrowPropagatesEngineFailuresAndReturnsForContentFailures() {
|
||||||
GenerationClosedException closed = new GenerationClosedException("router closed");
|
GenerationClosedException closed = new GenerationClosedException("router closed");
|
||||||
|
|||||||
Reference in new issue
Block a user