Merged biome retention

- Saved biome behavior is integrated with current Iris changes
This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-05 20:30:52 -04:00
commit 80843114fc
57 files changed
+4460 -385

No files matched your search

@@ -64,12 +64,26 @@ public class WorldReplacementBootstrapTest {
}
@Test
public void rejectsNestedMetadataAddedAfterImmutableEpochPublication() throws Exception {
public void acceptsFinderMetadataAddedAfterImmutableEpochPublication() throws Exception {
Transaction transaction = stagedTransaction(Phase.ARMED, true, "original");
configureReplacement(transaction);
Path dimensions = activePackRoot(paths(transaction).stage()).resolve("dimensions");
Files.writeString(dimensions.resolve(".DS_Store"), "Finder metadata");
WorldReplacementBootstrap.ReconcileResult result = reconcile();
assertEquals(1, result.published());
assertEquals("replacement", replacementContent(target.worldDirectory()));
assertEquals(Phase.PUBLISHED, loadSingle().phase());
}
@Test
public void rejectsNestedResourceAddedAfterImmutableEpochPublication() throws Exception {
Transaction transaction = stagedTransaction(Phase.ARMED, true, "original");
configureReplacement(transaction);
Path dimensions = activePackRoot(paths(transaction).stage()).resolve("dimensions");
Files.writeString(dimensions.resolve(".hidden-resource"), "changed resource");
assertThrows(IOException.class, this::reconcile);
assertTrue(Files.isDirectory(paths(transaction).stage()));
@@ -3,6 +3,12 @@ package art.arcane.iris.engine;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.GenerationRuntime.BiomeMaxes;
import art.arcane.iris.engine.framework.EngineEffects;
import art.arcane.iris.engine.framework.BiomeEnvironment;
import art.arcane.iris.engine.history.SavedBiomeRuntime;
import art.arcane.iris.engine.history.SavedBiomeUnavailableException;
import art.arcane.iris.engine.object.IrisRegion;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import art.arcane.iris.engine.framework.EngineMode;
import art.arcane.iris.engine.framework.EngineStage;
import art.arcane.iris.engine.framework.EngineTarget;
@@ -59,6 +65,7 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
public class IrisEngineGenerationRuntimeScopeTest {
@@ -86,6 +93,7 @@ public class IrisEngineGenerationRuntimeScopeTest {
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
IrisEngine.GenerationRuntimeBinding binding = detachedBinding(engine, selected.runtime);
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
when(router.biomes()).thenReturn(mock(SavedBiomeRuntime.class));
setField(engine, "generationHistoryRuntimeRouter", router);
doReturn(null).when(engine).getDimensionStackContext();
doReturn(false).when(engine).answersFromNaturalTerrain(anyInt(), anyInt());
@@ -128,6 +136,7 @@ public class IrisEngineGenerationRuntimeScopeTest {
RuntimeFixture selected = runtime(2, 2D, 2D, 2D);
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
when(router.biomes()).thenReturn(mock(SavedBiomeRuntime.class));
setField(engine, "generationHistoryRuntimeRouter", router);
ProceduralStream<IrisBiome> caves = mock(ProceduralStream.class);
when(active.complex.getCaveBiomeStream()).thenReturn(caves);
@@ -149,6 +158,82 @@ public class IrisEngineGenerationRuntimeScopeTest {
verify(router, times(3)).openCoordinateScope(19, -3);
}
@Test
@SuppressWarnings("unchecked")
public void unboundContentContextSkipsSavedBiomesAndSelectsTheCoordinateRuntime() throws Exception {
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
RuntimeFixture selected = runtime(2, 2D, 2D, 2D);
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
IrisEngine.GenerationRuntimeBinding binding = detachedBinding(engine, selected.runtime);
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
SavedBiomeRuntime saved = mock(SavedBiomeRuntime.class);
when(router.biomes()).thenReturn(saved);
setField(engine, "generationHistoryRuntimeRouter", router);
doReturn(null).when(engine).getDimensionStackContext();
doReturn(false).when(engine).answersFromNaturalTerrain(anyInt(), anyInt());
ChunkContext context = mock(ChunkContext.class);
when(context.getComplex()).thenReturn(active.complex);
when(context.isNaturalTerrain()).thenReturn(false);
IrisBiome cave = mock(IrisBiome.class);
IrisBiome surface = mock(IrisBiome.class);
IrisRegion region = mock(IrisRegion.class);
ProceduralStream<IrisBiome> caves = mock(ProceduralStream.class);
ProceduralStream<IrisBiome> surfaces = mock(ProceduralStream.class);
ProceduralStream<IrisRegion> regions = mock(ProceduralStream.class);
when(selected.complex.getCaveBiomeStream()).thenReturn(caves);
when(selected.complex.getTrueBiomeStream()).thenReturn(surfaces);
when(selected.complex.getRegionStream()).thenReturn(regions);
when(caves.get(19D, -3D)).thenReturn(cave);
when(surfaces.get(19D, -3D)).thenReturn(surface);
when(regions.get(19D, -3D)).thenReturn(region);
when(router.openCoordinateScope(19, -3)).thenAnswer(invocation -> {
assertSame(active.complex, engine.getComplex());
IrisEngine.GenerationRuntimeScope runtimeScope = engine.openGenerationRuntimeScope(binding);
GenerationHistoryRuntimeRouter.CoordinateScope coordinateScope = mock(GenerationHistoryRuntimeRouter.CoordinateScope.class);
doAnswer(close -> {
runtimeScope.close();
return null;
}).when(coordinateScope).close();
return coordinateScope;
});
try (IrisContext.Scope ignored = IrisContext.open(engine, 73L, context)) {
assertSame(cave, engine.getCaveBiome(19, -3));
assertSame(surface, engine.getSurfaceBiome(19, -3));
assertSame(region, engine.getRegion(19, -3));
assertSame(region, engine.getRegion(19, 20, -3));
assertSame(active.complex, engine.getComplex());
}
verify(router, times(4)).openCoordinateScope(19, -3);
verifyNoInteractions(saved);
assertNull(IrisContext.get());
}
@Test
public void gameplayContextWithoutAChunkStillReadsSavedBiomes() throws Exception {
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
when(active.target.getWorld().minHeight()).thenReturn(-64);
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
SavedBiomeRuntime saved = mock(SavedBiomeRuntime.class);
when(router.biomes()).thenReturn(saved);
setField(engine, "generationHistoryRuntimeRouter", router);
BiomeEnvironment environment = new BiomeEnvironment(4L, mock(IrisBiome.class),
mock(IrisRegion.class), mock(IrisDimension.class), mock(IrisData.class));
when(saved.resolve(19, -64, -3, true)).thenReturn(Optional.of(environment));
when(saved.resolveCaveBase(19, -3)).thenReturn(Optional.of(environment));
try (IrisContext.Scope ignored = IrisContext.open(engine, 0L, null)) {
assertSame(environment.biome(), engine.getCaveBiome(19, -3));
assertSame(environment.biome(), engine.getSurfaceBiome(19, -3));
assertSame(environment.region(), engine.getRegion(19, -3));
}
verify(saved).resolveCaveBase(19, -3);
verify(saved, times(2)).resolve(19, -64, -3, true);
verify(router, never()).openCoordinateScope(anyInt(), anyInt());
assertNull(IrisContext.get());
}
@Test
public void scopedGettersResolveDetachedGenerationStateAndWorldServicesStayActive() throws Exception {
RuntimeFixture active = runtime(1, 1D, 2D, 3D);
@@ -426,6 +511,7 @@ public class IrisEngineGenerationRuntimeScopeTest {
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
when(router.biomes()).thenReturn(mock(SavedBiomeRuntime.class));
when(router.engine()).thenReturn(engine);
engine.attachGenerationHistoryRuntimeRouter(router);
@@ -441,6 +527,7 @@ public class IrisEngineGenerationRuntimeScopeTest {
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
when(router.biomes()).thenReturn(mock(SavedBiomeRuntime.class));
when(router.engine()).thenReturn(engine);
engine.attachGenerationHistoryRuntimeRouter(router);
@@ -457,6 +544,7 @@ public class IrisEngineGenerationRuntimeScopeTest {
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
assertNull(engine.openGenerationHistoryCoordinateScope(0, 0));
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
when(router.biomes()).thenReturn(mock(SavedBiomeRuntime.class));
when(router.engine()).thenReturn(engine);
engine.attachGenerationHistoryRuntimeRouter(router);
engine.detachGenerationHistoryRuntimeRouter(router);
@@ -540,6 +628,83 @@ public class IrisEngineGenerationRuntimeScopeTest {
verify(detached.data).close();
}
@Test
public void savedBiomesAnswerWithoutOpeningGenerationRoutes() throws Exception {
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
when(active.target.getWorld().minHeight()).thenReturn(-64);
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
SavedBiomeRuntime saved = mock(SavedBiomeRuntime.class);
when(router.biomes()).thenReturn(saved);
setField(engine, "generationHistoryRuntimeRouter", router);
BiomeEnvironment environment = new BiomeEnvironment(4L, mock(IrisBiome.class),
mock(IrisRegion.class), mock(IrisDimension.class), mock(IrisData.class));
when(saved.resolve(19, -44, -3, false)).thenReturn(Optional.of(environment));
when(saved.resolve(19, -64, -3, true)).thenReturn(Optional.of(environment));
when(saved.resolveCaveBase(19, -3)).thenReturn(Optional.of(environment));
assertSame(environment, engine.getBiomeEnvironment(19, 20, -3));
assertSame(environment, engine.getSurfaceBiomeEnvironment(19, -3));
assertSame(environment.biome(), engine.getBiome(19, 20, -3));
assertSame(environment.biome(), engine.getBiomeOrMantle(19, 20, -3));
assertSame(environment.biome(), engine.getCaveOrMantleBiome(19, 20, -3));
assertSame(environment.biome(), engine.getCaveBiome(19, 20, -3));
assertSame(environment.biome(), engine.getCaveBiome(19, -3));
assertSame(environment.biome(), engine.getSurfaceBiome(19, -3));
assertSame(environment.region(), engine.getRegion(19, -3));
assertSame(environment.region(), engine.getRegion(19, 20, -3));
verify(router, never()).openCoordinateScope(anyInt(), anyInt());
}
@Test
public void unavailableSavedBiomesNeverUseTheActiveGenerator() throws Exception {
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
SavedBiomeRuntime saved = mock(SavedBiomeRuntime.class);
when(router.biomes()).thenReturn(saved);
setField(engine, "generationHistoryRuntimeRouter", router);
SavedBiomeUnavailableException failure = new SavedBiomeUnavailableException("Missing historical biome", false);
when(saved.resolve(19, 20, -3, false)).thenThrow(failure);
assertSame(failure, assertThrows(SavedBiomeUnavailableException.class,
() -> engine.getBiome(19, 20, -3)));
verify(router, never()).openCoordinateScope(anyInt(), anyInt());
}
@Test
public void definitionScopesRestoreAndLeaveGenerationRuntimeUnchanged() throws Exception {
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
RuntimeFixture selected = runtime(2, 2D, 2D, 2D);
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
BiomeEnvironment first = new BiomeEnvironment(4L, mock(IrisBiome.class),
mock(IrisRegion.class), mock(IrisDimension.class), mock(IrisData.class));
BiomeEnvironment second = new BiomeEnvironment(5L, mock(IrisBiome.class),
mock(IrisRegion.class), mock(IrisDimension.class), mock(IrisData.class));
try (BiomeEnvironment.Scope outer = engine.openBiomeEnvironmentScope(first)) {
assertSame(first.data(), engine.getData());
assertSame(first.dimension(), engine.getDimension());
assertSame(active.complex, engine.getComplex());
try (BiomeEnvironment.Scope inner = engine.openBiomeEnvironmentScope(second)) {
assertSame(second.data(), engine.getData());
assertThrows(IllegalStateException.class, outer::close);
CompletableFuture.runAsync(() -> assertThrows(IllegalStateException.class, inner::close))
.get(5, TimeUnit.SECONDS);
}
assertSame(first.data(), engine.getData());
try (IrisEngine.GenerationRuntimeScope ignored = engine.openGenerationRuntimeScope(
detachedBinding(engine, selected.runtime))) {
assertSame(selected.data, engine.getData());
assertSame(selected.dimension, engine.getDimension());
assertSame(selected.complex, engine.getComplex());
}
assertSame(first.data(), engine.getData());
}
assertSame(active.data, engine.getData());
assertSame(active.dimension, engine.getDimension());
}
private static IrisEngine engine(
GenerationRuntime generationRuntime,
EngineEffects effects,
@@ -549,6 +714,7 @@ public class IrisEngineGenerationRuntimeScopeTest {
setField(engine, "lifecycleLock", new Object());
setField(engine, "generationHistoryRuntimeRouterLock", new Object());
setField(engine, "runtimeAssembly", new ThreadLocal<EngineRuntimeBuilder.RuntimeAssembly>());
setField(engine, "biomeEnvironmentScopes", new ThreadLocal<Object>());
setField(engine, "generationRuntimeScopes", new GenerationRuntimeScopeState());
Set<GenerationRuntime> detached = Collections.synchronizedSet(
Collections.newSetFromMap(new IdentityHashMap<GenerationRuntime, Boolean>()));
@@ -4,6 +4,7 @@ import art.arcane.iris.engine.framework.EngineMode;
import art.arcane.iris.engine.framework.EnginePlatformHooks;
import art.arcane.iris.engine.framework.EngineStage;
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.engine.history.FloatingBiomeOverlay;
import art.arcane.iris.engine.history.BoundaryColumnGeometry;
import art.arcane.iris.engine.history.TerrainBoundarySignatureStore;
import art.arcane.iris.engine.history.TransitionGenerationPlan;
@@ -12,6 +13,9 @@ import art.arcane.iris.engine.history.TerrainBoundarySignature;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.util.project.context.IrisContext;
@@ -19,6 +23,7 @@ import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.iris.util.project.stream.ProceduralStream;
import art.arcane.volmlib.util.collection.KList;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.io.IOException;
import java.lang.reflect.Field;
@@ -46,12 +51,42 @@ import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doCallRealMethod;
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.verify;
import static org.mockito.Mockito.when;
public final class ResolvedTerrainProviderTest {
@Test
public void cachedSpeculativeFloatingIdentityIsPublishedOnlyByActualGeneration() throws Exception {
Fixture fixture = new Fixture();
fixture.floatingIdentity = new FloatingBiomeOverlay.Identity("floating-child", "region");
TransitionGenerationPlan plan = mock(TransitionGenerationPlan.class);
when(plan.hasTransitionAtChunk(0, 0)).thenReturn(true);
when(plan.newEpochWeightAt(anyInt(), anyInt())).thenReturn(1D);
when(fixture.complex.getTransitionGenerationPlan()).thenReturn(plan);
fixture.provider.column(0, 0);
verify(fixture.router, never()).recordFloatingBiomes(anyInt(), anyInt(), any());
IrisPlatform platform = mock(IrisPlatform.class);
PlatformRegistries registries = mock(PlatformRegistries.class);
when(platform.registries()).thenReturn(registries);
when(registries.blockOrNull("minecraft:stone")).thenReturn(fixture.stone);
when(registries.blockOrNull("minecraft:air")).thenReturn(fixture.air);
when(registries.biome("minecraft:plains")).thenReturn(fixture.biome);
ChunkContext context = new ChunkContext(0, 0, fixture.complex, false, ChunkContext.PrefillPlan.NONE, null);
try (MockedStatic<IrisPlatforms> platforms = mockStatic(IrisPlatforms.class);
IrisContext.Scope ignored = IrisContext.open(fixture.engine, 5L, context)) {
platforms.when(IrisPlatforms::get).thenReturn(platform);
fixture.provider.generate(fixture.mode, 0, 0, Hunk.newArrayHunk(16, 16, 16),
Hunk.newArrayHunk(16, 16, 16), false, context);
}
assertEquals(Integer.valueOf(1), fixture.computations.get("0,0"));
assertEquals(fixture.floatingIdentity, context.getFloatingBiomes().volumeAt(0, 0, 0));
assertEquals(fixture.floatingIdentity, context.getFloatingBiomes().surfaceAt(0, 0));
verify(fixture.router).recordFloatingBiomes(0, 0, context.getFloatingBiomes());
}
@Test
public void columnRequestsAreDeterministicAcrossOrderAndNegativeCoordinates() {
Fixture first = new Fixture();
@@ -352,6 +387,7 @@ public final class ResolvedTerrainProviderTest {
private final ResolvedTerrainProvider provider;
private long lastSessionId;
private EngineStage terrainOverride;
private FloatingBiomeOverlay.Identity floatingIdentity;
@SuppressWarnings("unchecked")
private Fixture() {
@@ -411,6 +447,10 @@ public final class ResolvedTerrainProviderTest {
}
}
}
if (floatingIdentity != null) {
context.floatingBiomes(16).record(0, 0, 0, floatingIdentity);
context.floatingBiomes(16).record(0, 2, 0, floatingIdentity);
}
}
private static PlatformBlockState block(String key, boolean air) {
@@ -0,0 +1,117 @@
package art.arcane.iris.engine;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.MarkerSpawnScanner.PreparedMarkerSpawn;
import art.arcane.iris.engine.framework.BiomeEnvironment;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.history.SavedBiomeUnavailableException;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisEntitySpawn;
import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisSpawner;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KSet;
import art.arcane.volmlib.util.math.RNG;
import org.bukkit.Chunk;
import org.bukkit.World;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.MockedStatic;
import java.lang.reflect.Field;
import java.util.List;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class WorldEntitySpawnerBiomeTest {
@Test
public void loadingSurfaceDefinitionsDoNotConsumeInitialSpawnFlag() throws Exception {
Engine engine = mock(Engine.class);
IrisWorldManager manager = manager(engine);
WorldChunkMaintenance maintenance = manager.chunkMaintenance;
World world = mock(World.class);
Chunk chunk = mock(Chunk.class);
when(chunk.getWorld()).thenReturn(world);
when(chunk.getX()).thenReturn(2);
when(chunk.getZ()).thenReturn(-1);
BiomeEnvironment environment = environment();
when(engine.getSurfaceBiomeEnvironment(40, -8))
.thenThrow(new SavedBiomeUnavailableException("Loading", true))
.thenReturn(environment);
IrisSettings settings = new IrisSettings();
settings.getWorld().setAmbientEntitySpawningSystem(true);
try (MockedStatic<IrisSettings> configured = mockStatic(IrisSettings.class)) {
configured.when(IrisSettings::get).thenReturn(settings);
WorldEntitySpawner spawner = new WorldEntitySpawner(manager);
spawner.prepareInitialSpawn(chunk, List.of());
verify(maintenance, never()).raiseInitialSpawnMarkerFlag(any(), anyInt(), anyInt(), any());
spawner.prepareInitialSpawn(chunk, List.of());
verify(maintenance).raiseInitialSpawnMarkerFlag(eq(world), eq(2), eq(-1), any());
}
}
@Test
public void preparedInitialMarkersKeepDefinitionsAfterSavedQueryEviction() throws Exception {
Engine engine = mock(Engine.class);
IrisWorldManager manager = manager(engine);
World world = mock(World.class);
Chunk chunk = mock(Chunk.class);
when(chunk.getWorld()).thenReturn(world);
BiomeEnvironment environment = environment();
BiomeEnvironment.Scope scope = mock(BiomeEnvironment.Scope.class);
when(engine.openBiomeEnvironmentScope(environment)).thenReturn(scope);
IrisSpawner definition = mock(IrisSpawner.class);
IrisEntitySpawn entity = mock(IrisEntitySpawn.class);
IrisPosition position = new IrisPosition(3, 90, 4);
when(definition.getInitialSpawns()).thenReturn(new KList<>(entity));
when(definition.canSpawn(engine, 0, 0)).thenReturn(true);
when(entity.getRarity()).thenReturn(1);
when(entity.getReferenceSpawner()).thenReturn(definition);
when(entity.spawn(eq(engine), eq(position), any(RNG.class))).thenReturn(1);
KSet<IrisSpawner> definitions = new KSet<>();
definitions.add(definition);
PreparedMarkerSpawn marker = new PreparedMarkerSpawn(position, definitions, environment);
IrisSettings settings = new IrisSettings();
settings.getWorld().setAmbientEntitySpawningSystem(false);
try (MockedStatic<IrisSettings> configured = mockStatic(IrisSettings.class)) {
configured.when(IrisSettings::get).thenReturn(settings);
new WorldEntitySpawner(manager).prepareInitialSpawn(chunk, List.of(marker));
ArgumentCaptor<Runnable> callback = ArgumentCaptor.forClass(Runnable.class);
verify(manager.chunkMaintenance).raiseInitialSpawnMarkerFlag(eq(world), eq(0), eq(0), callback.capture());
doThrow(new SavedBiomeUnavailableException("Evicted", true)).when(engine)
.getBiomeEnvironment(anyInt(), anyInt(), anyInt());
callback.getValue().run();
verify(entity).spawn(eq(engine), eq(position), any(RNG.class));
verify(engine).openBiomeEnvironmentScope(environment);
verify(scope).close();
verify(engine, never()).getBiomeEnvironment(anyInt(), anyInt(), anyInt());
}
}
private static IrisWorldManager manager(Engine engine) throws Exception {
IrisWorldManager manager = mock(IrisWorldManager.class);
when(manager.getEngine()).thenReturn(engine);
Field field = IrisWorldManager.class.getDeclaredField("chunkMaintenance");
field.setAccessible(true);
field.set(manager, mock(WorldChunkMaintenance.class));
return manager;
}
private static BiomeEnvironment environment() {
return new BiomeEnvironment(3L, mock(IrisBiome.class), mock(IrisRegion.class),
mock(IrisDimension.class), mock(IrisData.class));
}
}
@@ -1,6 +1,25 @@
package art.arcane.iris.engine.framework;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.history.SavedBiomeUnavailableException;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.junit.Test;
import org.mockito.MockedStatic;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -13,4 +32,35 @@ public class EnginePlayerTest {
assertFalse(EnginePlayer.needsSample(true, 55L, 82D));
assertTrue(EnginePlayer.needsSample(true, 56L, 82D));
}
@Test
public void loadingSavedBiomesRetryWithoutMovementAndKeepOneDefinitionContext() {
Engine engine = mock(Engine.class);
IrisWorld irisWorld = mock(IrisWorld.class);
World world = mock(World.class);
Player player = mock(Player.class);
when(engine.getWorld()).thenReturn(irisWorld);
when(irisWorld.minHeight()).thenReturn(-64);
when(player.getLocation()).thenReturn(new Location(world, 19, 80, -3));
BiomeEnvironment environment = new BiomeEnvironment(4L, mock(IrisBiome.class),
mock(IrisRegion.class), mock(IrisDimension.class), mock(IrisData.class));
when(engine.getBiomeEnvironment(19, 144, -3))
.thenThrow(new SavedBiomeUnavailableException("Loading", true))
.thenReturn(environment);
try (MockedStatic<BukkitWorldBinding> binding = mockStatic(BukkitWorldBinding.class)) {
binding.when(() -> BukkitWorldBinding.world(irisWorld)).thenReturn(world);
EnginePlayer sampled = new EnginePlayer(engine, player);
assertTrue(sampled.sample());
assertNull(sampled.getLastLocation());
assertNull(sampled.getEnvironment());
assertFalse(sampled.sample());
assertSame(environment, sampled.getEnvironment());
assertSame(environment.biome(), sampled.getBiome());
assertSame(environment.region(), sampled.getRegion());
assertFalse(sampled.sample());
verify(engine, times(2)).getBiomeEnvironment(19, 144, -3);
}
}
}
@@ -1,6 +1,7 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.core.IrisDatapackCompiler;
import art.arcane.iris.core.pack.AtomicDirectoryPublisher;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.nms.datapack.v1217.DataFixerV1217;
import art.arcane.iris.engine.object.IrisDimension;
@@ -47,7 +48,8 @@ public class ArchivedRegistryCompilerTest {
String oldEpoch = history.activeEpoch().epochId();
history.stageUpdate(secondPack, second.fingerprint(), second.dimension(), second.registry(), 256);
history.promotePending(List.of());
new GenerationPackRepository(world).releaseArchivedPacks(history.manifest());
assertTrue(Files.isDirectory(history.paths().packRoot(oldEpoch)));
AtomicDirectoryPublisher.deleteTree(history.paths().packRoot(oldEpoch));
assertFalse(Files.exists(history.paths().packRoot(oldEpoch)));
List<File> packs = List.of(history.activePackRoot().toFile());
@@ -3,6 +3,7 @@ package art.arcane.iris.engine.history;
import art.arcane.iris.core.nms.container.BlockPos;
import art.arcane.iris.core.nms.container.Pair;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.core.pack.AtomicDirectoryPublisher;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
@@ -50,6 +51,8 @@ public final class ArchivedSemanticQueryTest {
updated.stageUpdate(newPack, fingerprint(newPack), dimension, GenerationRegistryContract.empty(), 32);
updated.prepareCurrentGenerator(32);
GenerationHistory archived = GenerationHistory.open(world);
assertTrue(Files.exists(archived.paths().packRoot(oldEpoch)));
AtomicDirectoryPublisher.deleteTree(archived.paths().packRoot(oldEpoch));
assertFalse(Files.exists(archived.paths().packRoot(oldEpoch)));
IrisEngine engine = engine(archived);
@@ -0,0 +1,30 @@
package art.arcane.iris.engine.history;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
public class FloatingBiomeOverlayTest {
@Test
public void volumeFollowsTheQuartAnchorAndLastWriterWhileSurfaceUsesTheHighestWrite() {
FloatingBiomeOverlay overlay = new FloatingBiomeOverlay(16);
FloatingBiomeOverlay.Identity first = new FloatingBiomeOverlay.Identity("first", "region");
FloatingBiomeOverlay.Identity second = new FloatingBiomeOverlay.Identity("second", "region");
overlay.record(0, 8, 0, first);
overlay.record(0, 9, 0, second);
overlay.record(1, 8, 1, second);
assertEquals(first, overlay.volumeAt(3, 11, 3));
overlay.record(0, 8, 0, second);
assertEquals(second, overlay.volumeAt(3, 11, 3));
assertNull(overlay.volumeAt(4, 8, 0));
overlay.retainHighestSurfaces((x, z) -> x == 0 ? 9 : 12);
assertEquals(second, overlay.surfaceAt(0, 0));
assertEquals(9, overlay.surfaceYAt(0, 0));
assertNull(overlay.surfaceAt(1, 1));
assertEquals(-1, overlay.surfaceYAt(1, 1));
assertThrows(IllegalArgumentException.class, () -> overlay.record(16, 8, 0, first));
assertThrows(IllegalArgumentException.class, () -> overlay.volumeAt(0, 16, 0));
}
}
@@ -79,6 +79,33 @@ public class GenerationEpochContractFactoryTest {
assertEquals(164, contract.internalFluidHeight());
}
@Test
public void recordedEpochKeepsItsUpperTerrainFingerprintVersion() throws Exception {
File pack = temporary.newFolder("recorded-upper-pack");
Files.createDirectories(pack.toPath().resolve("objects"));
Files.writeString(pack.toPath().resolve("objects/.DS_Store"), "recorded metadata");
IrisData data = mock(IrisData.class);
IrisDimension dimension = dimension(IrisEnvironment.NORMAL).setUpperDimension("overworld");
dimension.setLoader(data);
when(data.getDataFolder()).thenReturn(pack);
GenerationEpoch.DimensionContract current = GenerationEpochContractFactory.create(
dimension, "overworld", "iris:overworld");
GenerationEpoch epoch = mock(GenerationEpoch.class);
when(epoch.dimensionContract()).thenReturn(current);
when(epoch.packFingerprintVersion()).thenReturn(1);
GenerationEpoch.DimensionContract recorded = GenerationEpochContractFactory.createForEpoch(
dimension, "iris:overworld", epoch);
assertEquals(GenerationPackFingerprint.compute(pack.toPath(), 1), recorded.upperTerrainPackFingerprint());
assertNotEquals(current.upperTerrainPackFingerprint(), recorded.upperTerrainPackFingerprint());
when(epoch.dimensionContract()).thenReturn(recorded);
assertEquals(recorded, GenerationEpochContractFactory.createForEpoch(dimension, "iris:overworld", epoch));
Files.writeString(pack.toPath().resolve("objects/.DS_Store"), "changed metadata");
assertNotEquals(recorded, GenerationEpochContractFactory.createForEpoch(dimension, "iris:overworld", epoch));
assertEquals(current, GenerationEpochContractFactory.create(dimension, "overworld", "iris:overworld"));
}
@Test
public void upperTerrainMustBeLocalAndPackIdentityDoesNotChangeItsLayout() throws Exception {
File pack = temporary.newFolder("upper-pack");
@@ -127,7 +154,7 @@ public class GenerationEpochContractFactoryTest {
dimension,
"overworld",
"iris:overworld"
)
)
);
}
@@ -1,7 +1,10 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.core.pack.AtomicDirectoryPublisher;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.framework.GenerationSessionManager;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.util.common.data.B;
@@ -40,6 +43,7 @@ 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.anyInt;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
@@ -177,10 +181,14 @@ public final class GenerationHistoryRuntimeRouterTest {
}
@Test
public void historicalRoutesWorkAfterArchivedPacksAreReleased() throws Exception {
public void historicalTerrainRoutesWorkWhenFrozenPackFixturesAreMissing() throws Exception {
Path world = temporaryFolder.newFolder("router-archived-world").toPath();
GenerationHistory history = createThreeActivationHistory(world, "router-archived");
new GenerationPackRepository(world).releaseArchivedPacks(history.manifest());
for (long activationId : List.of(1L, 2L)) {
Path archivedPack = history.paths().packRoot(history.manifest().activation(activationId).orElseThrow().epochId());
assertTrue(Files.isDirectory(archivedPack));
AtomicDirectoryPublisher.deleteTree(archivedPack);
}
assertFalse(Files.exists(history.paths().packRoot(history.manifest().activation(1L).orElseThrow().epochId())));
IrisEngine engine = mock(IrisEngine.class);
FakeRuntimeFactory runtimes = new FakeRuntimeFactory();
@@ -568,6 +576,18 @@ public final class GenerationHistoryRuntimeRouterTest {
GenerationHistory history = createHistory(world, pack);
IrisEngine engine = mock(IrisEngine.class);
IrisComplex complex = mock(IrisComplex.class);
IrisBiome biome = new IrisBiome();
biome.setLoadKey("alpha");
IrisRegion region = new IrisRegion();
region.setLoadKey("alpha");
when(engine.getMinHeight()).thenReturn(-64);
when(engine.getHeight()).thenReturn(384);
when(engine.getSurfaceBiome(anyInt(), anyInt())).thenReturn(biome);
when(engine.getCaveBiome(anyInt(), anyInt())).thenReturn(biome);
when(engine.getBiome(anyInt(), anyInt(), anyInt())).thenReturn(biome);
when(engine.getBiomeOrMantle(anyInt(), anyInt(), anyInt())).thenReturn(biome);
when(engine.getRegion(anyInt(), anyInt())).thenReturn(region);
when(engine.getRegion(anyInt(), anyInt(), anyInt())).thenReturn(region);
EngineMantle engineMantle = mock(EngineMantle.class);
@SuppressWarnings("unchecked")
Mantle<Matter> mantle = mock(Mantle.class);
@@ -596,6 +616,7 @@ public final class GenerationHistoryRuntimeRouterTest {
ChunkGenerationSemantics semantics = history.semantics(4, -3).orElseThrow();
assertTrue(semantics.sealed());
assertEquals(1L, semantics.activationId());
assertEquals("alpha", history.savedBiomes().get(4, -3).orElseThrow().biomeAt(0, 0, 0).biomeKey());
router.close();
}
@@ -669,7 +669,7 @@ public final class GenerationHistoryTest {
}
@Test
public void coldRecoveryKeepsStoredClaimsAndHistoricalMetadataButReleasesOldPacks() throws Exception {
public void coldRecoveryKeepsStoredClaimsHistoricalMetadataAndFrozenPacks() throws Exception {
Path world = temporaryFolder.newFolder("cold-recovery-world").toPath();
Path packA = createPack("cold-recovery-a", "alpha");
Path packB = createPack("cold-recovery-b", "beta");
@@ -695,7 +695,9 @@ public final class GenerationHistoryTest {
assertTrue(recovered.semantics(0, 0).isPresent());
assertTrue(recovered.semantics(1, 0).isPresent());
assertTrue(recovered.semantics(2, 0).isEmpty());
assertFalse(Files.exists(recovered.paths().packRoot(oldEpoch)));
assertTrue(Files.isDirectory(recovered.paths().packRoot(oldEpoch)));
assertEquals(GenerationPackFingerprint.compute(packA, GenerationPackFingerprint.CURRENT_VERSION),
GenerationPackFingerprint.compute(recovered.paths().packRoot(oldEpoch), GenerationPackFingerprint.CURRENT_VERSION));
assertTrue(Files.isDirectory(recovered.activePackRoot()));
assertTrue(Files.isDirectory(packA));
GenerationHistory reopened = GenerationHistory.open(world);
@@ -30,10 +30,46 @@ public class GenerationPackFingerprintTest {
String fingerprint = GenerationPackFingerprint.compute(
pack,
GenerationPackFingerprint.CURRENT_VERSION
1
);
assertEquals("ad7d39c2b95bd3001248778911ce6bbd6b4cbdcece28145838c2b923a0bf73b3", fingerprint);
Files.writeString(pack.resolve("objects/.DS_Store"), "finder");
assertEquals("b36c630e300a9939fabe3fbd02f546f2a76b31d2ebccbf9106a13fb7d8b8eb98",
GenerationPackFingerprint.compute(pack, 1));
assertEquals(fingerprint, GenerationPackFingerprint.compute(pack, 2));
}
@Test
public void versionTwoIgnoresFinderMetadataAtEveryDepth() throws Exception {
Path source = temporaryFolder.newFolder("metadata-source").toPath();
Files.createDirectories(source.resolve("objects/trees"));
Files.writeString(source.resolve("objects/trees/oak.json"), "{}");
Files.writeString(source.resolve("objects/.DS_Store"), "source metadata");
Path target = temporaryFolder.getRoot().toPath().resolve("metadata-copy");
GenerationPackRepository.copyPackTree(source, target);
String expected = GenerationPackFingerprint.compute(source, 2);
Files.writeString(target.resolve(".DS_Store"), "root metadata");
Files.writeString(target.resolve("objects/.DS_Store"), "destination metadata");
Files.writeString(target.resolve("objects/trees/.DS_Store"), "nested metadata");
assertEquals(expected, GenerationPackFingerprint.compute(target, 2));
assertNotEquals(GenerationPackFingerprint.compute(source, 1),
GenerationPackFingerprint.compute(target, 1));
Files.writeString(target.resolve("objects/trees/oak.json"), "{\"changed\":true}");
assertNotEquals(expected, GenerationPackFingerprint.compute(target, 2));
}
@Test
public void versionTwoStillIncludesOtherNestedHiddenResources() throws Exception {
Path pack = temporaryFolder.newFolder("nested-hidden-pack").toPath();
Files.createDirectories(pack.resolve("objects"));
Files.writeString(pack.resolve("objects/.hidden"), "first");
String first = GenerationPackFingerprint.compute(pack, 2);
Files.writeString(pack.resolve("objects/.hidden"), "second");
assertNotEquals(first, GenerationPackFingerprint.compute(pack, 2));
}
@Test
@@ -51,7 +87,7 @@ public class GenerationPackFingerprintTest {
@Test
public void unsupportedVersionsAndSymbolicLinksFailClosed() throws Exception {
Path pack = temporaryFolder.newFolder("unsafe-pack").toPath();
assertThrows(IOException.class, () -> GenerationPackFingerprint.compute(pack, 2));
assertThrows(IOException.class, () -> GenerationPackFingerprint.compute(pack, 3));
Path target = temporaryFolder.newFile("outside.json").toPath();
try {
@@ -4,6 +4,7 @@ import org.junit.Assume;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.mockito.MockedStatic;
import java.io.IOException;
import java.nio.file.Files;
@@ -12,11 +13,13 @@ import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mockStatic;
public class GenerationPackRepositoryTest {
@Rule
@@ -69,6 +72,50 @@ public class GenerationPackRepositoryTest {
));
}
@Test
public void finderMetadataCreatedDuringPublicationDoesNotChangeVersionTwoIdentity() throws Exception {
Path dimensionRoot = temporaryFolder.newFolder("finder-world").toPath();
Path source = createPack("finder-source", "{}");
String fingerprint = fingerprint(source);
GenerationPackRepository repository = new GenerationPackRepository(dimensionRoot);
String epochId = digest('b');
AtomicBoolean injected = new AtomicBoolean();
try (MockedStatic<GenerationPackFingerprint> ignored = mockStatic(GenerationPackFingerprint.class, invocation -> {
if (invocation.getMethod().getName().equals("compute")) {
Path pack = invocation.getArgument(0);
if (pack.getFileName().toString().startsWith(".pack-")) {
Files.writeString(pack.resolve(".DS_Store"), "root metadata");
Files.writeString(pack.resolve("dimensions/.DS_Store"), "nested metadata");
injected.set(true);
}
}
return invocation.callRealMethod();
})) {
Path published = repository.publish(epochId, fingerprint, 2, source);
assertTrue(injected.get());
assertTrue(Files.isRegularFile(published.resolve("dimensions/.DS_Store")));
assertEquals(published, repository.requireExactPack(epochId, fingerprint, 2));
Files.writeString(published.resolve("dimensions/main.json"), "{\"changed\":true}");
assertThrows(IOException.class, () -> repository.requireExactPack(epochId, fingerprint, 2));
}
}
@Test
public void versionOnePublicationsKeepTheirOriginalMetadataContract() throws Exception {
Path dimensionRoot = temporaryFolder.newFolder("version-one-world").toPath();
Path source = createPack("version-one-source", "{}");
Files.writeString(source.resolve("dimensions/.DS_Store"), "original metadata");
String fingerprint = GenerationPackFingerprint.compute(source, 1);
GenerationPackRepository repository = new GenerationPackRepository(dimensionRoot);
String epochId = digest('a');
Path published = repository.publish(epochId, fingerprint, 1, source);
assertEquals(published, repository.requireExactPack(epochId, fingerprint, 1));
Files.writeString(published.resolve("dimensions/.DS_Store"), "changed metadata");
assertThrows(IOException.class, () -> repository.requireExactPack(epochId, fingerprint, 1));
}
@Test
public void rejectsNestedSymbolicLinks() throws Exception {
Path dimensionRoot = temporaryFolder.newFolder("linked-world").toPath();
@@ -149,7 +196,7 @@ public class GenerationPackRepositoryTest {
}
@Test
public void releasesOnlyArchivedPacksAndCanRepublishTheirSnapshot() throws Exception {
public void publicationAndActivationRetainEveryFrozenPack() throws Exception {
Path dimensionRoot = temporaryFolder.newFolder("retention-world").toPath();
Path firstSource = createPack("retention-first", "{\"version\":1}");
Path secondSource = createPack("retention-second", "{\"version\":2}");
@@ -167,18 +214,17 @@ public class GenerationPackRepositoryTest {
repository.publish(pending.epochId(), pending.packFingerprint(), pending.packFingerprintVersion(), pendingSource);
store.preparePendingActivation(pending, 256);
repository.releaseArchivedPacks(store.manifest());
repository.releaseArchivedPacks(store.manifest());
assertFalse(Files.exists(repository.packRoot(first.epochId())));
assertTrue(Files.isDirectory(repository.packRoot(first.epochId())));
assertEquals(first.packFingerprint(), fingerprint(repository.packRoot(first.epochId())));
assertTrue(Files.isRegularFile(repository.epochRoot(first.epochId()).resolve("epoch.json")));
assertTrue(Files.isDirectory(repository.packRoot(second.epochId())));
assertTrue(Files.isDirectory(repository.packRoot(pending.epochId())));
assertTrue(Files.isRegularFile(firstSource.resolve("dimensions/main.json")));
assertEquals(store.manifest(), GenerationHistoryStore.open(repository.generationRoot()).manifest());
Path republished = repository.publish(first.epochId(), first.packFingerprint(), first.packFingerprintVersion(), firstSource);
assertEquals(Files.readString(firstSource.resolve("dimensions/main.json")),
Files.readString(republished.resolve("dimensions/main.json")));
Files.writeString(firstSource.resolve("dimensions/main.json"), "{\"version\":99}");
assertEquals("{\"version\":1}", Files.readString(repository.packRoot(first.epochId()).resolve("dimensions/main.json")));
assertEquals(repository.packRoot(first.epochId()), repository.requireExactPack(
first.epochId(), first.packFingerprint(), first.packFingerprintVersion()));
}
private static GenerationEpoch epoch(String fingerprint) {
@@ -0,0 +1,95 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.util.common.data.DataProvider;
import art.arcane.volmlib.util.collection.KList;
import org.junit.Test;
import java.util.Map;
import java.util.Optional;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.when;
public class NativeBiomeSpawnSelectionTest {
@Test
public void savedSelectionUsesTheActualPhysicalHolderAndOwningRuntime() {
IrisEngine engine = mock(IrisEngine.class);
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
SavedBiomeRuntime saved = mock(SavedBiomeRuntime.class);
when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
when(router.biomes()).thenReturn(saved);
NativeBiomeSpawnSelection retained = new NativeBiomeSpawnSelection(
NativeBiomeSpawnSelection.Mode.RETAINED, "minecraft:plains");
when(saved.nativeSpawnSelection(-17, 62, 31, "iris:old-physical")).thenReturn(retained);
assertEquals(retained, NativeBiomeSpawnSelection.at(engine, -17, 62, 31, "iris:old-physical"));
verify(saved).nativeSpawnSelection(-17, 62, 31, "iris:old-physical");
}
@Test
public void generationScopesUseCurrentPredictionWithoutReadingSavedBiomes() {
IrisEngine engine = mock(IrisEngine.class);
when(engine.hasGenerationRuntimeScope()).thenReturn(true);
assertEquals(NativeBiomeSpawnSelection.Mode.CURRENT,
NativeBiomeSpawnSelection.at(engine, 0, 64, 0, "iris:new-physical").mode());
verify(engine, never()).getGenerationHistoryRuntimeRouter();
}
@Test
public void loadingSkipsTheAttemptAndUnsupportedNeverUsesCurrentInheritance() {
IrisEngine engine = mock(IrisEngine.class);
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
SavedBiomeRuntime saved = mock(SavedBiomeRuntime.class);
when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
when(router.biomes()).thenReturn(saved);
when(saved.nativeSpawnSelection(0, 64, 0, "iris:old"))
.thenThrow(new SavedBiomeUnavailableException("loading", true))
.thenThrow(new SavedBiomeUnavailableException("unsupported", false));
assertEquals(NativeBiomeSpawnSelection.Mode.LOADING,
NativeBiomeSpawnSelection.at(engine, 0, 64, 0, "iris:old").mode());
assertEquals(NativeBiomeSpawnSelection.Mode.NONE,
NativeBiomeSpawnSelection.at(engine, 0, 64, 0, "iris:old").mode());
}
@Test
public void retainedPhysicalMappingsAreIndependentOfLaterParentDerivativeChanges() {
IrisData previous = retainedData("iris:same-physical", "minecraft:plains");
IrisData current = retainedData("iris:same-physical", "minecraft:desert");
Map<String, String> oldMappings = NativeBiomeSpawnSelection.retainedDerivatives(previous);
Map<String, String> newMappings = NativeBiomeSpawnSelection.retainedDerivatives(current);
assertEquals("minecraft:plains", oldMappings.get("iris:same-physical"));
assertEquals("minecraft:desert", newMappings.get("iris:same-physical"));
assertEquals(null, oldMappings.get("iris:unrelated-physical"));
assertTrue(oldMappings.keySet().contains("iris:same-physical"));
}
private static IrisData retainedData(String physicalKey, String vanillaDerivative) {
IrisData data = mock(IrisData.class, RETURNS_DEEP_STUBS);
IrisDimension dimension = mock(IrisDimension.class);
IrisBiome biome = mock(IrisBiome.class);
IrisBiomeCustom custom = new IrisBiomeCustom();
custom.setId("custom");
when(data.getDimensionLoader().getPossibleKeys()).thenReturn(new String[]{"main"});
when(data.getDimensionLoader().load("main")).thenReturn(dimension);
when(dimension.getReachableBiomes(any(DataProvider.class))).thenReturn(new KList<IrisBiome>().qadd(biome));
when(biome.isCustom()).thenReturn(true);
when(biome.getCustomDerivitives()).thenReturn(new KList<IrisBiomeCustom>().qadd(custom));
when(biome.getVanillaDerivativeKey()).thenReturn(vanillaDerivative);
when(data.customBiomeResourceKey(dimension, custom)).thenReturn(physicalKey);
return data;
}
}
@@ -0,0 +1,106 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisRegion;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
public class SavedBiomeCaptureTest {
@Test
public void retainsFloatingChildIdentityWhenItSharesTheHostDerivative() throws Exception {
Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS);
GenerationHistory.GenerationStage stage = mock(GenerationHistory.GenerationStage.class);
SavedBiomeRuntime historical = mock(SavedBiomeRuntime.class);
IrisBiome host = biome("host");
IrisBiome child = biome("floating-child");
host.setDerivative("minecraft:plains");
child.setDerivative("minecraft:plains");
IrisRegion region = region("host-region");
when(stage.activation()).thenReturn(GenerationActivation.initial("a".repeat(64), 1L));
when(engine.getMinHeight()).thenReturn(-64);
when(engine.getHeight()).thenReturn(16);
when(engine.getHeight(anyInt(), anyInt())).thenReturn(2);
when(engine.getComplex().getTransitionGenerationPlan()).thenReturn(null);
when(engine.getRegion(anyInt(), anyInt())).thenReturn(region);
when(engine.getRegion(anyInt(), anyInt(), anyInt())).thenReturn(region);
when(engine.getSurfaceBiome(anyInt(), anyInt())).thenReturn(host);
when(engine.getCaveBiome(anyInt(), anyInt())).thenReturn(host);
when(engine.getBiomeOrMantle(anyInt(), anyInt(), anyInt())).thenReturn(host);
FloatingBiomeOverlay floating = new FloatingBiomeOverlay(16);
FloatingBiomeOverlay.Identity childIdentity = new FloatingBiomeOverlay.Identity(child.getLoadKey(), region.getLoadKey());
floating.record(0, 8, 0, childIdentity);
floating.record(0, 9, 0, childIdentity);
floating.record(4, 8, 0, childIdentity);
floating.retainHighestSurfaces((x, z) -> x == 0 ? 9 : 12);
SavedBiomeChunk chunk = SavedBiomeCapture.capture(engine, stage, historical, floating);
assertEquals(host.getVanillaDerivativeKey(), child.getVanillaDerivativeKey());
assertEquals("floating-child", chunk.surfaceAt(0, 0).biomeKey());
assertEquals("host", chunk.surfaceAt(4, 0).biomeKey());
assertEquals("floating-child", chunk.biomeAt(0, -56, 0).biomeKey());
assertEquals("floating-child", chunk.biomeAt(3, -53, 3).biomeKey());
assertEquals("host", chunk.biomeAt(0, -52, 0).biomeKey());
assertEquals("host", chunk.caveBaseAt(0, 0).biomeKey());
assertEquals(1L, chunk.biomeAt(0, -56, 0).activationId());
verifyNoInteractions(historical);
}
@Test
public void recordsExactSurfaceColumnsAndIndependentCaveAndVerticalIdentities() throws Exception {
Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS);
GenerationHistory.GenerationStage stage = mock(GenerationHistory.GenerationStage.class);
SavedBiomeRuntime historical = mock(SavedBiomeRuntime.class);
IrisBiome left = biome("left");
IrisBiome right = biome("right");
IrisBiome cave = biome("base-cave");
IrisBiome lower = biome("lower-volume");
IrisBiome upper = biome("upper-volume");
IrisRegion surfaceRegion = region("surface-region");
IrisRegion caveRegion = region("volume-region");
when(stage.chunkX()).thenReturn(-1);
when(stage.chunkZ()).thenReturn(2);
when(stage.activation()).thenReturn(GenerationActivation.initial("a".repeat(64), 1L));
when(engine.getMinHeight()).thenReturn(-64);
when(engine.getHeight()).thenReturn(16);
when(engine.getComplex().getTransitionGenerationPlan()).thenReturn(null);
when(engine.getRegion(anyInt(), anyInt())).thenReturn(surfaceRegion);
when(engine.getRegion(anyInt(), anyInt(), anyInt())).thenReturn(caveRegion);
when(engine.getSurfaceBiome(anyInt(), anyInt())).thenAnswer(call -> (int) call.getArgument(0) == -15 ? right : left);
when(engine.getCaveBiome(anyInt(), anyInt())).thenReturn(cave);
when(engine.getBiomeOrMantle(anyInt(), anyInt(), anyInt())).thenAnswer(call -> (int) call.getArgument(1) < 8 ? lower : upper);
SavedBiomeChunk chunk = SavedBiomeCapture.capture(engine, stage, historical, null);
assertEquals(new SavedBiomeChunk.Header(-1, 2, 1L, -64, 16), chunk.header());
assertEquals("left", chunk.surfaceAt(0, 0).biomeKey());
assertEquals("right", chunk.surfaceAt(1, 0).biomeKey());
assertEquals("surface-region", chunk.surfaceAt(1, 0).regionKey());
assertEquals("base-cave", chunk.caveBaseAt(1, 0).biomeKey());
assertEquals("lower-volume", chunk.biomeAt(1, -57, 0).biomeKey());
assertEquals("upper-volume", chunk.biomeAt(1, -56, 0).biomeKey());
assertEquals("volume-region", chunk.biomeAt(1, -56, 0).regionKey());
assertEquals(2, chunk.column(0, 0).vertical().size());
assertEquals(chunk.column(0, 0).vertical(), chunk.column(3, 3).vertical());
verifyNoInteractions(historical);
}
private static IrisBiome biome(String key) {
IrisBiome biome = new IrisBiome();
biome.setLoadKey(key);
return biome;
}
private static IrisRegion region(String key) {
IrisRegion region = new IrisRegion();
region.setLoadKey(key);
return region;
}
}
@@ -0,0 +1,219 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisDimensionCarvingEntry;
import art.arcane.iris.engine.object.IrisDimensionStack;
import art.arcane.iris.engine.object.IrisFloatingChildBiomes;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisRiverPolicy;
import art.arcane.volmlib.util.collection.KList;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.util.Optional;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class SavedBiomeRecoveryTest {
private static final String BIOME = "transition-demo";
private static final String REGION = "transition-demo";
private static final int CHUNK_X = -2;
private static final int CHUNK_Z = 3;
private final GenerationHistory history = mock(GenerationHistory.class);
private final IrisData data = mock(IrisData.class, RETURNS_DEEP_STUBS);
private final IrisDimension dimension = new IrisDimension();
private final IrisRegion region = new IrisRegion();
private final IrisBiome biome = new IrisBiome();
private final SavedBiomeRecovery.Input input =
new SavedBiomeRecovery.Input(history, CHUNK_X, CHUNK_Z, data, dimension);
@Before
public void prepareSingletonPack() {
dimension.setLoadKey("overworld");
dimension.setRegions(new KList<String>().qadd(REGION));
region.setLoadKey(REGION);
region.setLandBiomes(new KList<String>().qadd(BIOME));
region.setSeaBiomes(new KList<String>().qadd(BIOME));
region.setShoreBiomes(new KList<String>().qadd(BIOME));
region.setCaveBiomes(new KList<String>().qadd(BIOME));
biome.setLoadKey(BIOME);
when(data.getRegionLoader().load(REGION)).thenReturn(region);
when(data.getBiomeLoader().load(BIOME)).thenReturn(biome);
when(history.resolveActivation(CHUNK_X, CHUNK_Z))
.thenReturn(GenerationActivation.initial("a".repeat(64), 1L));
GenerationEpoch epoch = mock(GenerationEpoch.class);
GenerationEpoch.DimensionContract contract = mock(GenerationEpoch.DimensionContract.class);
when(contract.dimensionKey()).thenReturn("overworld");
when(contract.minHeight()).thenReturn(-64);
when(contract.maxHeight()).thenReturn(320);
when(contract.height()).thenReturn(384);
when(epoch.dimensionContract()).thenReturn(contract);
when(history.resolveEpoch(CHUNK_X, CHUNK_Z)).thenReturn(epoch);
record(semantics(1L).seal().build());
}
@Test
public void recoversAllColumnsAndEntireHeightFromTheOwningActivation() throws IOException {
SavedBiomeChunk recovered = SavedBiomeRecovery.recover(input).orElseThrow();
SavedBiomeChunk.Cell expected = new SavedBiomeChunk.Cell(1L, BIOME, REGION);
assertEquals(new SavedBiomeChunk.Header(CHUNK_X, CHUNK_Z, 1L, -64, 384), recovered.header());
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
assertEquals(expected, recovered.surfaceAt(x, z));
assertEquals(expected, recovered.caveBaseAt(x, z));
assertEquals(expected, recovered.biomeAt(x, -64, z));
assertEquals(expected, recovered.biomeAt(x, 319, z));
assertEquals(1, recovered.column(x, z).vertical().size());
}
}
}
@Test
public void rejectsMissingOrUnsealedRecordedFacts() throws IOException {
when(history.semantics(CHUNK_X, CHUNK_Z)).thenReturn(Optional.empty());
assertTrue(SavedBiomeRecovery.recover(input).isEmpty());
record(semantics(1L).build());
assertTrue(SavedBiomeRecovery.recover(input).isEmpty());
}
@Test
public void rejectsMultipleSurfaceOrRegionIdentities() throws IOException {
record(semantics(1L).addSurfaceBiome("other").seal().build());
assertTrue(SavedBiomeRecovery.recover(input).isEmpty());
record(semantics(1L).addRegion("other").seal().build());
assertTrue(SavedBiomeRecovery.recover(input).isEmpty());
}
@Test
public void rejectsInconsistentOwnershipAndDifferentCaveIdentity() throws IOException {
record(semantics(2L).seal().build());
assertTrue(SavedBiomeRecovery.recover(input).isEmpty());
record(semantics(1L).addCaveBiome("other").seal().build());
assertTrue(SavedBiomeRecovery.recover(input).isEmpty());
}
@Test
public void acceptsRecordedCavesWithTheSameIdentity() throws IOException {
record(semantics(1L).addCaveBiome(BIOME).seal().build());
assertTrue(SavedBiomeRecovery.recover(input).isPresent());
}
@Test
public void rejectsMissingDefinitions() throws IOException {
when(data.getBiomeLoader().load(BIOME)).thenReturn(null);
assertTrue(SavedBiomeRecovery.recover(input).isEmpty());
when(data.getBiomeLoader().load(BIOME)).thenReturn(biome);
when(data.getRegionLoader().load(REGION)).thenReturn(null);
assertTrue(SavedBiomeRecovery.recover(input).isEmpty());
}
@Test
public void rejectsPoolsWithUnrecordedChoicesOrNoVerticalChoice() throws IOException {
region.getCaveBiomes().add("other");
assertTrue(SavedBiomeRecovery.recover(input).isEmpty());
region.getCaveBiomes().clear();
assertTrue(SavedBiomeRecovery.recover(input).isEmpty());
region.getCaveBiomes().add(BIOME);
region.getSeaBiomes().add("other");
assertTrue(SavedBiomeRecovery.recover(input).isEmpty());
}
@Test
public void rejectsChildrenAndFloatingBiomes() throws IOException {
biome.getChildren().add("other");
assertTrue(SavedBiomeRecovery.recover(input).isEmpty());
biome.getChildren().clear();
biome.getFloatingChildBiomes().add(new IrisFloatingChildBiomes());
assertTrue(SavedBiomeRecovery.recover(input).isEmpty());
}
@Test
public void rejectsStackedUpperAndCarvingOverrides() throws IOException {
dimension.setDimensionStack(new IrisDimensionStack());
assertTrue(SavedBiomeRecovery.recover(input).isEmpty());
dimension.setDimensionStack(null);
dimension.setUpperDimension("upper");
assertTrue(SavedBiomeRecovery.recover(input).isEmpty());
dimension.setUpperDimension("none");
dimension.getCarving().add(new IrisDimensionCarvingEntry());
assertTrue(SavedBiomeRecovery.recover(input).isEmpty());
}
@Test
public void rejectsDifferingRiverOverridesAtEachScope() throws IOException {
IrisRiverPolicy policy = new IrisRiverPolicy().setFloodedCaveBiomes(new KList<String>().qadd("other"));
dimension.setRiverPolicy(policy);
assertTrue(SavedBiomeRecovery.recover(input).isEmpty());
dimension.setRiverPolicy(null);
region.setRiverPolicy(policy);
assertTrue(SavedBiomeRecovery.recover(input).isEmpty());
region.setRiverPolicy(null);
biome.setRiverPolicy(policy);
assertTrue(SavedBiomeRecovery.recover(input).isEmpty());
}
@Test
public void rejectsDifferentDimensionAndFocusSelectors() throws IOException {
dimension.setLoadKey("other");
assertTrue(SavedBiomeRecovery.recover(input).isEmpty());
dimension.setLoadKey("overworld");
dimension.setFocus("other");
assertTrue(SavedBiomeRecovery.recover(input).isEmpty());
dimension.setFocus("");
dimension.getRegions().add("other");
assertTrue(SavedBiomeRecovery.recover(input).isEmpty());
}
@Test
public void rejectsOneBlendedColumnEvenWhenAllOtherColumnsAreNew() throws IOException {
TransitionGenerationPlan transition = nextActivation();
when(transition.newEpochWeightAt(-17, 63)).thenReturn(0.999D);
assertTrue(SavedBiomeRecovery.recover(input).isEmpty());
verify(transition).newEpochWeightAt(-17, 63);
}
@Test
public void retainsLaterActivationIdentityForUnblendedChunks() throws IOException {
nextActivation();
SavedBiomeChunk recovered = SavedBiomeRecovery.recover(input).orElseThrow();
assertEquals(2L, recovered.activationId());
assertEquals(2L, recovered.biomeAt(15, 319, 15).activationId());
}
@Test
public void propagatesUnreadableTransitionEvidence() throws IOException {
nextActivation();
when(history.transitionPlan(2L)).thenThrow(new IOException("Unreadable frozen transition"));
assertThrows(IOException.class, () -> SavedBiomeRecovery.recover(input));
}
private TransitionGenerationPlan nextActivation() throws IOException {
when(history.resolveActivation(CHUNK_X, CHUNK_Z))
.thenReturn(GenerationActivation.next(2L, "b".repeat(64), 1L, 2L, 64));
record(semantics(2L).seal().build());
TransitionGenerationPlan transition = mock(TransitionGenerationPlan.class);
when(transition.newEpochWeightAt(anyInt(), anyInt())).thenReturn(1D);
when(history.transitionPlan(2L)).thenReturn(transition);
return transition;
}
private void record(ChunkGenerationSemantics semantics) {
when(history.semantics(CHUNK_X, CHUNK_Z)).thenReturn(Optional.of(semantics));
}
private static ChunkGenerationSemantics.Builder semantics(long activationId) {
return ChunkGenerationSemantics.builder(CHUNK_X, CHUNK_Z, activationId)
.addSurfaceBiome(BIOME).addRegion(REGION);
}
}
@@ -0,0 +1,252 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.framework.BiomeEnvironment;
import art.arcane.iris.engine.framework.PreservationRegistry;
import art.arcane.iris.spi.IrisServices;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class SavedBiomeRuntimeTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
private final IrisEngine engine = mock(IrisEngine.class);
private final GenerationHistory history = mock(GenerationHistory.class);
private final SavedBiomeStore store = mock(SavedBiomeStore.class);
private IrisSettings previousSettings;
@Before
public void prepareServices() throws Exception {
previousSettings = IrisSettings.settings;
IrisSettings.settings = new IrisSettings();
IrisServices.register(PreservationRegistry.class, mock(PreservationRegistry.class));
when(history.savedBiomes()).thenReturn(store);
when(history.paths()).thenReturn(GenerationHistoryPaths.forDimension(temporaryFolder.newFolder().toPath()));
String epochId = "a".repeat(64);
GenerationManifest manifest = mock(GenerationManifest.class);
GenerationEpoch epoch = mock(GenerationEpoch.class);
when(history.manifest()).thenReturn(manifest);
when(manifest.activation(1L)).thenReturn(Optional.of(GenerationActivation.initial(epochId, 1L)));
when(manifest.activation(2L)).thenReturn(Optional.of(GenerationActivation.next(2L, epochId, 1L, 2L, 64)));
when(manifest.epoch(epochId)).thenReturn(Optional.of(epoch));
when(epoch.epochId()).thenReturn(epochId);
}
@After
public void restoreServices() {
IrisServices.clear();
IrisSettings.settings = previousSettings;
}
@Test
public void everyCallerReceivesLoadingWithoutWaitingForDisk() throws Exception {
CountDownLatch entered = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
when(store.get(0, 0)).thenAnswer(invocation -> {
entered.countDown();
assertTrue(release.await(5L, TimeUnit.SECONDS));
return Optional.of(unresolvedChunk(1));
});
SavedBiomeRuntime runtime = new SavedBiomeRuntime(engine, history);
try (ExecutorService caller = Executors.newSingleThreadExecutor()) {
Future<SavedBiomeUnavailableException> response = caller.submit(() -> assertThrows(
SavedBiomeUnavailableException.class, () -> runtime.resolve(0, 0, 0, true)));
assertTrue(entered.await(5L, TimeUnit.SECONDS));
assertTrue(response.get(1L, TimeUnit.SECONDS).isLoading());
} finally {
release.countDown();
runtime.close();
}
}
@Test
public void chunkReadinessDoesNotRequireEveryCellToHaveAResolvedIdentity() throws Exception {
when(store.get(0, 0)).thenReturn(Optional.of(unresolvedChunk(1)));
try (SavedBiomeRuntime runtime = new SavedBiomeRuntime(engine, history)) {
assertTrue(assertThrows(SavedBiomeUnavailableException.class,
() -> runtime.prepareChunk(0, 0)).isLoading());
awaitIdle(runtime);
runtime.prepareChunk(0, 0);
SavedBiomeUnavailableException unavailable = assertThrows(SavedBiomeUnavailableException.class,
() -> runtime.resolve(0, 0, 0, true));
assertEquals(false, unavailable.isLoading());
}
}
@Test
public void closeWaitsForInvalidatedWorkersAndRestoresInterruptStatus() throws Exception {
CountDownLatch entered = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
SavedBiomeChunk chunk = unresolvedChunk(1);
when(store.get(0, 0)).thenAnswer(invocation -> {
entered.countDown();
assertTrue(release.await(5L, TimeUnit.SECONDS));
return Optional.of(chunk);
});
SavedBiomeRuntime runtime = new SavedBiomeRuntime(engine, history);
assertTrue(assertThrows(SavedBiomeUnavailableException.class,
() -> runtime.resolve(0, 0, 0, true)).isLoading());
assertTrue(entered.await(5L, TimeUnit.SECONDS));
runtime.capture(chunk);
assertEquals(1, runtime.pendingQueryCount());
try (ExecutorService closer = Executors.newSingleThreadExecutor()) {
Future<Boolean> closed = closer.submit(() -> {
Thread.currentThread().interrupt();
runtime.close();
return Thread.currentThread().isInterrupted();
});
try {
assertThrows(TimeoutException.class, () -> closed.get(100L, TimeUnit.MILLISECONDS));
} finally {
release.countDown();
}
assertTrue(closed.get(5L, TimeUnit.SECONDS));
assertEquals(0, runtime.pendingQueryCount());
assertEquals(0, runtime.cachedQueryCount());
} finally {
release.countDown();
runtime.close();
}
}
@Test
public void completedSnapshotCacheRespectsItsByteBudgetAndNegativeCacheIsBounded() throws Exception {
when(store.get(anyInt(), anyInt())).thenReturn(Optional.of(unresolvedChunk(16)));
try (SavedBiomeRuntime runtime = new SavedBiomeRuntime(engine, history)) {
for (int chunkX = 0; chunkX < 110; chunkX++) {
int blockX = chunkX * 16;
assertTrue(assertThrows(SavedBiomeUnavailableException.class,
() -> runtime.resolve(blockX, 0, 0, true)).isLoading());
awaitIdle(runtime);
assertTrue(runtime.cachedQueryBytes() <= SavedBiomeChunk.MAXIMUM_ESTIMATED_BYTES);
}
assertTrue(runtime.cachedQueryCount() < 110);
}
when(store.get(anyInt(), anyInt())).thenReturn(Optional.empty());
when(history.isActiveUnowned(anyInt(), anyInt())).thenReturn(true);
when(history.semantics(anyInt(), anyInt())).thenReturn(Optional.empty());
try (SavedBiomeRuntime runtime = new SavedBiomeRuntime(engine, history)) {
for (int chunkX = 0; chunkX < 150; chunkX++) {
int blockX = chunkX * 16;
assertTrue(assertThrows(SavedBiomeUnavailableException.class,
() -> runtime.resolve(blockX, 0, 0, true)).isLoading());
awaitIdle(runtime);
assertEquals(Optional.empty(), runtime.resolve(blockX, 0, 0, true));
assertTrue(runtime.cachedQueryCount() <= 128);
}
}
}
@Test
public void preparedEnvironmentSurvivesLoaderEvictionWithoutReadingPackFilesAgain() throws Exception {
String epochId = "a".repeat(64);
Path pack = history.paths().packRoot(epochId);
Files.createDirectories(pack.resolve("dimensions"));
Files.createDirectories(pack.resolve("biomes"));
Files.createDirectories(pack.resolve("regions"));
Files.writeString(pack.resolve("dimensions/main.json"), "{}");
Files.writeString(pack.resolve("biomes/forest.json"), "{}");
Files.writeString(pack.resolve("regions/main.json"), "{}");
GenerationManifest manifest = mock(GenerationManifest.class);
GenerationEpoch epoch = mock(GenerationEpoch.class);
GenerationEpoch.DimensionContract contract = mock(GenerationEpoch.DimensionContract.class);
when(history.manifest()).thenReturn(manifest);
when(history.packRoot(1L)).thenReturn(pack);
when(history.packRoot(2L)).thenReturn(pack);
when(manifest.activation(1L)).thenReturn(Optional.of(GenerationActivation.initial(epochId, 1L)));
when(manifest.activation(2L)).thenReturn(Optional.of(GenerationActivation.next(2L, epochId, 1L, 2L, 64)));
when(manifest.epoch(epochId)).thenReturn(Optional.of(epoch));
when(epoch.epochId()).thenReturn(epochId);
when(epoch.registryContract()).thenReturn(GenerationRegistryContract.empty());
when(epoch.dimensionContract()).thenReturn(contract);
when(contract.dimensionKey()).thenReturn("main");
SavedBiomeChunk.Cell cell = new SavedBiomeChunk.Cell(1L, "forest", "main");
SavedBiomeChunk.Cell repeatedActivation = new SavedBiomeChunk.Cell(2L, "forest", "main");
SavedBiomeChunk.Cell removed = new SavedBiomeChunk.Cell(2L, "removed", "main");
SavedBiomeChunk.Builder builder = SavedBiomeChunk.builder(new SavedBiomeChunk.Header(0, 0, 2L, -64, 384));
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
SavedBiomeChunk.Cell selected = x < 8 ? cell : x < 12 ? repeatedActivation : removed;
builder.column(x, z, new SavedBiomeChunk.Column(selected, cell,
List.of(new SavedBiomeChunk.Span(-64, 320, selected))));
}
}
when(store.get(0, 0)).thenReturn(Optional.of(builder.build()));
try (SavedBiomeRuntime runtime = new SavedBiomeRuntime(engine, history)) {
assertTrue(assertThrows(SavedBiomeUnavailableException.class,
() -> runtime.resolve(0, 0, 0, true)).isLoading());
awaitIdle(runtime);
runtime.prepareChunk(0, 0);
BiomeEnvironment environment = runtime.resolve(0, 0, 0, true).orElseThrow();
BiomeEnvironment repeated = runtime.resolve(8, 0, 0, true).orElseThrow();
assertEquals(2L, repeated.activationId());
assertSame(environment.data(), repeated.data());
SavedBiomeUnavailableException missing = assertThrows(SavedBiomeUnavailableException.class,
() -> runtime.resolve(12, 0, 0, true));
assertEquals(false, missing.isLoading());
assertTrue(missing.getMessage().contains("removed"));
assertSame(environment, runtime.resolveCaveBase(12, 0).orElseThrow());
environment.data().getBiomeLoader().unload("forest");
environment.data().getRegionLoader().unload("main");
Files.delete(pack.resolve("biomes/forest.json"));
Files.delete(pack.resolve("regions/main.json"));
assertSame(environment, runtime.resolve(0, 0, 0, true).orElseThrow());
assertEquals("forest", environment.biome().getLoadKey());
}
}
private static void awaitIdle(SavedBiomeRuntime runtime) throws InterruptedException {
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5L);
while (runtime.pendingQueryCount() != 0 && System.nanoTime() < deadline) {
Thread.sleep(1L);
}
assertEquals(0, runtime.pendingQueryCount());
}
private static SavedBiomeChunk unresolvedChunk(int runCount) {
SavedBiomeChunk.Cell first = SavedBiomeChunk.Cell.unresolved(1L);
SavedBiomeChunk.Cell second = SavedBiomeChunk.Cell.unresolved(2L);
ArrayList<SavedBiomeChunk.Span> spans = new ArrayList<>(runCount);
for (int index = 0; index < runCount; index++) {
int start = -64 + index * 384 / runCount;
int end = -64 + (index + 1) * 384 / runCount;
spans.add(new SavedBiomeChunk.Span(start, end, (index & 1) == 0 ? first : second));
}
return chunk(new SavedBiomeChunk.Column(first, second, spans));
}
private static SavedBiomeChunk chunk(SavedBiomeChunk.Column column) {
SavedBiomeChunk.Builder builder = SavedBiomeChunk.builder(new SavedBiomeChunk.Header(0, 0, 2L, -64, 384));
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
builder.column(x, z, column);
}
}
return builder.build();
}
}
@@ -0,0 +1,389 @@
package art.arcane.iris.engine.history;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.IOException;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.zip.CRC32;
import java.util.zip.DeflaterOutputStream;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class SavedBiomeStoreTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void preservesEveryColumnHeightRegionAndOwningActivationAcrossReload() throws Exception {
Path root = temporaryFolder.newFolder().toPath();
SavedBiomeChunk original = chunk(-33, 31, 8L);
SavedBiomeStore writer = SavedBiomeStore.open(root);
assertTrue(writer.claimAndPersist(original));
assertFalse(writer.claimAndPersist(original));
SavedBiomeChunk loaded = SavedBiomeStore.open(root).get(-33, 31).orElseThrow();
assertEquals(original, loaded);
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
assertEquals(original.surfaceAt(x, z), loaded.surfaceAt(x, z));
assertEquals(original.caveBaseAt(x, z), loaded.caveBaseAt(x, z));
for (int y = -64; y < 320; y++) {
assertEquals(original.biomeAt(x, y, z), loaded.biomeAt(x, y, z));
}
}
}
assertEquals(7L, loaded.biomeAt(0, -64, 0).activationId());
assertEquals("cave/base-0", loaded.caveBaseAt(0, 0).biomeKey());
assertEquals(8L, loaded.caveBaseAt(0, 0).activationId());
assertFalse(loaded.caveBaseAt(0, 0).equals(loaded.biomeAt(0, -64, 0)));
assertEquals("region/0", loaded.surfaceAt(0, 0).regionKey());
assertEquals("region/1", loaded.surfaceAt(0, 1).regionKey());
assertThrows(IndexOutOfBoundsException.class, () -> loaded.biomeAt(0, -65, 0));
assertThrows(IndexOutOfBoundsException.class, () -> loaded.biomeAt(0, 320, 0));
}
@Test
public void preservesUnresolvedHistoricalCellsAlongsideKnownBiomeAssignments() throws Exception {
Path root = temporaryFolder.newFolder().toPath();
SavedBiomeChunk.Cell unresolved = SavedBiomeChunk.Cell.unresolved(1L);
SavedBiomeChunk.Cell resolved = new SavedBiomeChunk.Cell(2L, "biome/current", "region/current");
SavedBiomeChunk.Builder builder = SavedBiomeChunk.builder(new SavedBiomeChunk.Header(-1, -1, 2L, -64, 384));
List<SavedBiomeChunk.Span> spans = List.of(new SavedBiomeChunk.Span(-64, 16, unresolved),
new SavedBiomeChunk.Span(16, 320, resolved));
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
builder.column(x, z, new SavedBiomeChunk.Column(x < 8 ? unresolved : resolved, unresolved, spans));
}
}
SavedBiomeChunk original = builder.build();
SavedBiomeStore.open(root).claimAndPersist(original);
SavedBiomeChunk loaded = SavedBiomeStore.open(root).get(-1, -1).orElseThrow();
assertEquals(original, loaded);
assertFalse(loaded.surfaceAt(0, 0).isResolved());
assertEquals(unresolved, loaded.caveBaseAt(15, 15));
assertEquals(1L, loaded.surfaceAt(0, 0).activationId());
assertEquals(SavedBiomeChunk.Resolution.UNRESOLVED, loaded.biomeAt(15, -64, 15).resolution());
assertEquals(null, loaded.biomeAt(15, -64, 15).biomeKey());
assertEquals(null, loaded.biomeAt(15, -64, 15).regionKey());
assertTrue(loaded.surfaceAt(8, 0).isResolved());
assertEquals(resolved, loaded.biomeAt(0, 16, 0));
assertThrows(IllegalArgumentException.class, () -> SavedBiomeChunk.Cell.unresolved(0L));
assertThrows(IllegalArgumentException.class, () -> new SavedBiomeChunk.Cell(1L, "biome/known", null));
assertThrows(IllegalArgumentException.class, () -> new SavedBiomeChunk.Cell(1L, null, "region/known"));
assertThrows(IllegalArgumentException.class, () -> new SavedBiomeChunk.Cell(1L, "biome/known", null,
SavedBiomeChunk.Resolution.UNRESOLVED));
assertThrows(IllegalArgumentException.class, () -> new SavedBiomeChunk.Cell(1L, null, "region/known",
SavedBiomeChunk.Resolution.UNRESOLVED));
assertThrows(IllegalArgumentException.class, () -> new SavedBiomeChunk.Cell(1L, null, null,
SavedBiomeChunk.Resolution.RESOLVED));
}
@Test
public void rejectsConflictingClaimsWithoutReplacingSavedContent() throws Exception {
Path root = temporaryFolder.newFolder().toPath();
SavedBiomeStore store = SavedBiomeStore.open(root);
SavedBiomeChunk original = chunk(1, 2, 3L);
store.claimAndPersist(original);
byte[] bytes = Files.readAllBytes(regionPath(root, 0, 0));
assertThrows(IOException.class, () -> store.claimAndPersist(chunk(1, 2, 4L)));
assertTrue(Arrays.equals(bytes, Files.readAllBytes(regionPath(root, 0, 0))));
assertEquals(Optional.of(original), SavedBiomeStore.open(root).get(1, 2));
}
@Test
public void missingSnapshotsLeaveExistingHistoryUntouchedAndCachedReadsAvoidDisk() throws Exception {
Path root = temporaryFolder.newFolder().toPath();
Files.createDirectories(root.resolve("iris/generation/semantics"));
Path semantics = root.resolve("iris/generation/semantics/existing.isem");
Files.writeString(semantics, "unchanged format six data");
SavedBiomeStore store = SavedBiomeStore.open(root);
assertTrue(store.get(0, 0).isEmpty());
assertFalse(Files.exists(store.storageDirectory()));
SavedBiomeChunk chunk = chunk(0, 0, 3L);
store.claimAndPersist(chunk);
Files.delete(regionPath(root, 0, 0));
assertEquals(Optional.of(chunk), store.get(0, 0));
assertEquals(Optional.of(chunk), store.cached(0, 0));
assertEquals("unchanged format six data", Files.readString(semantics));
}
@Test
public void prunesOnlyUnstoredSelectedClaimsAndAllowsReplacementAfterCacheInvalidation() throws Exception {
Path root = temporaryFolder.newFolder().toPath();
SavedBiomeStore store = SavedBiomeStore.open(root);
SavedBiomeChunk stored = chunk(0, 0, 3L);
SavedBiomeChunk orphan = chunk(1, 0, 3L);
SavedBiomeChunk previous = chunk(2, 0, 2L);
SavedBiomeChunk unrelatedRegion = chunk(32, 0, 2L);
store.claimAndPersist(stored);
store.claimAndPersist(orphan);
store.claimAndPersist(previous);
store.claimAndPersist(unrelatedRegion);
byte[] untouched = Files.readAllBytes(regionPath(root, 1, 0));
WorldChunkInventory inventory = WorldChunkInventory.ofPackedChunks(ChunkGenerationOwnership.packChunk(0, 0));
assertEquals(1, store.discardUnstoredClaims(inventory, Set.of(3L)));
assertTrue(store.cached(1, 0).isEmpty());
assertTrue(store.get(1, 0).isEmpty());
assertEquals(Optional.of(stored), store.get(0, 0));
assertEquals(Optional.of(previous), store.get(2, 0));
assertEquals(Optional.of(unrelatedRegion), store.get(32, 0));
assertTrue(Arrays.equals(untouched, Files.readAllBytes(regionPath(root, 1, 0))));
SavedBiomeChunk replacement = chunk(1, 0, 4L);
assertTrue(store.claimAndPersist(replacement));
assertEquals(0, store.discardUnstoredClaims(inventory, Set.of(3L)));
SavedBiomeStore loaded = SavedBiomeStore.open(root);
assertEquals(Optional.of(replacement), loaded.get(1, 0));
assertEquals(Optional.of(stored), loaded.get(0, 0));
assertEquals(Optional.of(previous), loaded.get(2, 0));
assertEquals(1, loaded.discardUnstoredClaims(WorldChunkInventory.empty(), Set.of(4L)));
assertTrue(SavedBiomeStore.open(root).get(1, 0).isEmpty());
assertThrows(IllegalArgumentException.class, () -> loaded.discardUnstoredClaims(inventory, Set.of(0L)));
}
@Test
public void tornLastAppendTruncatesOnlyIncompleteClaimAndAllowsNewAppend() throws Exception {
Path root = temporaryFolder.newFolder().toPath();
SavedBiomeStore store = SavedBiomeStore.open(root);
SavedBiomeChunk first = chunk(0, 0, 3L);
store.claimAndPersist(first);
Path path = regionPath(root, 0, 0);
long firstLength = Files.size(path);
store.claimAndPersist(chunk(1, 0, 3L));
byte[] bytes = Files.readAllBytes(path);
Files.write(path, Arrays.copyOf(bytes, bytes.length - 3));
SavedBiomeStore loaded = SavedBiomeStore.open(root);
assertEquals(Optional.of(first), loaded.get(0, 0));
assertEquals(firstLength, Files.size(path));
assertTrue(loaded.get(1, 0).isEmpty());
assertTrue(loaded.claimAndPersist(chunk(2, 0, 3L)));
assertTrue(SavedBiomeStore.open(root).get(2, 0).isPresent());
}
@Test
public void rejectsChecksumCorruptionAndDuplicateCompleteRecords() throws Exception {
Path root = temporaryFolder.newFolder().toPath();
SavedBiomeStore store = SavedBiomeStore.open(root);
store.claimAndPersist(chunk(0, 0, 3L));
Path path = regionPath(root, 0, 0);
byte[] original = Files.readAllBytes(path);
byte[] corrupt = original.clone();
corrupt[corrupt.length - 1] ^= 1;
Files.write(path, corrupt);
assertThrows(IOException.class, () -> SavedBiomeStore.open(root).get(0, 0));
Files.write(path, original);
Files.write(path, Arrays.copyOfRange(original, 20, original.length), StandardOpenOption.APPEND);
assertThrows(IOException.class, () -> SavedBiomeStore.open(root).get(0, 0));
}
@Test
public void rejectsUnknownFormatEvenWithValidHeaderChecksum() throws Exception {
Path root = temporaryFolder.newFolder().toPath();
SavedBiomeStore.open(root).claimAndPersist(chunk(0, 0, 3L));
Path path = regionPath(root, 0, 0);
byte[] bytes = Files.readAllBytes(path);
ByteBuffer.wrap(bytes).putInt(4, 2);
CRC32 crc = new CRC32();
crc.update(bytes, 0, 16);
ByteBuffer.wrap(bytes).putInt(16, (int) crc.getValue());
Files.write(path, bytes);
assertThrows(IOException.class, () -> SavedBiomeStore.open(root).get(0, 0));
}
@Test
public void rejectsOversizedDecodedHeadersPalettesRunsAndStringsBeforeAllocation() throws Exception {
Path root = temporaryFolder.newFolder().toPath();
SavedBiomeStore.open(root).claimAndPersist(chunk(0, 0, 3L));
Path path = regionPath(root, 0, 0);
rewritePayload(path, new byte[24], Integer.MAX_VALUE);
assertThrows(IOException.class, () -> SavedBiomeStore.open(root).get(0, 0));
for (int mode = 0; mode < 3; mode++) {
ByteArrayOutputStream raw = new ByteArrayOutputStream();
try (DataOutputStream output = new DataOutputStream(raw)) {
output.writeLong(3L);
output.writeInt(-64);
output.writeInt(384);
output.writeInt(mode == 0 ? Integer.MAX_VALUE : 1);
if (mode == 0) {
output.writeInt(0);
} else {
output.writeLong(3L);
output.writeByte(mode == 1 ? 1 : 0);
if (mode == 1) {
output.writeInt(1);
output.writeByte(0);
output.writeByte(0);
output.write(new byte[]{(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, 0x07});
} else {
output.writeShort(65_535);
}
}
}
rewritePayload(path, raw.toByteArray(), raw.size());
assertThrows(IOException.class, () -> SavedBiomeStore.open(root).get(0, 0));
}
}
@Test
public void modelRejectsDenseColumnsBeyondItsDecodedFootprintLimit() {
SavedBiomeChunk.Cell first = new SavedBiomeChunk.Cell(3L, "biome/first", "region/main");
SavedBiomeChunk.Cell second = new SavedBiomeChunk.Cell(3L, "biome/second", "region/main");
ArrayList<SavedBiomeChunk.Span> spans = new ArrayList<>(4096);
for (int y = 0; y < 4096; y++) {
spans.add(new SavedBiomeChunk.Span(y, y + 1, (y & 1) == 0 ? first : second));
}
SavedBiomeChunk.Column column = new SavedBiomeChunk.Column(first, second, spans);
SavedBiomeChunk.Builder builder = SavedBiomeChunk.builder(new SavedBiomeChunk.Header(0, 0, 3L, 0, 4096));
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
builder.column(x, z, column);
}
}
IllegalArgumentException failure = assertThrows(IllegalArgumentException.class, builder::build);
assertTrue(failure.getMessage().contains("footprint limit"));
}
@Test
public void boundedCachesReloadEvictedRegionsAndChunks() throws Exception {
Path root = temporaryFolder.newFolder().toPath();
SavedBiomeStore store = SavedBiomeStore.open(root);
for (int index = 0; index < 140; index++) {
store.claimAndPersist(chunk(index * 32, -32, 3L));
assertTrue(store.cachedChunkCount() <= 128);
assertTrue(store.cachedRegionCount() <= 64);
assertTrue(store.cachedByteCount() <= 64L * 1024L * 1024L);
}
assertTrue(store.cached(0, -32).isEmpty());
assertEquals(Optional.of(chunk(0, -32, 3L)), store.get(0, -32));
assertEquals(Optional.of(chunk(139 * 32, -32, 3L)), SavedBiomeStore.open(root).get(139 * 32, -32));
}
@Test
public void compressesRepeatedVerticalColumnsAndKeepsDistinctExactSurfaceColumns() throws Exception {
Path root = temporaryFolder.newFolder().toPath();
SavedBiomeChunk.Header header = new SavedBiomeChunk.Header(0, 0, 5L, -64, 384);
SavedBiomeChunk.Cell cave = new SavedBiomeChunk.Cell(5L, "biome/cave", "region/main");
List<SavedBiomeChunk.Span> vertical = List.of(new SavedBiomeChunk.Span(-64, 320, cave));
SavedBiomeChunk.Builder builder = SavedBiomeChunk.builder(header);
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
SavedBiomeChunk.Cell surface = new SavedBiomeChunk.Cell(5L, "biome/surface-" + x, "region/" + z);
builder.column(x, z, new SavedBiomeChunk.Column(surface, cave, vertical));
}
}
SavedBiomeChunk original = builder.build();
SavedBiomeStore.open(root).claimAndPersist(original);
assertTrue(Files.size(regionPath(root, 0, 0)) < 4096);
assertEquals(Optional.of(original), SavedBiomeStore.open(root).get(0, 0));
}
@Test
public void concurrentlyPublishesSeparateRegionsAndSingleImmutableClaims() throws Exception {
Path root = temporaryFolder.newFolder().toPath();
SavedBiomeStore store = SavedBiomeStore.open(root);
try (ExecutorService workers = Executors.newFixedThreadPool(4)) {
ArrayList<Future<Boolean>> futures = new ArrayList<>();
for (int index = 0; index < 12; index++) {
int chunkX = (index % 6) * 32;
futures.add(workers.submit(() -> store.claimAndPersist(chunk(chunkX, 0, 3L))));
}
int added = 0;
for (Future<Boolean> future : futures) {
if (future.get()) {
added++;
}
}
assertEquals(6, added);
}
SavedBiomeStore loaded = SavedBiomeStore.open(root);
for (int index = 0; index < 6; index++) {
assertEquals(Optional.of(chunk(index * 32, 0, 3L)), loaded.get(index * 32, 0));
}
}
@Test
public void modelCopiesInputsCompactsRunsAndRejectsGapsMissingColumnsAndInvalidOwners() {
SavedBiomeChunk.Cell cell = new SavedBiomeChunk.Cell(3L, "biome/main", "region/main");
ArrayList<SavedBiomeChunk.Span> spans = new ArrayList<>(List.of(
new SavedBiomeChunk.Span(-64, 0, cell), new SavedBiomeChunk.Span(0, 320, cell)));
SavedBiomeChunk.Column column = new SavedBiomeChunk.Column(cell, cell, spans);
spans.clear();
assertEquals(List.of(new SavedBiomeChunk.Span(-64, 320, cell)), column.vertical());
assertThrows(UnsupportedOperationException.class, () -> column.vertical().clear());
SavedBiomeChunk.Builder builder = SavedBiomeChunk.builder(new SavedBiomeChunk.Header(0, 0, 3L, -64, 384));
assertThrows(NullPointerException.class, builder::build);
assertThrows(IllegalArgumentException.class, () -> new SavedBiomeChunk.Cell(0L, "biome/main", "region/main"));
assertThrows(IllegalArgumentException.class, () -> new SavedBiomeChunk.Header(0, 0, 3L, Integer.MAX_VALUE, 1));
assertThrows(IllegalArgumentException.class, () -> new SavedBiomeChunk.Column(cell, cell, List.of(
new SavedBiomeChunk.Span(-64, 0, cell), new SavedBiomeChunk.Span(1, 320, cell))));
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
builder.column(x, z, column);
}
}
SavedBiomeChunk saved = builder.build();
SavedBiomeChunk.Column shorter = new SavedBiomeChunk.Column(cell, cell, List.of(new SavedBiomeChunk.Span(-64, 319, cell)));
builder.column(0, 0, shorter);
assertEquals(column, saved.column(0, 0));
assertThrows(IllegalArgumentException.class, builder::build);
}
private static void rewritePayload(Path path, byte[] raw, int declaredSize) throws IOException {
byte[] header = Arrays.copyOf(Files.readAllBytes(path), 20);
ByteArrayOutputStream body = new ByteArrayOutputStream();
DataOutputStream coordinates = new DataOutputStream(body);
coordinates.writeInt(0);
coordinates.writeInt(0);
coordinates.writeInt(declaredSize);
try (DeflaterOutputStream compressed = new DeflaterOutputStream(coordinates)) {
compressed.write(raw);
}
byte[] encoded = body.toByteArray();
CRC32 crc = new CRC32();
crc.update(encoded);
ByteArrayOutputStream file = new ByteArrayOutputStream();
try (DataOutputStream output = new DataOutputStream(file)) {
output.write(header);
output.writeInt(encoded.length);
output.write(encoded);
output.writeInt((int) crc.getValue());
}
Files.write(path, file.toByteArray());
}
private static Path regionPath(Path root, int regionX, int regionZ) {
return root.resolve("iris/generation/biomes/r." + regionX + "." + regionZ + ".ibio");
}
private static SavedBiomeChunk chunk(int chunkX, int chunkZ, long activation) {
SavedBiomeChunk.Builder builder = SavedBiomeChunk.builder(new SavedBiomeChunk.Header(chunkX, chunkZ, activation, -64, 384));
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
SavedBiomeChunk.Cell cave = new SavedBiomeChunk.Cell(activation - 1L, "cave/" + (x / 4), "region/" + (z / 4));
SavedBiomeChunk.Cell surface = new SavedBiomeChunk.Cell(activation, "surface/" + x, "region/" + z);
builder.column(x, z, new SavedBiomeChunk.Column(surface,
new SavedBiomeChunk.Cell(activation, "cave/base-" + z, "region/" + z), List.of(
new SavedBiomeChunk.Span(-64, 16 + z, cave), new SavedBiomeChunk.Span(16 + z, 320, surface))));
}
}
return builder.build();
}
}
@@ -0,0 +1,74 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.HeightMap;
import org.bukkit.Location;
import org.bukkit.Particle;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class IrisEffectSurfaceTest {
@Test
public void samplesLiveHeightOnTargetRegionThenEmitsOnPlayerThread() {
World world = mock(World.class);
Player player = mock(Player.class);
Location location = new Location(world, -17, 100, 32);
when(player.getWorld()).thenReturn(world);
when(world.isChunkLoaded(-2, 2)).thenReturn(true);
when(world.getHighestBlockYAt(-17, 32, HeightMap.OCEAN_FLOOR)).thenReturn(23);
AtomicReference<Runnable> regionTask = new AtomicReference<>();
AtomicReference<Runnable> playerTask = new AtomicReference<>();
try (MockedStatic<J> scheduler = mockStatic(J.class)) {
scheduler.when(() -> J.runAt(eq(location), any(Runnable.class))).thenAnswer(call -> {
regionTask.set(call.getArgument(1));
return true;
});
scheduler.when(() -> J.runEntity(eq(player), any(Runnable.class))).thenAnswer(call -> {
playerTask.set(call.getArgument(1));
return true;
});
new IrisEffect().applyParticles(player, Particle.FLAME, location);
assertNotNull(regionTask.get());
assertNull(playerTask.get());
verify(world, never()).getHighestBlockYAt(anyInt(), anyInt(), any(HeightMap.class));
regionTask.get().run();
assertEquals(24D, location.getY(), 0D);
assertNotNull(playerTask.get());
playerTask.get().run();
verify(player).spawnParticle(eq(Particle.FLAME), anyDouble(), eq(24D), anyDouble(),
anyInt(), anyDouble(), anyDouble(), anyDouble());
}
}
@Test
public void unloadedParticleColumnsDoNotLoadTerrainOrEmit() {
World world = mock(World.class);
Player player = mock(Player.class);
Location location = new Location(world, 0, 100, 0);
try (MockedStatic<J> scheduler = mockStatic(J.class)) {
scheduler.when(() -> J.isOwnedByCurrentRegion(world, 0, 0)).thenReturn(true);
new IrisEffect().applyParticles(player, Particle.FLAME, location);
verify(world, never()).getHighestBlockYAt(anyInt(), anyInt(), any(HeightMap.class));
scheduler.verify(() -> J.runEntity(eq(player), any(Runnable.class)), never());
}
}
}