mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-25 13:30:08 +00:00
Merged biome retention
- Saved biome behavior is integrated with current Iris changes
This commit is contained in:
commit
80843114fc
57 files changed
+4460
-385
No files matched your search
+15
-1
@@ -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));
|
||||
}
|
||||
}
|
||||
+28
-1
@@ -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"
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+23
-2
@@ -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);
|
||||
|
||||
+38
-2
@@ -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 {
|
||||
|
||||
+54
-8
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user