mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-22 08:07:42 +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
+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.NativeModdedServer;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
||||
import art.arcane.volmlib.util.mantle.MantleClosedException;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.server.level.ServerChunkCache;
|
||||
@@ -16,14 +17,22 @@ import org.mockito.MockedStatic;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
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.assertThrows;
|
||||
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.eq;
|
||||
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.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
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
|
||||
public void bindFailureNoticeNamesTheLevelTheCauseAndThatNothingGenerates() {
|
||||
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.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.GenerationSessionException;
|
||||
import art.arcane.iris.generation.runtime.GenerationSessionLease;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBiomeSourceAccess;
|
||||
@@ -84,6 +85,29 @@ public class ModdedBiomePolicyTest {
|
||||
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,
|
||||
AtomicReference<Set<String>> configured) {
|
||||
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.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.GenerationClosedException;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.structure.nativegen.IrisImportedFeatureControl;
|
||||
import org.junit.Test;
|
||||
@@ -11,6 +12,9 @@ import net.minecraft.SharedConstants;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
|
||||
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.never;
|
||||
import static org.mockito.Mockito.spy;
|
||||
@@ -56,17 +60,78 @@ public class ModdedImportedFeatureStageTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void closingRuntimeDoesNotBuildFeatureTables() {
|
||||
public void aSealedRuntimeWithoutASettledTableRefusesDecorationInsteadOfSkippingFeatures() {
|
||||
NativeFeatureBiomeSource source = mock(NativeFeatureBiomeSource.class);
|
||||
when(source.packGeneration()).thenReturn(1L);
|
||||
Engine engine = mock(Engine.class);
|
||||
when(engine.getCacheID()).thenReturn(7);
|
||||
when(engine.isClosing()).thenReturn(true);
|
||||
ModdedImportedFeatureStage stage = new ModdedImportedFeatureStage(source);
|
||||
|
||||
stage.prepare(engine);
|
||||
stage.prepareWithoutWaiting(engine);
|
||||
assertThrows(GenerationClosedException.class, () -> stage.prepare(engine));
|
||||
|
||||
verify(source, never()).packGeneration();
|
||||
verify(engine, never()).getDimension();
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user