Reworked terrain transitions

- New terrain heights meet saved boundaries while retaining local relief
- Surface water levels approach saved fluid heights across the band
- Cave and physical biome matching use a four-block seam patch
- Terrain generates once without speculative neighbor containment
- Bounded caches serve transition membership and boundary lookups
- Saved biome writes honor the selected durability policy
- Release labels and presentation code leave generation identity unchanged
- Unfinished objects retain blocks and metadata across pack changes
- Whole structure assemblies preserve pending excavation and foundations
- New placements keep pending historical pieces clear
- Transition probes cover cutovers and retained object fragments
This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-27 15:31:31 -04:00
1 parent 9eb05fd987
commit 8c3e5d547b
159 files changed
+6092 -1905

No files matched your search

@@ -4,12 +4,12 @@ import art.arcane.volmlib.nativelib.v26_2_R1.terrain.NativeChunkGenerator;
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeTerrainColumns; import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeTerrainColumns;
import art.arcane.iris.platform.bukkit.nms.BukkitTerrainColumnPolicy; import art.arcane.iris.platform.bukkit.nms.BukkitTerrainColumnPolicy;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.GenerationSessionLease; import art.arcane.iris.generation.runtime.GenerationSessionLease;
import art.arcane.iris.world.history.BoundaryColumnGeometry; import art.arcane.iris.world.history.BoundaryColumnGeometry;
import art.arcane.iris.world.history.TerrainBoundarySignature; import art.arcane.iris.world.history.TerrainBoundarySignature;
import art.arcane.iris.testsupport.PlatformLeakGuard; import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.volmlib.nativelib.terrain.NativeTerrainColumnPolicy;
import net.minecraft.SharedConstants; import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap; import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.LevelHeightAccessor; import net.minecraft.world.level.LevelHeightAccessor;
@@ -24,7 +24,7 @@ import java.util.Optional;
import java.util.OptionalInt; import java.util.OptionalInt;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.CALLS_REAL_METHODS; import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
@@ -32,6 +32,7 @@ import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.mockito.Mockito.never;
public final class IrisChunkGeneratorHeightCacheTest { public final class IrisChunkGeneratorHeightCacheTest {
@Rule @Rule
@@ -44,68 +45,72 @@ public final class IrisChunkGeneratorHeightCacheTest {
} }
@Test @Test
public void repeatedNativeHeightSurvivesResolvedTerrainEvictionAndRuntimeChange() throws Exception { public void repeatedNativeHeightUsesLiveHeightQueriesAcrossRuntimeChanges() throws Exception {
Fixture fixture = fixture(); Fixture fixture = fixture();
LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32); LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32);
assertEquals(-11, fixture.generator().getBaseHeight(-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null)); when(fixture.engine().getHeight(-1, -33, false)).thenReturn(4);
when(fixture.complex().resolvedTerrainColumn(-1, -33))
.thenThrow(new IllegalStateException("Full terrain was evicted"));
for (int index = 0; index < 100; index++) { for (int index = 0; index < 100; index++) {
assertEquals(-11, fixture.generator().getBaseHeight( assertEquals(-11, fixture.generator().getBaseHeight(
-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null)); -1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
} }
verify(fixture.complex(), times(1)).resolvedTerrainColumn(-1, -33); verify(fixture.engine(), times(100)).getHeight(-1, -33, false);
when(fixture.engine().getCacheID()).thenReturn(2); when(fixture.engine().getCacheID()).thenReturn(2);
doReturn(Optional.of(signature())).when(fixture.complex()).resolvedTerrainColumn(-1, -33); when(fixture.engine().getHeight(-1, -33, false)).thenReturn(7);
assertEquals(-11, fixture.generator().getBaseHeight(-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null)); assertEquals(-8, fixture.generator().getBaseHeight(-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
verify(fixture.complex(), times(2)).resolvedTerrainColumn(-1, -33); verify(fixture.engine(), never()).getComplex();
verify(fixture.engine(), never()).getMode();
} }
@Test @Test
public void ordinaryHeightRemainsLiveUntilResolvedTransitionTerrainExists() throws Exception { public void nativeHeightPredicatesSelectFluidOrSolidHeightWithoutGeneratingTerrain() throws Exception {
Fixture fixture = fixture(); Fixture fixture = fixture();
LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32); LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32);
when(fixture.complex().resolvedTerrainColumn(0, 0))
.thenReturn(Optional.empty(), Optional.empty(), Optional.of(signature()));
when(fixture.engine().getHeight(0, 0, false)).thenReturn(20, 25); when(fixture.engine().getHeight(0, 0, false)).thenReturn(20, 25);
when(fixture.engine().getHeight(0, 0, true)).thenReturn(8);
assertEquals(5, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null)); assertEquals(5, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
assertEquals(10, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null)); assertEquals(10, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
assertEquals(-11, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null)); assertEquals(-7, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.OCEAN_FLOOR, bounds, null));
assertEquals(-11, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
verify(fixture.engine(), times(2)).getHeight(0, 0, false); verify(fixture.engine(), times(2)).getHeight(0, 0, false);
verify(fixture.complex(), times(3)).resolvedTerrainColumn(0, 0); verify(fixture.engine(), times(1)).getHeight(0, 0, true);
verify(fixture.engine(), never()).getComplex();
verify(fixture.engine(), never()).getMode();
} }
@Test @Test
public void nativePredicatesAndAccessorBoundsRemainExact() throws Exception { public void nativePredicatesAndAccessorBoundsRemainExact() throws Exception {
Fixture fixture = fixture(); NativeTerrainColumnPolicy policy = mock(NativeTerrainColumnPolicy.class);
NativeTerrainColumnPolicy.ColumnSession session = mock(NativeTerrainColumnPolicy.ColumnSession.class);
when(policy.openQuery(any())).thenReturn(session);
when(policy.placementKey(anyString())).thenAnswer(invocation -> invocation.getArgument(0));
doReturn(Optional.of(signature().geometry())).when(session).resolvedColumn();
NativeChunkGenerator<?, ?, ?, ?, ?> generator = generator(policy);
LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32); LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32);
for (int repeat = 0; repeat < 2; repeat++) { for (int repeat = 0; repeat < 2; repeat++) {
assertEquals(-11, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null)); assertEquals(-11, generator.getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
assertEquals(-13, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.OCEAN_FLOOR, bounds, null)); assertEquals(-13, generator.getBaseHeight(0, 0, Heightmap.Types.OCEAN_FLOOR, bounds, null));
assertEquals(-13, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING, bounds, null)); assertEquals(-13, generator.getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING, bounds, null));
assertEquals(-14, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, bounds, null)); assertEquals(-14, generator.getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, bounds, null));
assertEquals(-14, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, assertEquals(-14, generator.getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE,
LevelHeightAccessor.create(-16, 2), null)); LevelHeightAccessor.create(-16, 2), null));
assertEquals(-14, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, assertEquals(-14, generator.getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING_NO_LEAVES,
LevelHeightAccessor.create(-14, 3), null)); LevelHeightAccessor.create(-14, 3), null));
} }
verify(fixture.complex(), times(6)).resolvedTerrainColumn(0, 0); verify(session, times(6)).resolvedColumn();
} }
private static Fixture fixture() throws Exception { private static Fixture fixture() throws Exception {
Engine engine = mock(Engine.class); Engine engine = mock(Engine.class);
IrisComplex complex = mock(IrisComplex.class);
when(engine.getComplex()).thenReturn(complex);
when(engine.getCacheID()).thenReturn(1); when(engine.getCacheID()).thenReturn(1);
when(engine.acquireGenerationLease(anyString())).thenReturn(GenerationSessionLease.noop()); when(engine.acquireGenerationLease(anyString())).thenReturn(GenerationSessionLease.noop());
when(complex.resolvedTerrainColumn(anyInt(), anyInt())).thenReturn(Optional.of(signature())); return new Fixture(generator(new BukkitTerrainColumnPolicy(engine, null)), engine);
}
private static NativeChunkGenerator<?, ?, ?, ?, ?> generator(NativeTerrainColumnPolicy policy) throws Exception {
NativeChunkGenerator<?, ?, ?, ?, ?> generator = mock(NativeChunkGenerator.class, CALLS_REAL_METHODS); NativeChunkGenerator<?, ?, ?, ?, ?> generator = mock(NativeChunkGenerator.class, CALLS_REAL_METHODS);
set(generator, "terrainColumns", new NativeTerrainColumns(new BukkitTerrainColumnPolicy(engine, null))); set(generator, "terrainColumns", new NativeTerrainColumns(policy));
return new Fixture(generator, engine, complex); return generator;
} }
private static void set(NativeChunkGenerator<?, ?, ?, ?, ?> generator, String name, Object value) throws Exception { private static void set(NativeChunkGenerator<?, ?, ?, ?, ?> generator, String name, Object value) throws Exception {
@@ -127,6 +132,6 @@ public final class IrisChunkGeneratorHeightCacheTest {
new TerrainBoundarySignature.BiomeEncoding(List.of("minecraft:plains"), new short[]{0})), geometry); new TerrainBoundarySignature.BiomeEncoding(List.of("minecraft:plains"), new short[]{0})), geometry);
} }
private record Fixture(NativeChunkGenerator<?, ?, ?, ?, ?> generator, Engine engine, IrisComplex complex) { private record Fixture(NativeChunkGenerator<?, ?, ?, ?, ?> generator, Engine engine) {
} }
} }
@@ -4,12 +4,12 @@ import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeChunkGenerator;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeTerrainColumns; import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeTerrainColumns;
import art.arcane.iris.platform.bukkit.nms.BukkitTerrainColumnPolicy; import art.arcane.iris.platform.bukkit.nms.BukkitTerrainColumnPolicy;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.GenerationSessionLease; import art.arcane.iris.generation.runtime.GenerationSessionLease;
import art.arcane.iris.world.history.BoundaryColumnGeometry; import art.arcane.iris.world.history.BoundaryColumnGeometry;
import art.arcane.iris.world.history.TerrainBoundarySignature; import art.arcane.iris.world.history.TerrainBoundarySignature;
import art.arcane.iris.testsupport.PlatformLeakGuard; import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.volmlib.nativelib.terrain.NativeTerrainColumnPolicy;
import net.minecraft.SharedConstants; import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap; import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.LevelHeightAccessor; import net.minecraft.world.level.LevelHeightAccessor;
@@ -24,7 +24,7 @@ import java.util.Optional;
import java.util.OptionalInt; import java.util.OptionalInt;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.CALLS_REAL_METHODS; import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
@@ -32,6 +32,7 @@ import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.mockito.Mockito.never;
public final class IrisChunkGeneratorHeightCacheTest { public final class IrisChunkGeneratorHeightCacheTest {
@Rule @Rule
@@ -45,68 +46,72 @@ public final class IrisChunkGeneratorHeightCacheTest {
} }
@Test @Test
public void repeatedNativeHeightSurvivesResolvedTerrainEvictionAndRuntimeChange() throws Exception { public void repeatedNativeHeightUsesLiveHeightQueriesAcrossRuntimeChanges() throws Exception {
Fixture fixture = fixture(); Fixture fixture = fixture();
LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32); LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32);
assertEquals(-11, fixture.generator().getBaseHeight(-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null)); when(fixture.engine().getHeight(-1, -33, false)).thenReturn(4);
when(fixture.complex().resolvedTerrainColumn(-1, -33))
.thenThrow(new IllegalStateException("Full terrain was evicted"));
for (int index = 0; index < 100; index++) { for (int index = 0; index < 100; index++) {
assertEquals(-11, fixture.generator().getBaseHeight( assertEquals(-11, fixture.generator().getBaseHeight(
-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null)); -1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
} }
verify(fixture.complex(), times(1)).resolvedTerrainColumn(-1, -33); verify(fixture.engine(), times(100)).getHeight(-1, -33, false);
when(fixture.engine().getCacheID()).thenReturn(2); when(fixture.engine().getCacheID()).thenReturn(2);
doReturn(Optional.of(signature())).when(fixture.complex()).resolvedTerrainColumn(-1, -33); when(fixture.engine().getHeight(-1, -33, false)).thenReturn(7);
assertEquals(-11, fixture.generator().getBaseHeight(-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null)); assertEquals(-8, fixture.generator().getBaseHeight(-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
verify(fixture.complex(), times(2)).resolvedTerrainColumn(-1, -33); verify(fixture.engine(), never()).getComplex();
verify(fixture.engine(), never()).getMode();
} }
@Test @Test
public void ordinaryHeightRemainsLiveUntilResolvedTransitionTerrainExists() throws Exception { public void nativeHeightPredicatesSelectFluidOrSolidHeightWithoutGeneratingTerrain() throws Exception {
Fixture fixture = fixture(); Fixture fixture = fixture();
LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32); LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32);
when(fixture.complex().resolvedTerrainColumn(0, 0))
.thenReturn(Optional.empty(), Optional.empty(), Optional.of(signature()));
when(fixture.engine().getHeight(0, 0, false)).thenReturn(20, 25); when(fixture.engine().getHeight(0, 0, false)).thenReturn(20, 25);
when(fixture.engine().getHeight(0, 0, true)).thenReturn(8);
assertEquals(5, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null)); assertEquals(5, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
assertEquals(10, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null)); assertEquals(10, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
assertEquals(-11, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null)); assertEquals(-7, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.OCEAN_FLOOR, bounds, null));
assertEquals(-11, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
verify(fixture.engine(), times(2)).getHeight(0, 0, false); verify(fixture.engine(), times(2)).getHeight(0, 0, false);
verify(fixture.complex(), times(3)).resolvedTerrainColumn(0, 0); verify(fixture.engine(), times(1)).getHeight(0, 0, true);
verify(fixture.engine(), never()).getComplex();
verify(fixture.engine(), never()).getMode();
} }
@Test @Test
public void nativePredicatesAndAccessorBoundsRemainExact() throws Exception { public void nativePredicatesAndAccessorBoundsRemainExact() throws Exception {
Fixture fixture = fixture(); NativeTerrainColumnPolicy policy = mock(NativeTerrainColumnPolicy.class);
NativeTerrainColumnPolicy.ColumnSession session = mock(NativeTerrainColumnPolicy.ColumnSession.class);
when(policy.openQuery(any())).thenReturn(session);
when(policy.placementKey(anyString())).thenAnswer(invocation -> invocation.getArgument(0));
doReturn(Optional.of(signature().geometry())).when(session).resolvedColumn();
NativeChunkGenerator<?, ?, ?, ?, ?> generator = generator(policy);
LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32); LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32);
for (int repeat = 0; repeat < 2; repeat++) { for (int repeat = 0; repeat < 2; repeat++) {
assertEquals(-11, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null)); assertEquals(-11, generator.getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
assertEquals(-13, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.OCEAN_FLOOR, bounds, null)); assertEquals(-13, generator.getBaseHeight(0, 0, Heightmap.Types.OCEAN_FLOOR, bounds, null));
assertEquals(-13, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING, bounds, null)); assertEquals(-13, generator.getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING, bounds, null));
assertEquals(-14, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, bounds, null)); assertEquals(-14, generator.getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, bounds, null));
assertEquals(-14, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, assertEquals(-14, generator.getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE,
LevelHeightAccessor.create(-16, 2), null)); LevelHeightAccessor.create(-16, 2), null));
assertEquals(-14, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, assertEquals(-14, generator.getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING_NO_LEAVES,
LevelHeightAccessor.create(-14, 3), null)); LevelHeightAccessor.create(-14, 3), null));
} }
verify(fixture.complex(), times(6)).resolvedTerrainColumn(0, 0); verify(session, times(6)).resolvedColumn();
} }
private static Fixture fixture() throws Exception { private static Fixture fixture() throws Exception {
Engine engine = mock(Engine.class); Engine engine = mock(Engine.class);
IrisComplex complex = mock(IrisComplex.class);
when(engine.getComplex()).thenReturn(complex);
when(engine.getCacheID()).thenReturn(1); when(engine.getCacheID()).thenReturn(1);
when(engine.acquireGenerationLease(anyString())).thenReturn(GenerationSessionLease.noop()); when(engine.acquireGenerationLease(anyString())).thenReturn(GenerationSessionLease.noop());
when(complex.resolvedTerrainColumn(anyInt(), anyInt())).thenReturn(Optional.of(signature())); return new Fixture(generator(new BukkitTerrainColumnPolicy(engine, null)), engine);
}
private static NativeChunkGenerator<?, ?, ?, ?, ?> generator(NativeTerrainColumnPolicy policy) throws Exception {
NativeChunkGenerator<?, ?, ?, ?, ?> generator = mock(NativeChunkGenerator.class, CALLS_REAL_METHODS); NativeChunkGenerator<?, ?, ?, ?, ?> generator = mock(NativeChunkGenerator.class, CALLS_REAL_METHODS);
set(generator, "terrainColumns", new NativeTerrainColumns(new BukkitTerrainColumnPolicy(engine, null))); set(generator, "terrainColumns", new NativeTerrainColumns(policy));
return new Fixture(generator, engine, complex); return generator;
} }
private static void set(NativeChunkGenerator<?, ?, ?, ?, ?> generator, String name, Object value) throws Exception { private static void set(NativeChunkGenerator<?, ?, ?, ?, ?> generator, String name, Object value) throws Exception {
@@ -128,6 +133,6 @@ public final class IrisChunkGeneratorHeightCacheTest {
new TerrainBoundarySignature.BiomeEncoding(List.of("minecraft:plains"), new short[]{0})), geometry); new TerrainBoundarySignature.BiomeEncoding(List.of("minecraft:plains"), new short[]{0})), geometry);
} }
private record Fixture(NativeChunkGenerator<?, ?, ?, ?, ?> generator, Engine engine, IrisComplex complex) { private record Fixture(NativeChunkGenerator<?, ?, ?, ?, ?> generator, Engine engine) {
} }
} }
@@ -66,7 +66,6 @@ import art.arcane.iris.world.history.TerrainNativeBlockKeys;
import art.arcane.volmlib.nativelib.terrain.NativeChunkWritePolicy; import art.arcane.volmlib.nativelib.terrain.NativeChunkWritePolicy;
import art.arcane.iris.world.history.TerrainBoundarySignature;
import art.arcane.iris.world.history.NativeBiomeSpawnSelection; import art.arcane.iris.world.history.NativeBiomeSpawnSelection;
import art.arcane.iris.pack.loading.IrisData; import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.pack.PackValidationRegistry; import art.arcane.iris.pack.PackValidationRegistry;
@@ -1086,7 +1085,7 @@ public final class IrisModdedChunkGenerator implements NativeGeneratorOwner, Nat
@Override @Override
public NativeBlockColumn resolvedColumn(Engine current, int x, int z) { public NativeBlockColumn resolvedColumn(Engine current, int x, int z) {
return current.getComplex().resolvedTerrainColumn(x, z).map(TerrainBoundarySignature::geometry).orElse(null); return null;
} }
@Override @Override
@@ -102,7 +102,8 @@ final class ModdedNativeStructurePolicy implements NativeModdedStructureStage.Po
@Override @Override
public NativeBlockPositionPredicate protectedPositions(Engine context) { public NativeBlockPositionPredicate protectedPositions(Engine context) {
return GenerationWritePolicy.protectedPositions(context.getDimension().getStaticObjectLayer(context.getData()), return GenerationWritePolicy.protectedPositions(context.getDimension().getStaticObjectLayer(context.getData())
.forTransition(context),
context.getDimensionStackContext(), context.getMinHeight()); context.getDimensionStackContext(), context.getMinHeight());
} }
@@ -0,0 +1,40 @@
package art.arcane.iris.modded;
import art.arcane.iris.generation.runtime.Engine;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
public final class ModdedTerrainHeightQueryTest {
@BeforeClass
public static void bootstrap() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void nativeQueriesUseLiveHeightsWithoutGeneratingTerrain() {
IrisModdedChunkGenerator generator = mock(IrisModdedChunkGenerator.class, CALLS_REAL_METHODS);
Engine engine = mock(Engine.class);
assertNull(generator.resolvedColumn(engine, -1, -33));
verifyNoInteractions(engine);
when(engine.getHeight(-1, -33, true)).thenReturn(12, 18);
when(engine.getHeight(-1, -33, false)).thenReturn(24);
assertEquals(12, generator.terrainHeight(engine, -1, -33, true, true));
assertEquals(18, generator.terrainHeight(engine, -1, -33, true, true));
assertEquals(24, generator.terrainHeight(engine, -1, -33, false, true));
verify(engine, never()).getComplex();
verify(engine, never()).getMode();
}
}
@@ -29,6 +29,10 @@ public final class GenerationRevisionScope {
"core/src/main/java/art/arcane/iris/world/safeguard/Diagnostic.java", "core/src/main/java/art/arcane/iris/world/safeguard/Diagnostic.java",
"core/src/main/java/art/arcane/iris/world/safeguard/ValueWithDiagnostics.java", "core/src/main/java/art/arcane/iris/world/safeguard/ValueWithDiagnostics.java",
"core/src/main/java/art/arcane/iris/diagnostics/splash/IrisSplashRenderer.java", "core/src/main/java/art/arcane/iris/diagnostics/splash/IrisSplashRenderer.java",
"core/src/main/java/art/arcane/iris/diagnostics/splash/IrisSplashComposer.java",
"core/src/main/java/art/arcane/iris/diagnostics/splash/IrisSplashPackScanner.java",
"adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedIrisSplash.java",
"adapters/bukkit/plugin/src/main/java/art/arcane/iris/command/CommandDebug.java",
"adapters/bukkit/plugin/src/main/java/art/arcane/iris/command/CommandDeveloper.java", "adapters/bukkit/plugin/src/main/java/art/arcane/iris/command/CommandDeveloper.java",
"adapters/bukkit/plugin/src/main/java/art/arcane/iris/command/CommandPregen.java", "adapters/bukkit/plugin/src/main/java/art/arcane/iris/command/CommandPregen.java",
"adapters/bukkit/plugin/src/main/java/art/arcane/iris/command/CommandIris.java", "adapters/bukkit/plugin/src/main/java/art/arcane/iris/command/CommandIris.java",
@@ -33,6 +33,31 @@ public class GenerationBuildRevisionTest {
GenerationBuildRevision.fingerprint(GenerationBuildRevision.capture(fixture.options(), fixture.dependencies()))); GenerationBuildRevision.fingerprint(GenerationBuildRevision.capture(fixture.options(), fixture.dependencies())));
} }
@Test
public void dependencyReleaseNamesAndMavenMetadataDoNotChangeGenerationIdentity() throws Exception {
Fixture fixture = fixture();
String original = GenerationBuildRevision.fingerprint(GenerationBuildRevision.read(fixture.revision()));
Path released = fixture.root().resolve("math-2.0.jar");
try (ZipOutputStream output = new ZipOutputStream(Files.newOutputStream(released))) {
for (Map.Entry<String, String> entry : Map.of(
"META-INF/MANIFEST.MF", "Manifest-Version: 1.0\r\nImplementation-Version: 2.0\r\nBuild-Time: 12345\r\n\r\n",
"META-INF/maven/example/math/pom.properties", "groupId=example\nartifactId=math\nversion=2.0\n",
"META-INF/maven/example/math/pom.xml", "<project><version>2.0</version></project>",
"example/Math.class", "same code").entrySet()) {
ZipEntry archiveEntry = new ZipEntry(entry.getKey());
archiveEntry.setTime(1_700_000_000_000L);
output.putNextEntry(archiveEntry);
output.write(entry.getValue().getBytes(StandardCharsets.UTF_8));
output.closeEntry();
}
}
Map<String, Path> dependencies = Map.of("math", released);
GenerationBuildRevision.verifySnapshot(fixture.root(), fixture.revision(), dependencies);
assertEquals(original, GenerationBuildRevision.fingerprint(
GenerationBuildRevision.capture(fixture.options(), dependencies)));
}
@Test @Test
public void rejectsChangedAddedAndDeletedGenerationSources() throws Exception { public void rejectsChangedAddedAndDeletedGenerationSources() throws Exception {
Fixture fixture = fixture(); Fixture fixture = fixture();
@@ -44,6 +44,25 @@ public class GenerationRevisionScopeTest {
} }
} }
@Test
public void releaseVersionAndExpandedPluginMetadataPreserveTheGenerationFingerprint() throws Exception {
Fixture fixture = fixture();
String original = GenerationBuildRevision.fingerprint(GenerationBuildRevision.read(fixture.revision()));
for (String version : List.of("4.1.2-26.2", "4.1.3-26.2", "4.1.3-26.2-build-123")) {
write(fixture.root().resolve("gradle.properties"), "irisVersion=" + version + "\n");
write(fixture.root().resolve("adapters/bukkit/plugin/src/main/resources/paper-plugin.yml"),
"name: Iris\nversion: " + version + "\n");
write(fixture.root().resolve("adapters/bukkit/plugin/build/resources/main/paper-plugin.yml"),
"name: Iris\nversion: " + version + "\n");
write(fixture.root().resolve("adapters/fabric/build/resources/main/fabric.mod.json"),
"{\"id\":\"iris\",\"version\":\"" + version + "\"}\n");
GenerationBuildRevision.verifySnapshot(fixture.root(), fixture.revision(), fixture.dependencies());
assertEquals(original, GenerationBuildRevision.fingerprint(
GenerationBuildRevision.capture(fixture.options(), fixture.dependencies())));
}
}
@Test @Test
public void generationChangesProduceANewBuildRevision() throws Exception { public void generationChangesProduceANewBuildRevision() throws Exception {
Fixture fixture = fixture(); Fixture fixture = fixture();
@@ -88,6 +107,10 @@ public class GenerationRevisionScopeTest {
} }
assertFalse(manifest.sources().containsKey(ENGINE_ROOT + "EngineDiagnostics.java")); assertFalse(manifest.sources().containsKey(ENGINE_ROOT + "EngineDiagnostics.java"));
assertFalse(manifest.sources().containsKey(COMMAND_ROOT + "CommandIris.java")); assertFalse(manifest.sources().containsKey(COMMAND_ROOT + "CommandIris.java"));
assertFalse(manifest.sources().containsKey(COMMAND_ROOT + "CommandDebug.java"));
assertFalse(manifest.sources().containsKey("core/src/main/java/art/arcane/iris/diagnostics/splash/IrisSplashComposer.java"));
assertFalse(manifest.sources().containsKey("core/src/main/java/art/arcane/iris/diagnostics/splash/IrisSplashPackScanner.java"));
assertFalse(manifest.sources().containsKey("adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedIrisSplash.java"));
} }
@Test @Test
@@ -21,7 +21,6 @@ import java.util.concurrent.CompletableFuture;
public class ChunkContext { public class ChunkContext {
private boolean naturalTerrain = true; private boolean naturalTerrain = true;
private boolean speculativeTerrain;
private Hunk<NativeBiome> terrainBiomes; private Hunk<NativeBiome> terrainBiomes;
private FloatingBiomeOverlay floatingBiomes; private FloatingBiomeOverlay floatingBiomes;
private final int x; private final int x;
@@ -164,14 +163,6 @@ public class ChunkContext {
} }
} }
public boolean isSpeculativeTerrain() {
return speculativeTerrain;
}
public void beginSpeculativeTerrain() {
speculativeTerrain = true;
}
public boolean isNaturalTerrain() { public boolean isNaturalTerrain() {
return naturalTerrain; return naturalTerrain;
} }
@@ -2,17 +2,14 @@ package art.arcane.iris.generation.decoration;
import art.arcane.iris.generation.runtime.IrisComplex; import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.world.history.TerrainBoundarySignature;
import art.arcane.iris.generation.hydrology.HydrologyColumnLayer; import art.arcane.iris.generation.hydrology.HydrologyColumnLayer;
import art.arcane.iris.generation.hydrology.HydrologyColumnSample; import art.arcane.iris.generation.hydrology.HydrologyColumnSample;
import art.arcane.iris.generation.biome.IrisBiome; import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState; import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.volmlib.util.hunk.Hunk; import art.arcane.volmlib.util.hunk.Hunk;
import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.math.RNG;
import java.util.Optional;
final class IrisSugarCane { final class IrisSugarCane {
private static final int[][] NEIGHBORS = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}}; private static final int[][] NEIGHBORS = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
@@ -74,11 +71,6 @@ final class IrisSugarCane {
private static boolean plannedWater(Engine engine, int x, int y, int z) { private static boolean plannedWater(Engine engine, int x, int y, int z) {
IrisComplex complex = engine.getComplex(); IrisComplex complex = engine.getComplex();
Optional<TerrainBoundarySignature> resolved = complex.resolvedTerrainColumn(x, z);
if (resolved.isPresent()) {
String key = resolved.get().geometry().voxelAt(y + engine.getMinHeight()).stateKey();
return supportsAdjacent(IrisPlatforms.get().registries().blockOrNull(key));
}
HydrologyColumnSample sample = complex.sampleHydrologyColumn(x, z); HydrologyColumnSample sample = complex.sampleHydrologyColumn(x, z);
HydrologyColumnLayer fluid = sample == null ? null : sample.primarySurfaceFluidLayerOrNull(); HydrologyColumnLayer fluid = sample == null ? null : sample.primarySurfaceFluidLayerOrNull();
if (fluid != null) { if (fluid != null) {
@@ -21,8 +21,6 @@ package art.arcane.iris.generation.mantle;
import art.arcane.iris.configuration.IrisSettings; import art.arcane.iris.configuration.IrisSettings;
import art.arcane.iris.pack.loading.IrisData; import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.generation.runtime.IrisComplex; import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.world.history.TerrainBoundarySignature;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.generation.runtime.DimensionStackContext; import art.arcane.iris.generation.runtime.DimensionStackContext;
import art.arcane.iris.generation.runtime.DimensionStackLayout; import art.arcane.iris.generation.runtime.DimensionStackLayout;
import art.arcane.iris.generation.runtime.UpperDimensionContext; import art.arcane.iris.generation.runtime.UpperDimensionContext;
@@ -55,8 +53,6 @@ import org.jetbrains.annotations.UnmodifiableView;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
public interface EngineMantle extends MatterGenerator { public interface EngineMantle extends MatterGenerator {
@@ -112,10 +108,6 @@ public interface EngineMantle extends MatterGenerator {
} }
default int getHighest(int x, int z, IrisData data, boolean ignoreFluid) { default int getHighest(int x, int z, IrisData data, boolean ignoreFluid) {
OptionalInt resolved = getComplex().resolvedTerrainHeight(x, z, ignoreFluid);
if (resolved.isPresent()) {
return resolved.getAsInt();
}
return ignoreFluid ? trueHeight(x, z) : Math.max(trueHeight(x, z), getFluidHeight(x, z)); return ignoreFluid ? trueHeight(x, z) : Math.max(trueHeight(x, z), getFluidHeight(x, z));
} }
@@ -124,21 +116,6 @@ public interface EngineMantle extends MatterGenerator {
} }
default boolean isCarved(int x, int h, int z) { default boolean isCarved(int x, int h, int z) {
Optional<TerrainBoundarySignature> resolved = getComplex().resolvedTerrainColumn(x, z);
if (resolved.isPresent()) {
PreObjectMatterCell cell = getMantle().get(x, h, z, PreObjectMatterCell.class);
if (cell != null && cell.blockCaptured()) {
NativeBlockState block = getMantle().get(x, h, z, NativeBlockState.class);
if (block != null) {
return (block.isAir() || block.isFluid())
&& resolved.get().geometry().hasSolidAbove(h + getEngine().getMinHeight());
}
}
if (cell != null && cell.cavernCaptured()) {
return getMantle().get(x, h, z, MatterCavern.class) != null;
}
return resolved.get().geometry().isEnclosedOpenAt(h + getEngine().getMinHeight());
}
HydrologyCaveCell hydrology = HydrologyCaveStorage.getIfPresent(getMantle(), x, h, z); HydrologyCaveCell hydrology = HydrologyCaveStorage.getIfPresent(getMantle(), x, h, z);
if (hydrology != null) { if (hydrology != null) {
return hydrology.carves(); return hydrology.carves();
@@ -149,18 +126,6 @@ public interface EngineMantle extends MatterGenerator {
default NativeBlockState get(int x, int y, int z) { default NativeBlockState get(int x, int y, int z) {
NativeBlockState block = getMantle().get(x, y, z, NativeBlockState.class); NativeBlockState block = getMantle().get(x, y, z, NativeBlockState.class);
Optional<TerrainBoundarySignature> resolved = getComplex().resolvedTerrainColumn(x, z);
if (resolved.isPresent()) {
PreObjectMatterCell cell = getMantle().get(x, y, z, PreObjectMatterCell.class);
if (cell == null || !cell.blockCaptured()) {
String stateKey = resolved.get().geometry().voxelAt(y + getEngine().getMinHeight()).stateKey();
NativeBlockState natural = IrisPlatforms.get().registries().blockOrNull(stateKey);
if (natural == null) {
throw new IllegalStateException("Saved terrain state is unavailable: " + stateKey);
}
return natural;
}
}
return block == null ? AIR.get() : block; return block == null ? AIR.get() : block;
} }
@@ -177,10 +142,6 @@ public interface EngineMantle extends MatterGenerator {
} }
default int getFluidHeight(int x, int z) { default int getFluidHeight(int x, int z) {
Optional<TerrainBoundarySignature> resolved = getComplex().resolvedTerrainColumn(x, z);
if (resolved.isPresent()) {
return resolved.get().fluidHeight().orElse(-1);
}
return (int) Math.round(getComplex().getRiverWaterSurfaceStream().get(x, z)); return (int) Math.round(getComplex().getRiverWaterSurfaceStream().get(x, z));
} }
@@ -29,16 +29,24 @@ import art.arcane.volmlib.util.collection.KList;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.function.Supplier;
import java.util.Objects;
import art.arcane.volmlib.util.matter.IrisMatter;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.iris.world.storage.matter.IrisMatterSupport;
import java.util.Map; import java.util.Map;
final class FloatingObjectPlacementTransaction implements IObjectPlacer { final class FloatingObjectPlacementTransaction implements IObjectPlacer {
private final IslandObjectPlacer delegate; private final IObjectPlacer delegate;
private final ObjectContinuationBundle.PlacementKey placementKey;
private final KList<BufferedMutation> mutations; private final KList<BufferedMutation> mutations;
private final Map<PositionKey, NativeBlockState> bufferedBlocks; private final Map<PositionKey, NativeBlockState> bufferedBlocks;
private int blockWrites; private int blockWrites;
FloatingObjectPlacementTransaction(IslandObjectPlacer delegate) { FloatingObjectPlacementTransaction(IObjectPlacer delegate, ObjectContinuationBundle.PlacementKey placementKey) {
this.delegate = delegate; this.delegate = Objects.requireNonNull(delegate);
this.placementKey = Objects.requireNonNull(placementKey);
this.mutations = new KList<>(); this.mutations = new KList<>();
this.bufferedBlocks = new HashMap<>(); this.bufferedBlocks = new HashMap<>();
} }
@@ -56,7 +64,8 @@ final class FloatingObjectPlacementTransaction implements IObjectPlacer {
discard(); discard();
return CommitResult.REJECTED_TRANSITION; return CommitResult.REJECTED_TRANSITION;
} }
if (!delegate.canWriteObjectBlock(mutation.x(), mutation.y(), mutation.z())) { if (delegate instanceof IslandObjectPlacer island
&& !island.canWriteObjectBlock(mutation.x(), mutation.y(), mutation.z())) {
discard(); discard();
return CommitResult.REJECTED_SUPPORT; return CommitResult.REJECTED_SUPPORT;
} }
@@ -64,10 +73,38 @@ final class FloatingObjectPlacementTransaction implements IObjectPlacer {
for (BufferedMutation mutation : mutations) { for (BufferedMutation mutation : mutations) {
mutation.apply(delegate); mutation.apply(delegate);
} }
persistContinuation(engine);
discard(); discard();
return CommitResult.COMMITTED; return CommitResult.COMMITTED;
} }
private void persistContinuation(Engine engine) {
int minimumX = Integer.MAX_VALUE;
int minimumZ = Integer.MAX_VALUE;
int maximumX = Integer.MIN_VALUE;
int maximumZ = Integer.MIN_VALUE;
for (BufferedMutation mutation : mutations) {
minimumX = Math.min(minimumX, mutation.x());
minimumZ = Math.min(minimumZ, mutation.z());
maximumX = Math.max(maximumX, mutation.x());
maximumZ = Math.max(maximumZ, mutation.z());
}
if ((minimumX >> 4) == (maximumX >> 4) && (minimumZ >> 4) == (maximumZ >> 4)) {
return;
}
IrisMatterSupport.ensureRegistered();
LinkedHashMap<ObjectContinuationBundle.ChunkPosition, Matter> payloads = new LinkedHashMap<>();
for (BufferedMutation mutation : mutations) {
ObjectContinuationBundle.ChunkPosition chunk = new ObjectContinuationBundle.ChunkPosition(mutation.x() >> 4, mutation.z() >> 4);
Matter payload = payloads.computeIfAbsent(chunk, ignored -> new IrisMatter(16, engine.getHeight(), 16));
ObjectContinuationPersistence.put(payload, mutation.x() & 15, mutation.y(), mutation.z() & 15, mutation.value());
}
LinkedHashMap<ObjectContinuationBundle.ChunkPosition, Supplier<Matter>> destinations = new LinkedHashMap<>();
payloads.forEach((chunk, payload) -> destinations.put(chunk, () -> payload));
ObjectContinuationPersistence.persist(engine.getMantle().getMantle(), placementKey,
new ObjectContinuationBundle.Bounds(minimumX, minimumZ, maximumX, maximumZ), destinations);
}
void discard() { void discard() {
mutations.clear(); mutations.clear();
bufferedBlocks.clear(); bufferedBlocks.clear();
@@ -81,12 +81,14 @@ public class IrisStructureComponent extends IrisMantleComponent {
if (!complex.allowsNewGenerationChunk(x, z)) { if (!complex.allowsNewGenerationChunk(x, z)) {
return; return;
} }
int ordinal = 0;
for (IrisStructurePlacement placement : StructurePlacementScope.placementsAt( for (IrisStructurePlacement placement : StructurePlacementScope.placementsAt(
getEngineMantle().getEngine(), x, z)) { getEngineMantle().getEngine(), x, z)) {
int placementOrdinal = ordinal++;
if (!placement.hasIrisStructures()) { if (!placement.hasIrisStructures()) {
continue; continue;
} }
placeFromPlacement(writer, placement, x, z, complex); placeFromPlacement(writer, placement, x, z, placementOrdinal);
} }
} }
@@ -96,7 +98,7 @@ public class IrisStructureComponent extends IrisMantleComponent {
IrisStructurePlacement placement, IrisStructurePlacement placement,
int cx, int cx,
int cz, int cz,
IrisComplex complex int ordinal
) { ) {
IrisStructureLocator.ResolvedPlacement resolved = IrisStructureLocator.resolvePlacement( IrisStructureLocator.ResolvedPlacement resolved = IrisStructureLocator.resolvePlacement(
getEngineMantle().getEngine(), placement, cx, cz); getEngineMantle().getEngine(), placement, cx, cz);
@@ -117,16 +119,7 @@ public class IrisStructureComponent extends IrisMantleComponent {
return; return;
} }
IrisStructureTerrain terrain = placement.resolvedTerrain(); IrisStructureTerrain terrain = placement.resolvedTerrain();
int[] bounds = computePieceBounds(pieces); if (!IrisStructureLocator.allowsResolvedFootprint(getEngineMantle().getEngine(), resolved)) {
int horizontalPadding = terrain.resolvedMode() == StructureTerrainMode.FORCE_CARVE
|| terrain.resolvedMode() == StructureTerrainMode.BORE
? Math.max(0, terrain.getHorizontalPadding())
: 0;
if (bounds == null || !complex.allowsNewGenerationFootprint(
saturatedOffset(bounds[0], -horizontalPadding),
saturatedOffset(bounds[2], -horizontalPadding),
saturatedOffset(bounds[3], horizontalPadding),
saturatedOffset(bounds[5], horizontalPadding))) {
return; return;
} }
RNG rng = resolved.rng(); RNG rng = resolved.rng();
@@ -135,60 +128,66 @@ public class IrisStructureComponent extends IrisMantleComponent {
IrisLogging.debug("[StructTrace] ASSEMBLED chunk=" + cx + "," + cz + " key=" + key + " baseY=" + baseY + " pieces=" + pieces.size()); IrisLogging.debug("[StructTrace] ASSEMBLED chunk=" + cx + "," + cz + " key=" + key + " baseY=" + baseY + " pieces=" + pieces.size());
} }
if (!placement.isAnchoredUnderground()) { try (MantleWriter.ObjectPlacementCapture capture = writer.captureObjectPlacement(
clearIntersectingObjectTrees(writer, resolved); new ObjectContinuationBundle.PlacementKey(ObjectContinuationBundle.Kind.STRUCTURE, cx, cz, ordinal))) {
} if (!placement.isAnchoredUnderground()) {
clearIntersectingObjectTrees(writer, resolved);
}
StructureTerrainMode terrainMode = terrain.resolvedMode(); StructureTerrainMode terrainMode = terrain.resolvedMode();
if (terrainMode == StructureTerrainMode.FORCE_CARVE) { if (terrainMode == StructureTerrainMode.FORCE_CARVE) {
forceCarveStructure(writer, pieces, terrain); forceCarveStructure(writer, pieces, terrain);
} else if (terrainMode == StructureTerrainMode.BORE) { } else if (terrainMode == StructureTerrainMode.BORE) {
boreStructure(writer, pieces, terrain); boreStructure(writer, pieces, terrain);
} else if (terrainMode != StructureTerrainMode.PRESERVE } else if (terrainMode != StructureTerrainMode.PRESERVE
&& terrainMode != StructureTerrainMode.SOURCE) { && terrainMode != StructureTerrainMode.SOURCE) {
throw new IllegalStateException("Iris assembly terrain mode " + terrainMode throw new IllegalStateException("Iris assembly terrain mode " + terrainMode
+ " is not implemented for structure '" + key + "'"); + " is not implemented for structure '" + key + "'");
} }
ObjectPlaceMode mode = structure.getPlaceMode(); ObjectPlaceMode mode = structure.getPlaceMode();
int failedPieces = 0; int failedPieces = 0;
IrisStructureStiltSettings stilt = placement.getStilt(); IrisStructureStiltSettings stilt = placement.getStilt();
Long2IntOpenHashMap foundationColumns = stilt == null Long2IntOpenHashMap foundationColumns = stilt == null
? null : new Long2IntOpenHashMap(); ? null : new Long2IntOpenHashMap();
boolean supportNonOccluding = stilt != null && stilt.isSupportNonOccluding(); boolean supportNonOccluding = stilt != null && stilt.isSupportNonOccluding();
if (placement.isAnchoredUnderground()) { if (placement.isAnchoredUnderground()) {
ObjectPlaceMode undergroundMode = (mode == ObjectPlaceMode.ORGANIC_STILT || mode == ObjectPlaceMode.CEILING_HANG) ObjectPlaceMode undergroundMode = (mode == ObjectPlaceMode.ORGANIC_STILT || mode == ObjectPlaceMode.CEILING_HANG)
? mode : ObjectPlaceMode.STRUCTURE_PIECE; ? mode : ObjectPlaceMode.STRUCTURE_PIECE;
for (PlacedStructurePiece p : pieces) { for (PlacedStructurePiece p : pieces) {
if (placeObject(writer, structure, p, undergroundMode, p.getY(), rng, if (placeObject(writer, structure, p, undergroundMode, p.getY(), rng,
foundationColumns, supportNonOccluding) == -1) {
failedPieces++;
}
}
} else if (mode == ObjectPlaceMode.STRUCTURE_PIECE || mode == ObjectPlaceMode.FLOATING) {
for (PlacedStructurePiece p : pieces) {
if (placeObject(writer, structure, p, ObjectPlaceMode.STRUCTURE_PIECE, p.getY(), rng,
foundationColumns, supportNonOccluding) == -1) {
failedPieces++;
}
}
} else if (pieces.size() == 1) {
if (placeObject(writer, structure, pieces.getFirst(), mode, -1, rng,
foundationColumns, supportNonOccluding) == -1) { foundationColumns, supportNonOccluding) == -1) {
failedPieces++; failedPieces++;
} }
} } else {
} else if (mode == ObjectPlaceMode.STRUCTURE_PIECE || mode == ObjectPlaceMode.FLOATING) { for (PlacedStructurePiece p : pieces) {
for (PlacedStructurePiece p : pieces) { if (placeObject(writer, structure, p, ObjectPlaceMode.STRUCTURE_PIECE, p.getY(), rng,
if (placeObject(writer, structure, p, ObjectPlaceMode.STRUCTURE_PIECE, p.getY(), rng, foundationColumns, supportNonOccluding) == -1) {
foundationColumns, supportNonOccluding) == -1) { failedPieces++;
failedPieces++; }
} }
} }
} else if (pieces.size() == 1) { requireAppliedPieces(resolved, cx, cz, failedPieces);
if (placeObject(writer, structure, pieces.getFirst(), mode, -1, rng, if (failedPieces == 0 && stilt != null) {
foundationColumns, supportNonOccluding) == -1) { placeFoundation(writer, foundationColumns, stilt, rng, !placement.isAnchoredUnderground());
failedPieces++;
} }
} else { if (failedPieces == 0) {
for (PlacedStructurePiece p : pieces) { capture.commit();
if (placeObject(writer, structure, p, ObjectPlaceMode.STRUCTURE_PIECE, p.getY(), rng,
foundationColumns, supportNonOccluding) == -1) {
failedPieces++;
}
} }
} }
requireAppliedPieces(resolved, cx, cz, failedPieces);
if (failedPieces == 0 && stilt != null) {
placeFoundation(writer, foundationColumns, stilt, rng, !placement.isAnchoredUnderground());
}
} }
private void placeFoundation(MantleWriter writer, Long2IntOpenHashMap columns, private void placeFoundation(MantleWriter writer, Long2IntOpenHashMap columns,
@@ -558,11 +557,6 @@ public class IrisStructureComponent extends IrisMantleComponent {
return new int[]{minX, minY, minZ, maxX, maxY, maxZ}; return new int[]{minX, minY, minZ, maxX, maxY, maxZ};
} }
private static int saturatedOffset(int coordinate, int offset) {
long result = (long) coordinate + offset;
return (int) Math.max(Integer.MIN_VALUE, Math.min(Integer.MAX_VALUE, result));
}
private int placeObject(MantleWriter writer, IrisStructure structure, PlacedStructurePiece p, private int placeObject(MantleWriter writer, IrisStructure structure, PlacedStructurePiece p,
ObjectPlaceMode mode, int y, RNG rng, Long2IntOpenHashMap foundationColumns, ObjectPlaceMode mode, int y, RNG rng, Long2IntOpenHashMap foundationColumns,
boolean supportNonOccluding) { boolean supportNonOccluding) {
@@ -18,6 +18,7 @@
package art.arcane.iris.generation.mantle; package art.arcane.iris.generation.mantle;
import java.io.UncheckedIOException;
import art.arcane.iris.pack.loading.IrisData; import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.generation.runtime.IrisComplex; import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.cache.Cache; import art.arcane.iris.generation.cache.Cache;
@@ -65,6 +66,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
return; return;
} }
IrisData data = getData(); IrisData data = getData();
PlacementSequence placements = new PlacementSequence();
int minX = x << 4; int minX = x << 4;
int minZ = z << 4; int minZ = z << 4;
RNG chunkRng = new RNG(Cache.key(x, z) + seed() + 0x0FA710BEL); RNG chunkRng = new RNG(Cache.key(x, z) + seed() + 0x0FA710BEL);
@@ -122,7 +124,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
KList<IrisObjectPlacement> floating = entry.getFloatingObjects(); KList<IrisObjectPlacement> floating = entry.getFloatingObjects();
if (floating != null && !floating.isEmpty()) { if (floating != null && !floating.isEmpty()) {
for (IrisObjectPlacement placement : floating) { for (IrisObjectPlacement placement : floating) {
tryPlaceFloatingChunk(writer, complex, chunkRng, data, placement, columns, minX, minZ, entry); tryPlaceFloatingChunk(writer, complex, chunkRng, data, placement, columns, minX, minZ, entry, placements);
} }
} }
@@ -135,12 +137,12 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
interior = interiorColumns(sampleResolver, columns, minX, minZ, entry, IslandObjectPlacer.AnchorFace.TOP); interior = interiorColumns(sampleResolver, columns, minX, minZ, entry, IslandObjectPlacer.AnchorFace.TOP);
if (hasSurface) { if (hasSurface) {
for (IrisObjectPlacement placement : surface) { for (IrisObjectPlacement placement : surface) {
tryPlaceAnchoredChunk(writer, complex, chunkRng, data, placement, samples, sampleResolver, columns, interior, minX, minZ, entry); tryPlaceAnchoredChunk(writer, complex, chunkRng, data, placement, samples, sampleResolver, columns, interior, minX, minZ, entry, placements);
} }
} }
if (hasExtras) { if (hasExtras) {
for (IrisObjectPlacement placement : extras) { for (IrisObjectPlacement placement : extras) {
tryPlaceAnchoredChunk(writer, complex, chunkRng, data, placement, samples, sampleResolver, columns, interior, minX, minZ, entry); tryPlaceAnchoredChunk(writer, complex, chunkRng, data, placement, samples, sampleResolver, columns, interior, minX, minZ, entry, placements);
} }
} }
} }
@@ -159,14 +161,14 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
if (bottom != null && !bottom.isEmpty()) { if (bottom != null && !bottom.isEmpty()) {
KList<Integer> interior = interiorColumns(sampleResolver, columns, minX, minZ, entry, IslandObjectPlacer.AnchorFace.BOTTOM); KList<Integer> interior = interiorColumns(sampleResolver, columns, minX, minZ, entry, IslandObjectPlacer.AnchorFace.BOTTOM);
for (IrisObjectPlacement placement : bottom) { for (IrisObjectPlacement placement : bottom) {
tryPlaceInvertedChunk(writer, complex, chunkRng, data, placement, samples, sampleResolver, columns, interior, minX, minZ, entry); tryPlaceInvertedChunk(writer, complex, chunkRng, data, placement, samples, sampleResolver, columns, interior, minX, minZ, entry, placements);
} }
} }
} }
} }
@ChunkCoordinates @ChunkCoordinates
private void tryPlaceFloatingChunk(MantleWriter writer, IrisComplex complex, RNG rng, IrisData data, IrisObjectPlacement placement, KList<Integer> columns, int minX, int minZ, IrisFloatingChildBiomes entry) { private void tryPlaceFloatingChunk(MantleWriter writer, IrisComplex complex, RNG rng, IrisData data, IrisObjectPlacement placement, KList<Integer> columns, int minX, int minZ, IrisFloatingChildBiomes entry, PlacementSequence placements) {
if (placement == null || columns == null || columns.isEmpty()) { if (placement == null || columns == null || columns.isEmpty()) {
return; return;
} }
@@ -210,21 +212,32 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
} }
int id = rng.i(0, Integer.MAX_VALUE); int id = rng.i(0, Integer.MAX_VALUE);
FloatingObjectPlacementTransaction transaction = new FloatingObjectPlacementTransaction(
writer, placements.next(minX >> 4, minZ >> 4));
try { try {
obj.place(xx, -1, zz, writer, floatingPlacement, rng, (b, bd) -> { int resultY = obj.place(xx, -1, zz, transaction, floatingPlacement, rng, (b, bd) -> {
String marker = placementMarker(obj, id); String marker = placementMarker(obj, id);
if (marker != null && shouldWritePlacementMarker(writer, bd, b.getX(), b.getY(), b.getZ())) { if (marker != null && shouldWritePlacementMarker(transaction, bd, b.getX(), b.getY(), b.getZ())) {
writer.setData(b.getX(), b.getY(), b.getZ(), marker); transaction.setData(b.getX(), b.getY(), b.getZ(), marker);
} }
}, null, data); }, null, data);
if (resultY < 0) {
transaction.discard();
} else {
transaction.commit();
}
} catch (UncheckedIOException failure) {
transaction.discard();
throw failure;
} catch (Throwable e) { } catch (Throwable e) {
transaction.discard();
IrisLogging.reportError(e); IrisLogging.reportError(e);
} }
} }
} }
@ChunkCoordinates @ChunkCoordinates
private void tryPlaceAnchoredChunk(MantleWriter writer, IrisComplex complex, RNG rng, IrisData data, IrisObjectPlacement placement, FloatingIslandSample[] samples, IslandObjectPlacer.SampleProvider sampleProvider, KList<Integer> columns, KList<Integer> interior, int minX, int minZ, IrisFloatingChildBiomes entry) { private void tryPlaceAnchoredChunk(MantleWriter writer, IrisComplex complex, RNG rng, IrisData data, IrisObjectPlacement placement, FloatingIslandSample[] samples, IslandObjectPlacer.SampleProvider sampleProvider, KList<Integer> columns, KList<Integer> interior, int minX, int minZ, IrisFloatingChildBiomes entry, PlacementSequence placements) {
if (placement == null || columns.isEmpty()) { if (placement == null || columns.isEmpty()) {
return; return;
} }
@@ -293,7 +306,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
int yv = pickTopY + 1 - fp.getLowestSolidKeyY(); int yv = pickTopY + 1 - fp.getLowestSolidKeyY();
IslandObjectPlacer islandPlacer = IslandObjectPlacer.top(writer, sampleProvider, entry, pickTopY); IslandObjectPlacer islandPlacer = IslandObjectPlacer.top(writer, sampleProvider, entry, pickTopY);
FloatingObjectPlacementTransaction transaction = new FloatingObjectPlacementTransaction(islandPlacer); FloatingObjectPlacementTransaction transaction = new FloatingObjectPlacementTransaction(islandPlacer, placements.next(minX >> 4, minZ >> 4));
int id = rng.i(0, Integer.MAX_VALUE); int id = rng.i(0, Integer.MAX_VALUE);
try { try {
@@ -308,6 +321,9 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
} else { } else {
transaction.commit(); transaction.commit();
} }
} catch (UncheckedIOException failure) {
transaction.discard();
throw failure;
} catch (Throwable e) { } catch (Throwable e) {
transaction.discard(); transaction.discard();
IrisLogging.reportError(e); IrisLogging.reportError(e);
@@ -316,7 +332,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
} }
@ChunkCoordinates @ChunkCoordinates
private void tryPlaceInvertedChunk(MantleWriter writer, IrisComplex complex, RNG rng, IrisData data, IrisObjectPlacement placement, FloatingIslandSample[] samples, IslandObjectPlacer.SampleProvider sampleProvider, KList<Integer> columns, KList<Integer> interior, int minX, int minZ, IrisFloatingChildBiomes entry) { private void tryPlaceInvertedChunk(MantleWriter writer, IrisComplex complex, RNG rng, IrisData data, IrisObjectPlacement placement, FloatingIslandSample[] samples, IslandObjectPlacer.SampleProvider sampleProvider, KList<Integer> columns, KList<Integer> interior, int minX, int minZ, IrisFloatingChildBiomes entry, PlacementSequence placements) {
if (placement == null || columns.isEmpty()) { if (placement == null || columns.isEmpty()) {
return; return;
} }
@@ -402,7 +418,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
int yv = invertedBaseY(pickBottomY, fp, invertedRotation); int yv = invertedBaseY(pickBottomY, fp, invertedRotation);
IslandObjectPlacer islandPlacer = IslandObjectPlacer.bottom(writer, sampleProvider, entry, pickBottomY); IslandObjectPlacer islandPlacer = IslandObjectPlacer.bottom(writer, sampleProvider, entry, pickBottomY);
FloatingObjectPlacementTransaction transaction = new FloatingObjectPlacementTransaction(islandPlacer); FloatingObjectPlacementTransaction transaction = new FloatingObjectPlacementTransaction(islandPlacer, placements.next(minX >> 4, minZ >> 4));
int id = rng.i(0, Integer.MAX_VALUE); int id = rng.i(0, Integer.MAX_VALUE);
try { try {
@@ -417,6 +433,9 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
} else { } else {
transaction.commit(); transaction.commit();
} }
} catch (UncheckedIOException failure) {
transaction.discard();
throw failure;
} catch (Throwable e) { } catch (Throwable e) {
transaction.discard(); transaction.discard();
IrisLogging.reportError(e); IrisLogging.reportError(e);
@@ -636,4 +655,12 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
} }
return radius; return radius;
} }
private static final class PlacementSequence {
private int ordinal;
private ObjectContinuationBundle.PlacementKey next(int sourceX, int sourceZ) {
return new ObjectContinuationBundle.PlacementKey(ObjectContinuationBundle.Kind.FLOATING, sourceX, sourceZ, ordinal++);
}
}
} }
@@ -23,9 +23,6 @@ import art.arcane.iris.configuration.IrisSettings;
import art.arcane.iris.generation.block.B; import art.arcane.iris.generation.block.B;
import art.arcane.iris.generation.cache.Cache; import art.arcane.iris.generation.cache.Cache;
import art.arcane.iris.generation.runtime.IrisComplex; import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.world.history.TerrainBoundarySignature;
import art.arcane.iris.world.history.BoundaryColumnGeometry;
import java.util.Optional;
import art.arcane.iris.generation.runtime.UpperDimensionContext; import art.arcane.iris.generation.runtime.UpperDimensionContext;
import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial; import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial;
@@ -145,11 +142,12 @@ public class MantleObjectComponent extends IrisMantleComponent {
x, x,
z, z,
getRadius(), getRadius(),
(sourceX, sourceZ) -> transaction.apply(sourcePlans.get( (sourceX, sourceZ) -> {
sourceX, ObjectSourcePlan plan = sourcePlans.get(sourceX, sourceZ,
sourceZ, () -> buildSourcePlan(writer, sourceX, sourceZ, context));
() -> buildSourcePlan(writer, sourceX, sourceZ, context) transaction.apply(plan);
)) plan.persistContinuations(writer, sourceX, sourceZ, x, z);
}
); );
transaction.commit(); transaction.commit();
} }
@@ -668,6 +666,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
try { try {
int placeResult = -1; int placeResult = -1;
CaveObjectPlacementTransaction.CommitResult commitResult = CaveObjectPlacementTransaction.CommitResult.EMPTY; CaveObjectPlacementTransaction.CommitResult commitResult = CaveObjectPlacementTransaction.CommitResult.EMPTY;
int continuation = writer.beginObjectPlacement();
if (carving) { if (carving) {
int caveFloorY = caveAnchor.y(); int caveFloorY = caveAnchor.y();
if (golden) { if (golden) {
@@ -707,6 +706,9 @@ public class MantleObjectComponent extends IrisMantleComponent {
} }
}, null, getData()); }, null, getData());
} }
if (placeResult >= 0 && (!carving || commitResult == CaveObjectPlacementTransaction.CommitResult.COMMITTED)) {
writer.endObjectPlacement(continuation);
}
if (golden) { if (golden) {
IrisLogging.debug("Goldendebug procedural result: chunk=" + x + "," + z IrisLogging.debug("Goldendebug procedural result: chunk=" + x + "," + z
+ " placement=" + p.getName() + " placement=" + p.getName()
@@ -778,19 +780,10 @@ public class MantleObjectComponent extends IrisMantleComponent {
private boolean acceptsCaveAnchorAt(int x, int y, int z, boolean underwater, private boolean acceptsCaveAnchorAt(int x, int y, int z, boolean underwater,
MatterCavern cavern, HydrologyCaveCell hydrology) { MatterCavern cavern, HydrologyCaveCell hydrology) {
Optional<TerrainBoundarySignature> resolved = getEngineMantle().getComplex().resolvedTerrainColumn(x, z);
if (resolved.isPresent()) {
return acceptsResolvedCaveAnchor(underwater,
resolved.get().geometry().voxelAt(y + getEngineMantle().getEngine().getMinHeight()));
}
return acceptsCaveAnchorFluid(underwater, hydrology == null ? cavern : hydrology.asCavern(), return acceptsCaveAnchorFluid(underwater, hydrology == null ? cavern : hydrology.asCavern(),
hydrology, y, getDimension().getCaveLavaHeight()); hydrology, y, getDimension().getCaveLavaHeight());
} }
static boolean acceptsResolvedCaveAnchor(boolean underwater, BoundaryColumnGeometry.Voxel voxel) {
return !voxel.protectedContent() && (voxel.phase() == BoundaryColumnGeometry.Phase.AIR
|| underwater && voxel.phase() == BoundaryColumnGeometry.Phase.FLUID);
}
static boolean acceptsCaveAnchorFluid(boolean underwater, MatterCavern cavern, int y, int lavaHeight) { static boolean acceptsCaveAnchorFluid(boolean underwater, MatterCavern cavern, int y, int lavaHeight) {
return acceptsCaveAnchorFluid(underwater, cavern, null, y, lavaHeight); return acceptsCaveAnchorFluid(underwater, cavern, null, y, lavaHeight);
@@ -914,6 +907,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
} }
try { try {
String marker = placementMarker(v, id, "surface"); String marker = placementMarker(v, id, "surface");
int continuation = writer.beginObjectPlacement();
int result = v.place(xx, -1, zz, placePlacer, effectivePlacement, rng, (b, data) -> { int result = v.place(xx, -1, zz, placePlacer, effectivePlacement, rng, (b, data) -> {
if (marker != null) { if (marker != null) {
writer.setData(b.getX(), b.getY(), b.getZ(), marker); writer.setData(b.getX(), b.getY(), b.getZ(), marker);
@@ -927,6 +921,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
}, null, getData()); }, null, getData());
if (result >= 0) { if (result >= 0) {
writer.endObjectPlacement(continuation);
placed++; placed++;
} else { } else {
rejected++; rejected++;
@@ -1096,6 +1091,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
IrisObjectPlacement effectivePlacement = resolveCavePlacement(objectPlacement, object, caveProfile); IrisObjectPlacement effectivePlacement = resolveCavePlacement(objectPlacement, object, caveProfile);
try { try {
int continuation = writer.beginObjectPlacement();
ContainedPlacementResult contained = placeContainedCaveObject( ContainedPlacementResult contained = placeContainedCaveObject(
writer, writer,
object, object,
@@ -1112,6 +1108,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
int result = contained.resultY(); int result = contained.resultY();
boolean wroteBlocks = contained.commitResult() == CaveObjectPlacementTransaction.CommitResult.COMMITTED; boolean wroteBlocks = contained.commitResult() == CaveObjectPlacementTransaction.CommitResult.COMMITTED;
if (wroteBlocks) { if (wroteBlocks) {
writer.endObjectPlacement(continuation);
placed++; placed++;
} else { } else {
rejected++; rejected++;
@@ -1286,7 +1283,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
} }
boolean treePlacement = isTreePlacement(v, objectPlacement); boolean treePlacement = isTreePlacement(v, objectPlacement);
String marker = placementMarker(v, id, "upper"); String marker = placementMarker(v, id, "upper");
int continuation = writer.beginObjectPlacement();
int result = v.place(xx, anchorY, zz, writer, placement, rng, (b, data) -> { int result = v.place(xx, anchorY, zz, writer, placement, rng, (b, data) -> {
if (marker != null) { if (marker != null) {
writer.setData(b.getX(), b.getY(), b.getZ(), marker); writer.setData(b.getX(), b.getY(), b.getZ(), marker);
@@ -1299,6 +1296,9 @@ public class MantleObjectComponent extends IrisMantleComponent {
} }
}, null, getData()); }, null, getData());
if (result >= 0) {
writer.endObjectPlacement(continuation);
}
if (traceRegen) { if (traceRegen) {
IrisLogging.debug("Upper object placement: chunk=" + chunkX + "," + chunkZ IrisLogging.debug("Upper object placement: chunk=" + chunkX + "," + chunkZ
+ " scope=" + scope + " scope=" + scope
@@ -32,9 +32,6 @@ import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.IrisComplex; import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.terrain.Terrain3DColumn; import art.arcane.iris.generation.terrain.Terrain3DColumn;
import art.arcane.iris.world.history.TransitionGenerationPlan; import art.arcane.iris.world.history.TransitionGenerationPlan;
import art.arcane.iris.world.history.TerrainBoundarySignature;
import art.arcane.iris.world.history.BoundaryColumnGeometry;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.generation.noise.IrisGeneratorStyle; import art.arcane.iris.generation.noise.IrisGeneratorStyle;
import art.arcane.iris.pack.value.IrisPosition; import art.arcane.iris.pack.value.IrisPosition;
import art.arcane.iris.generation.block.TileData; import art.arcane.iris.generation.block.TileData;
@@ -46,6 +43,7 @@ import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk; import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.matter.Matter; import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.IrisMatter;
import art.arcane.volmlib.util.matter.MatterCavern; import art.arcane.volmlib.util.matter.MatterCavern;
import art.arcane.volmlib.util.matter.MatterSlice; import art.arcane.volmlib.util.matter.MatterSlice;
import art.arcane.volmlib.util.noise.CNG; import art.arcane.volmlib.util.noise.CNG;
@@ -61,9 +59,13 @@ import art.arcane.iris.generation.block.B;
import org.bukkit.util.Vector; import org.bukkit.util.Vector;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.function.Supplier;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import java.util.Map;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.concurrent.atomic.AtomicReferenceArray; import java.util.concurrent.atomic.AtomicReferenceArray;
@@ -86,6 +88,8 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
@EqualsAndHashCode.Exclude @EqualsAndHashCode.Exclude
@ToString.Exclude @ToString.Exclude
private final ThreadLocal<Integer> activeComponentPriority = new ThreadLocal<>(); private final ThreadLocal<Integer> activeComponentPriority = new ThreadLocal<>();
@Getter(AccessLevel.NONE)
private final ThreadLocal<ObjectPlacementCapture> objectPlacementCapture = new ThreadLocal<>();
public MantleWriter(EngineMantle engineMantle, Mantle<Matter> mantle, int x, int z, int radius, boolean multicore) { public MantleWriter(EngineMantle engineMantle, Mantle<Matter> mantle, int x, int z, int radius, boolean multicore) {
this(engineMantle, mantle, x, z, radius, radius * 2, multicore); this(engineMantle, mantle, x, z, radius, radius * 2, multicore);
@@ -292,6 +296,22 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
(int) Math.round(style.warp(rng, data, z, -y, z, x)), t); (int) Math.round(style.warp(rng, data, z, -y, z, x)), t);
} }
public ObjectPlacementCapture captureObjectPlacement(ObjectContinuationBundle.PlacementKey key) {
if (objectPlacementCapture.get() != null) {
throw new IllegalStateException("Object placement capture is already active");
}
ObjectPlacementCapture capture = new ObjectPlacementCapture(key);
objectPlacementCapture.set(capture);
return capture;
}
private void recordObjectValue(int x, int y, int z, Class<?> type, Object value) {
ObjectPlacementCapture capture = objectPlacementCapture.get();
if (capture != null) {
capture.set(x, y, z, type, value);
}
}
public <T> void setData(int x, int y, int z, T t) { public <T> void setData(int x, int y, int z, T t) {
if (t == null || !allowsWrite(x, z)) { if (t == null || !allowsWrite(x, z)) {
return; return;
@@ -323,6 +343,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
Class<?> sliceType = t instanceof NativeBlockState ? NativeBlockState.class : matter.getClass(t); Class<?> sliceType = t instanceof NativeBlockState ? NativeBlockState.class : matter.getClass(t);
capturePreObjectOriginal(matter, x, y, z, sliceType); capturePreObjectOriginal(matter, x, y, z, sliceType);
matter.slice(sliceType).set(x & 15, y & 15, z & 15, t); matter.slice(sliceType).set(x & 15, y & 15, z & 15, t);
recordObjectValue(x, y, z, sliceType, t);
} }
} }
@@ -361,6 +382,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
cavernSlice = matter.slice(MatterCavern.class); cavernSlice = matter.slice(MatterCavern.class);
} }
cavernSlice.set(x & 15, y & 15, z & 15, value); cavernSlice.set(x & 15, y & 15, z & 15, value);
recordObjectValue(x, y, z, MatterCavern.class, value);
return true; return true;
} }
} }
@@ -384,6 +406,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
if (blockSlice != null) { if (blockSlice != null) {
capturePreObjectOriginal(matter, x, y, z, NativeBlockState.class); capturePreObjectOriginal(matter, x, y, z, NativeBlockState.class);
blockSlice.set(x & 15, y & 15, z & 15, null); blockSlice.set(x & 15, y & 15, z & 15, null);
recordObjectValue(x, y, z, NativeBlockState.class, AIR.get());
} }
clearDeferredPlacement(matter, x, y, z); clearDeferredPlacement(matter, x, y, z);
MatterSlice<MatterCavern> cavernSlice = matter.getSlice(MatterCavern.class); MatterSlice<MatterCavern> cavernSlice = matter.getSlice(MatterCavern.class);
@@ -395,6 +418,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
cavernSlice = matter.slice(MatterCavern.class); cavernSlice = matter.slice(MatterCavern.class);
} }
cavernSlice.set(x & 15, y & 15, z & 15, value); cavernSlice.set(x & 15, y & 15, z & 15, value);
recordObjectValue(x, y, z, MatterCavern.class, value);
return true; return true;
} }
} }
@@ -413,14 +437,13 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
if (hasProtectedHydrology(matter, x, y, z)) { if (hasProtectedHydrology(matter, x, y, z)) {
return; return;
} }
MatterSlice<NativeBlockState> blockSlice = matter.getSlice(NativeBlockState.class); capturePreObjectOriginal(matter, x, y, z, NativeBlockState.class);
if (blockSlice != null) { matter.<NativeBlockState>slice(NativeBlockState.class).set(x & 15, y & 15, z & 15, AIR.get());
capturePreObjectOriginal(matter, x, y, z, NativeBlockState.class);
blockSlice.set(x & 15, y & 15, z & 15, null);
}
clearDeferredPlacement(matter, x, y, z); clearDeferredPlacement(matter, x, y, z);
capturePreObjectOriginal(matter, x, y, z, MatterCavern.class); capturePreObjectOriginal(matter, x, y, z, MatterCavern.class);
matter.<MatterCavern>slice(MatterCavern.class).set(x & 15, y & 15, z & 15, value); matter.<MatterCavern>slice(MatterCavern.class).set(x & 15, y & 15, z & 15, value);
recordObjectValue(x, y, z, NativeBlockState.class, AIR.get());
recordObjectValue(x, y, z, MatterCavern.class, value);
} }
} }
@@ -448,6 +471,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
if (blockSlice != null) { if (blockSlice != null) {
capturePreObjectOriginal(matter, x, y, z, NativeBlockState.class); capturePreObjectOriginal(matter, x, y, z, NativeBlockState.class);
blockSlice.set(x & 15, y & 15, z & 15, null); blockSlice.set(x & 15, y & 15, z & 15, null);
recordObjectValue(x, y, z, NativeBlockState.class, AIR.get());
} }
clearDeferredPlacement(matter, x, y, z); clearDeferredPlacement(matter, x, y, z);
} }
@@ -517,34 +541,14 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
} }
} }
private Optional<TerrainBoundarySignature> resolvedColumn(int x, int z) {
IrisComplex complex = engineMantle.getComplex();
return complex == null ? Optional.empty() : complex.resolvedTerrainColumn(x, z);
}
private NativeBlockState resolvedBlock(TerrainBoundarySignature column, int y) {
String stateKey = column.geometry().voxelAt(y + engineMantle.getEngine().getMinHeight()).stateKey();
NativeBlockState state = IrisPlatforms.get().registries().blockOrNull(stateKey);
if (state == null) {
throw new IllegalStateException("Saved terrain state is unavailable: " + stateKey);
}
return state;
}
public NativeBlockState getPrerequisiteBlock(int x, int y, int z) { public NativeBlockState getPrerequisiteBlock(int x, int y, int z) {
Optional<TerrainBoundarySignature> resolved = resolvedColumn(x, z);
if (resolved.isPresent()) {
return resolvedBlock(resolved.get(), y);
}
NativeBlockState block = getPrerequisiteDataIfPresent(x, y, z, NativeBlockState.class); NativeBlockState block = getPrerequisiteDataIfPresent(x, y, z, NativeBlockState.class);
return block == null ? AIR.get() : block; return block == null ? AIR.get() : block;
} }
public boolean isPrerequisiteCarved(int x, int y, int z) { public boolean isPrerequisiteCarved(int x, int y, int z) {
Optional<TerrainBoundarySignature> resolved = resolvedColumn(x, z);
if (resolved.isPresent()) {
return resolved.get().geometry().isEnclosedOpenAt(y + engineMantle.getEngine().getMinHeight());
}
HydrologyCaveCell hydrology = getPrerequisiteDataIfPresent(x, y, z, HydrologyCaveCell.class); HydrologyCaveCell hydrology = getPrerequisiteDataIfPresent(x, y, z, HydrologyCaveCell.class);
if (hydrology != null) { if (hydrology != null) {
return hydrology.carves(); return hydrology.carves();
@@ -554,10 +558,6 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
} }
public byte[] getPrerequisiteCarvedColumn(int x, int z, int height) { public byte[] getPrerequisiteCarvedColumn(int x, int z, int height) {
Optional<TerrainBoundarySignature> resolved = resolvedColumn(x, z);
if (resolved.isPresent()) {
return resolvedCarvedColumn(resolved.get(), height);
}
int cappedHeight = Math.min(Math.max(height, 0), mantle.getWorldHeight()); int cappedHeight = Math.min(Math.max(height, 0), mantle.getWorldHeight());
byte[] carvedColumn = new byte[cappedHeight]; byte[] carvedColumn = new byte[cappedHeight];
if (cappedHeight <= 0) { if (cappedHeight <= 0) {
@@ -646,6 +646,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
return false; return false;
} }
restoreRaw(matter, x, y, z, type, cell.original(type)); restoreRaw(matter, x, y, z, type, cell.original(type));
recordObjectValue(x, y, z, type, cell.original(type));
return true; return true;
} }
} }
@@ -672,12 +673,15 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
} }
if (cell.blockCaptured()) { if (cell.blockCaptured()) {
restoreRaw(matter, x, y, z, NativeBlockState.class, cell.block()); restoreRaw(matter, x, y, z, NativeBlockState.class, cell.block());
recordObjectValue(x, y, z, NativeBlockState.class, cell.block());
} }
if (cell.stringCaptured()) { if (cell.stringCaptured()) {
restoreRaw(matter, x, y, z, String.class, cell.string()); restoreRaw(matter, x, y, z, String.class, cell.string());
recordObjectValue(x, y, z, String.class, cell.string());
} }
if (cell.cavernCaptured()) { if (cell.cavernCaptured()) {
restoreRaw(matter, x, y, z, MatterCavern.class, cell.cavern()); restoreRaw(matter, x, y, z, MatterCavern.class, cell.cavern());
recordObjectValue(x, y, z, MatterCavern.class, cell.cavern());
} }
return true; return true;
} }
@@ -705,6 +709,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
} }
capturePreObjectOriginal(matter, x, y, z, type); capturePreObjectOriginal(matter, x, y, z, type);
matter.getSlice(type).set(x & 15, y & 15, z & 15, null); matter.getSlice(type).set(x & 15, y & 15, z & 15, null);
recordObjectValue(x, y, z, type, null);
} }
} }
@@ -747,6 +752,8 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
capturePreObjectOriginal(matter, x, y, z, NativeBlockState.class); capturePreObjectOriginal(matter, x, y, z, NativeBlockState.class);
blockSlice.set(x & 15, y & 15, z & 15, baseState); blockSlice.set(x & 15, y & 15, z & 15, baseState);
identifierSlice.set(x & 15, y & 15, z & 15, identifier); identifierSlice.set(x & 15, y & 15, z & 15, identifier);
recordObjectValue(x, y, z, NativeBlockState.class, baseState);
recordObjectValue(x, y, z, Identifier.class, identifier);
} }
} }
@@ -905,13 +912,6 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
// Read-only probe: getDataIfPresent returns the identical answer without materializing // Read-only probe: getDataIfPresent returns the identical answer without materializing
// a 16^3 section + slice on a miss the way getData's getOrCreate path does. // a 16^3 section + slice on a miss the way getData's getOrCreate path does.
NativeBlockState block = getDataIfPresent(x, y, z, NativeBlockState.class); NativeBlockState block = getDataIfPresent(x, y, z, NativeBlockState.class);
Optional<TerrainBoundarySignature> resolved = resolvedColumn(x, z);
if (resolved.isPresent()) {
PreObjectMatterCell cell = getDataIfPresent(x, y, z, PreObjectMatterCell.class);
if (cell == null || !cell.blockCaptured()) {
return resolvedBlock(resolved.get(), y);
}
}
if (block == null) if (block == null)
return AIR.get(); return AIR.get();
return block; return block;
@@ -924,20 +924,6 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
@Override @Override
public boolean isCarved(int x, int y, int z) { public boolean isCarved(int x, int y, int z) {
Optional<TerrainBoundarySignature> resolved = resolvedColumn(x, z);
if (resolved.isPresent()) {
PreObjectMatterCell cell = getDataIfPresent(x, y, z, PreObjectMatterCell.class);
if (cell != null && cell.blockCaptured()) {
NativeBlockState block = getDataIfPresent(x, y, z, NativeBlockState.class);
if (block != null) {
return (block.isAir() || block.isFluid())
&& resolved.get().geometry().hasSolidAbove(y + engineMantle.getEngine().getMinHeight());
}
}
return cell != null && cell.cavernCaptured()
? getDataIfPresent(x, y, z, MatterCavern.class) != null
: resolved.get().geometry().isEnclosedOpenAt(y + engineMantle.getEngine().getMinHeight());
}
HydrologyCaveCell hydrology = getDataIfPresent(x, y, z, HydrologyCaveCell.class); HydrologyCaveCell hydrology = getDataIfPresent(x, y, z, HydrologyCaveCell.class);
if (hydrology != null) { if (hydrology != null) {
return hydrology.carves(); return hydrology.carves();
@@ -946,45 +932,8 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
|| engineMantle.getComplex().isTerrain3DOpening(x, y, z); || engineMantle.getComplex().isTerrain3DOpening(x, y, z);
} }
private byte[] resolvedCarvedColumn(TerrainBoundarySignature column, int height) {
int cappedHeight = Math.min(Math.max(height, 0), mantle.getWorldHeight());
byte[] carved = new byte[cappedHeight];
int minimumY = engineMantle.getEngine().getMinHeight();
BoundaryColumnGeometry geometry = column.geometry();
boolean ceiling = false;
for (int worldY = geometry.minimumY() + geometry.height() - 1; worldY >= minimumY; worldY--) {
BoundaryColumnGeometry.Voxel voxel = geometry.voxelAt(worldY);
int internalY = worldY - minimumY;
if (voxel.phase() == BoundaryColumnGeometry.Phase.SOLID) {
ceiling = true;
} else if (ceiling && internalY < cappedHeight) {
carved[internalY] = 1;
}
}
return carved;
}
public byte[] getCarvedColumn(int x, int z, int height) { public byte[] getCarvedColumn(int x, int z, int height) {
Optional<TerrainBoundarySignature> resolved = resolvedColumn(x, z);
if (resolved.isPresent()) {
byte[] carved = resolvedCarvedColumn(resolved.get(), height);
for (int y = 0; y < carved.length; y++) {
PreObjectMatterCell cell = getDataIfPresent(x, y, z, PreObjectMatterCell.class);
if (cell != null && cell.blockCaptured()) {
NativeBlockState block = getDataIfPresent(x, y, z, NativeBlockState.class);
if (block != null) {
carved[y] = (block.isAir() || block.isFluid())
&& resolved.get().geometry().hasSolidAbove(y + engineMantle.getEngine().getMinHeight())
? (byte) 1 : 0;
continue;
}
}
if (cell != null && cell.cavernCaptured()) {
carved[y] = getDataIfPresent(x, y, z, MatterCavern.class) != null ? (byte) 1 : 0;
}
}
return carved;
}
int cappedHeight = Math.min(Math.max(height, 0), mantle.getWorldHeight()); int cappedHeight = Math.min(Math.max(height, 0), mantle.getWorldHeight());
byte[] carvedColumn = new byte[cappedHeight]; byte[] carvedColumn = new byte[cappedHeight];
if (cappedHeight <= 0) { if (cappedHeight <= 0) {
@@ -1045,11 +994,6 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
@Override @Override
public boolean isSurfaceSolid(int x, int y, int z) { public boolean isSurfaceSolid(int x, int y, int z) {
Optional<TerrainBoundarySignature> resolved = resolvedColumn(x, z);
if (resolved.isPresent()) {
return resolved.get().geometry()
.voxelAt(y + engineMantle.getEngine().getMinHeight()).phase() == BoundaryColumnGeometry.Phase.SOLID;
}
return engineMantle.getEngine().isTerrainSurfaceSolid(x, y, z); return engineMantle.getEngine().isTerrainSurfaceSolid(x, y, z);
} }
@@ -1564,4 +1508,64 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
} }
} }
} }
public final class ObjectPlacementCapture implements AutoCloseable {
private final ObjectContinuationBundle.PlacementKey key;
private final Long2ObjectOpenHashMap<Matter> fragments = new Long2ObjectOpenHashMap<>();
private int minimumX = Integer.MAX_VALUE;
private int minimumZ = Integer.MAX_VALUE;
private int maximumX = Integer.MIN_VALUE;
private int maximumZ = Integer.MIN_VALUE;
private boolean closed;
private ObjectPlacementCapture(ObjectContinuationBundle.PlacementKey key) {
this.key = Objects.requireNonNull(key);
}
public void commit() {
close();
if (fragments.size() > 1) {
Map<ObjectContinuationBundle.ChunkPosition, Supplier<Matter>> pending = new LinkedHashMap<>();
for (Long2ObjectMap.Entry<Matter> entry : fragments.long2ObjectEntrySet()) {
long coordinate = entry.getLongKey();
Matter matter = entry.getValue();
pending.put(new ObjectContinuationBundle.ChunkPosition((int) (coordinate >> 32), (int) coordinate), () -> matter);
}
ObjectContinuationPersistence.persist(mantle, key,
new ObjectContinuationBundle.Bounds(minimumX, minimumZ, maximumX, maximumZ), pending);
}
}
@Override
public void close() {
if (closed) {
return;
}
if (objectPlacementCapture.get() != this) {
throw new IllegalStateException("Object placement capture closed outside its owner");
}
objectPlacementCapture.remove();
closed = true;
}
private void set(int x, int y, int z, Class<?> type, Object value) {
long coordinate = ((long) (x >> 4) << 32) | ((z >> 4) & 0xffffffffL);
Matter fragment = fragments.get(coordinate);
if (fragment == null) {
fragment = new IrisMatter(16, mantle.getWorldHeight(), 16);
fragments.put(coordinate, fragment);
}
minimumX = Math.min(minimumX, x);
minimumZ = Math.min(minimumZ, z);
maximumX = Math.max(maximumX, x);
maximumZ = Math.max(maximumZ, z);
if (type == NativeBlockState.class) {
fragment.slice(Identifier.class).set(x & 15, y, z & 15, null);
fragment.slice(type).set(x & 15, y, z & 15, value == null ? AIR.get() : value);
} else {
fragment.slice(type).set(x & 15, y, z & 15, value);
}
}
}
} }
@@ -0,0 +1,178 @@
package art.arcane.iris.generation.mantle;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.IrisMatter;
import art.arcane.volmlib.util.matter.MatterSlice;
import art.arcane.volmlib.util.io.CountingDataInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.UncheckedIOException;
import art.arcane.iris.world.storage.matter.IrisMatterSupport;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Objects;
public record ObjectContinuationBundle(List<Fragment> fragments) {
private static final int MAX_ENTRIES = 1_048_576;
private static final int MAX_PAYLOAD_BYTES = 64 * 1024 * 1024;
public ObjectContinuationBundle {
fragments = List.copyOf(fragments);
}
public ObjectContinuationBundle with(Fragment fragment) {
LinkedHashMap<PlacementKey, Fragment> merged = new LinkedHashMap<>();
for (Fragment existing : fragments) {
merged.put(existing.key(), existing);
}
merged.put(fragment.key(), fragment);
return new ObjectContinuationBundle(new ArrayList<>(merged.values()));
}
public void write(DataOutputStream output) throws IOException {
output.writeInt(fragments.size());
for (Fragment fragment : fragments) {
output.writeUTF(fragment.key().kind().name());
output.writeInt(fragment.key().sourceChunkX());
output.writeInt(fragment.key().sourceChunkZ());
output.writeInt(fragment.key().ordinal());
output.writeInt(fragment.bounds().minimumX());
output.writeInt(fragment.bounds().minimumZ());
output.writeInt(fragment.bounds().maximumX());
output.writeInt(fragment.bounds().maximumZ());
output.writeInt(fragment.touchedChunks().size());
for (ChunkPosition chunk : fragment.touchedChunks()) {
output.writeInt(chunk.x());
output.writeInt(chunk.z());
}
output.writeInt(fragment.payload.length);
output.write(fragment.payload);
}
}
public static ObjectContinuationBundle read(DataInputStream input) throws IOException {
int count = checkedCount(input.readInt(), MAX_ENTRIES);
ArrayList<Fragment> fragments = new ArrayList<>(count);
for (int index = 0; index < count; index++) {
PlacementKey key = new PlacementKey(Kind.valueOf(input.readUTF()), input.readInt(), input.readInt(), input.readInt());
Bounds bounds = new Bounds(input.readInt(), input.readInt(), input.readInt(), input.readInt());
int touchedCount = checkedCount(input.readInt(), MAX_ENTRIES);
ArrayList<ChunkPosition> touched = new ArrayList<>(touchedCount);
for (int chunk = 0; chunk < touchedCount; chunk++) {
touched.add(new ChunkPosition(input.readInt(), input.readInt()));
}
int length = checkedCount(input.readInt(), MAX_PAYLOAD_BYTES);
byte[] payload = new byte[length];
input.readFully(payload);
fragments.add(new Fragment(key, bounds, touched, payload));
}
return new ObjectContinuationBundle(fragments);
}
public static byte[] encode(Matter payload) {
IrisMatterSupport.ensureRegistered();
try {
ByteArrayOutputStream output = new ByteArrayOutputStream();
payload.write(output);
return output.toByteArray();
} catch (IOException failure) {
throw new UncheckedIOException("Unable to encode object continuation", failure);
}
}
private static int checkedCount(int count, int maximum) throws IOException {
if (count < 0 || count > maximum) {
throw new IOException("Invalid object continuation size: " + count);
}
return count;
}
public enum Kind {
BIOME, FLOATING, STRUCTURE, STATIC
}
public record PlacementKey(Kind kind, int sourceChunkX, int sourceChunkZ, int ordinal) {
public PlacementKey {
Objects.requireNonNull(kind, "placement kind");
if (ordinal < 0) {
throw new IllegalArgumentException("Placement ordinal must be nonnegative");
}
}
}
public record ChunkPosition(int x, int z) {
}
public record Bounds(int minimumX, int minimumZ, int maximumX, int maximumZ) {
public Bounds {
if (minimumX > maximumX || minimumZ > maximumZ) {
throw new IllegalArgumentException("Object continuation bounds are inverted");
}
}
}
public record Fragment(PlacementKey key, Bounds bounds, List<ChunkPosition> touchedChunks, byte[] payload) {
public Fragment {
Objects.requireNonNull(key, "placement key");
Objects.requireNonNull(bounds, "placement bounds");
touchedChunks = List.copyOf(touchedChunks);
if (touchedChunks.size() < 2 || payload.length == 0 || payload.length > MAX_PAYLOAD_BYTES) {
throw new IllegalArgumentException("Object continuation must span chunks with a bounded payload");
}
payload = payload.clone();
}
@Override
public byte[] payload() {
return payload.clone();
}
public int encodedSize() {
return payload.length;
}
public Matter decode() throws IOException {
try (CountingDataInputStream input = CountingDataInputStream.wrap(new ByteArrayInputStream(payload))) {
int width = input.readInt();
int height = input.readInt();
int depth = input.readInt();
if (width != 16 || depth != 16 || height < 1 || height > 4096) {
throw new IOException("Invalid object continuation dimensions");
}
Matter matter = new IrisMatter(width, height, depth);
int sliceCount = input.readUnsignedByte();
matter.getHeader().read(input);
for (int index = 0; index < sliceCount; index++) {
int size = input.readInt();
long end = input.count() + size;
if (size <= 0 || end > payload.length) {
throw new IOException("Invalid object continuation slice size");
}
String identifier = input.readUTF();
Class<?> type = IrisMatter.getSliceType(identifier);
if (type == null || matter.hasSlice(type)) {
throw new IOException("Unavailable or duplicate object continuation slice: " + identifier);
}
MatterSlice<?> slice = matter.createSlice(type, matter);
if (slice == null) {
throw new IOException("Unavailable object continuation codec: " + identifier);
}
slice.read(input);
if (input.count() != end) {
throw new IOException("Object continuation slice size mismatch: " + identifier);
}
matter.putSlice(type, slice);
}
if (input.count() != payload.length) {
throw new IOException("Trailing object continuation bytes");
}
return matter;
}
}
}
}
@@ -0,0 +1,77 @@
package art.arcane.iris.generation.mantle;
import art.arcane.iris.generation.block.TileData;
import art.arcane.iris.integration.Identifier;
import art.arcane.iris.world.storage.matter.TileWrapper;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.matter.Matter;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
public final class ObjectContinuationPersistence {
private ObjectContinuationPersistence() {
}
public static void persist(Mantle<Matter> mantle, ObjectContinuationBundle.PlacementKey key,
ObjectContinuationBundle.Bounds bounds,
Map<ObjectContinuationBundle.ChunkPosition, Supplier<Matter>> destinations) {
if (destinations.size() < 2) {
return;
}
List<ObjectContinuationBundle.ChunkPosition> touched = List.copyOf(destinations.keySet());
for (Map.Entry<ObjectContinuationBundle.ChunkPosition, Supplier<Matter>> entry : destinations.entrySet()) {
ObjectContinuationBundle.ChunkPosition destination = entry.getKey();
MantleChunk<Matter> chunk = mantle.getChunk(destination.x(), destination.z()).use();
try {
synchronized (chunk) {
if (chunk.isFlagged(MantleFlag.REAL)) {
continue;
}
Matter section = chunk.getOrCreate(0);
ObjectContinuationBundle bundle = section.hasSlice(ObjectContinuationBundle.class)
? section.<ObjectContinuationBundle>getSlice(ObjectContinuationBundle.class).get(0, 0, 0) : null;
if (contains(bundle, key)) {
continue;
}
ObjectContinuationBundle.Fragment fragment = new ObjectContinuationBundle.Fragment(
key, bounds, touched, ObjectContinuationBundle.encode(entry.getValue().get()));
section.slice(ObjectContinuationBundle.class).set(0, 0, 0,
bundle == null ? new ObjectContinuationBundle(List.of(fragment)) : bundle.with(fragment));
}
} finally {
chunk.release();
}
}
}
public static void put(Matter payload, int x, int y, int z, Object value) {
if (value instanceof NativeBlockState state) {
String customKey = state.deferredPlacementKey();
NativeBlockState base = state.placementBaseState();
boolean custom = state.isCustom() && customKey != null && base != null;
payload.slice(NativeBlockState.class).set(x, y, z, custom ? base : state);
payload.slice(Identifier.class).set(x, y, z, custom ? Identifier.fromString(customKey) : null);
} else if (value instanceof TileData tile) {
payload.slice(TileWrapper.class).set(x, y, z, new TileWrapper(tile));
} else {
payload.slice(payload.getClass(value)).set(x, y, z, value);
}
}
private static boolean contains(ObjectContinuationBundle bundle, ObjectContinuationBundle.PlacementKey key) {
if (bundle == null) {
return false;
}
for (ObjectContinuationBundle.Fragment fragment : bundle.fragments()) {
if (fragment.key().equals(key)) {
return true;
}
}
return false;
}
}
@@ -30,6 +30,7 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
private final int destinationChunkZ; private final int destinationChunkZ;
private final int worldHeight; private final int worldHeight;
private final List<Mutation> mutations; private final List<Mutation> mutations;
private final List<PlacementRange> placements = new ArrayList<>();
private final Long2ObjectOpenHashMap<Int2ObjectOpenHashMap<OverlayCell>> overlay; private final Long2ObjectOpenHashMap<Int2ObjectOpenHashMap<OverlayCell>> overlay;
ObjectDestinationTransaction(MantleWriter writer, int destinationChunkX, int destinationChunkZ) { ObjectDestinationTransaction(MantleWriter writer, int destinationChunkX, int destinationChunkZ) {
@@ -53,7 +54,25 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
if (checkpoint < 0 || checkpoint > mutations.size()) { if (checkpoint < 0 || checkpoint > mutations.size()) {
throw new IllegalArgumentException("Mutation checkpoint is outside the transaction"); throw new IllegalArgumentException("Mutation checkpoint is outside the transaction");
} }
return new ObjectSourcePlan(mutations.subList(checkpoint, mutations.size())); ArrayList<List<Mutation>> accepted = new ArrayList<>();
for (PlacementRange placement : placements) {
if (placement.start() >= checkpoint) {
accepted.add(List.copyOf(mutations.subList(placement.start(), placement.end())));
}
}
return new ObjectSourcePlan(mutations.subList(checkpoint, mutations.size()), accepted);
}
@Override
public int beginObjectPlacement() {
return mutations.size();
}
@Override
public void endObjectPlacement(int checkpoint) {
if (checkpoint >= 0 && checkpoint < mutations.size()) {
placements.add(new PlacementRange(checkpoint, mutations.size()));
}
} }
void apply(ObjectSourcePlan plan) { void apply(ObjectSourcePlan plan) {
@@ -413,6 +432,9 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
} }
} }
private record PlacementRange(int start, int end) {
}
record DataKey(int x, int y, int z, Class<?> type) { record DataKey(int x, int y, int z, Class<?> type) {
} }
@@ -8,6 +8,13 @@ public interface ObjectPassPlacer extends IObjectPlacer {
byte[] getCarvedColumn(int x, int z, int height); byte[] getCarvedColumn(int x, int z, int height);
default int beginObjectPlacement() {
return -1;
}
default void endObjectPlacement(int checkpoint) {
}
@Override @Override
default <T> @Nullable T getData(int x, int y, int z, Class<T> type) { default <T> @Nullable T getData(int x, int y, int z, Class<T> type) {
return getDataIfPresent(x, y, z, type); return getDataIfPresent(x, y, z, type);
@@ -3,15 +3,39 @@ package art.arcane.iris.generation.mantle;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap; import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import art.arcane.iris.integration.Identifier;
import art.arcane.iris.world.storage.matter.IrisMatterSupport;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.matter.IrisMatter;
import art.arcane.volmlib.util.matter.Matter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.List; import java.util.List;
final class ObjectSourcePlan { final class ObjectSourcePlan {
private static final int DESTINATION_INDEX_WEIGHT = 8; private static final int DESTINATION_INDEX_WEIGHT = 8;
private final Long2ObjectOpenHashMap<List<ObjectDestinationTransaction.Mutation>> destinations; private final Long2ObjectOpenHashMap<List<ObjectDestinationTransaction.Mutation>> destinations;
private final int mutationWeight; private final int mutationWeight;
private final List<Placement> placements;
ObjectSourcePlan(List<ObjectDestinationTransaction.Mutation> mutations) { ObjectSourcePlan(List<ObjectDestinationTransaction.Mutation> mutations) {
this(mutations, List.of());
}
ObjectSourcePlan(List<ObjectDestinationTransaction.Mutation> mutations, List<List<ObjectDestinationTransaction.Mutation>> placements) {
ArrayList<Placement> crossing = new ArrayList<>();
for (int ordinal = 0; ordinal < placements.size(); ordinal++) {
List<ObjectDestinationTransaction.Mutation> placement = placements.get(ordinal);
LinkedHashMap<ObjectContinuationBundle.ChunkPosition, List<ObjectDestinationTransaction.Mutation>> fragments = group(placement);
if (fragments.size() > 1) {
crossing.add(new Placement(ordinal, bounds(placement), List.copyOf(fragments.keySet()), fragments));
}
}
this.placements = List.copyOf(crossing);
Long2ObjectOpenHashMap<ArrayList<ObjectDestinationTransaction.Mutation>> grouped = new Long2ObjectOpenHashMap<>(2); Long2ObjectOpenHashMap<ArrayList<ObjectDestinationTransaction.Mutation>> grouped = new Long2ObjectOpenHashMap<>(2);
long weight = 1L; long weight = 1L;
for (ObjectDestinationTransaction.Mutation mutation : mutations) { for (ObjectDestinationTransaction.Mutation mutation : mutations) {
@@ -29,6 +53,12 @@ final class ObjectSourcePlan {
for (Long2ObjectMap.Entry<ArrayList<ObjectDestinationTransaction.Mutation>> entry : grouped.long2ObjectEntrySet()) { for (Long2ObjectMap.Entry<ArrayList<ObjectDestinationTransaction.Mutation>> entry : grouped.long2ObjectEntrySet()) {
destinations.put(entry.getLongKey(), List.copyOf(entry.getValue())); destinations.put(entry.getLongKey(), List.copyOf(entry.getValue()));
} }
for (Placement placement : this.placements) {
weight += 8L + placement.touched().size() * DESTINATION_INDEX_WEIGHT;
for (List<ObjectDestinationTransaction.Mutation> fragment : placement.fragments().values()) {
weight += fragment.size();
}
}
this.mutationWeight = weight >= Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) weight; this.mutationWeight = weight >= Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) weight;
} }
@@ -37,10 +67,95 @@ final class ObjectSourcePlan {
return mutations == null ? List.of() : mutations; return mutations == null ? List.of() : mutations;
} }
void persistContinuations(MantleWriter writer, int sourceX, int sourceZ, int destinationX, int destinationZ) {
ObjectContinuationBundle.ChunkPosition destination = new ObjectContinuationBundle.ChunkPosition(destinationX, destinationZ);
for (Placement placement : placements) {
if (!placement.fragments().containsKey(destination)) {
continue;
}
ObjectContinuationBundle.PlacementKey key = new ObjectContinuationBundle.PlacementKey(ObjectContinuationBundle.Kind.BIOME, sourceX, sourceZ, placement.ordinal());
for (Map.Entry<ObjectContinuationBundle.ChunkPosition, List<ObjectDestinationTransaction.Mutation>> entry : placement.fragments().entrySet()) {
ObjectContinuationBundle.ChunkPosition target = entry.getKey();
if (target.equals(destination)) {
continue;
}
writer.withChunkFence(target.x(), target.z(), () -> {
MantleChunk<Matter> chunk = writer.acquireChunk(target.x(), target.z());
if (chunk.isFlagged(MantleFlag.REAL)) {
return;
}
int x = target.x() << 4;
int z = target.z() << 4;
ObjectContinuationBundle bundle = writer.getDataIfPresent(x, 0, z, ObjectContinuationBundle.class);
if (bundle != null) {
for (ObjectContinuationBundle.Fragment existing : bundle.fragments()) {
if (existing.key().equals(key)) {
return;
}
}
}
ObjectContinuationBundle.Fragment fragment = new ObjectContinuationBundle.Fragment(key, placement.bounds(), placement.touched(),
encode(entry.getValue(), writer.getMantle().getWorldHeight()));
writer.setData(x, 0, z, bundle == null ? new ObjectContinuationBundle(List.of(fragment)) : bundle.with(fragment));
});
}
}
}
static byte[] encode(List<ObjectDestinationTransaction.Mutation> mutations, int height) {
IrisMatterSupport.ensureRegistered();
Matter payload = new IrisMatter(16, height, 16);
for (ObjectDestinationTransaction.Mutation mutation : mutations) {
ObjectDestinationTransaction.DataKey key = mutation.key();
int x = key.x() & 15;
int z = key.z() & 15;
if (mutation instanceof ObjectDestinationTransaction.CustomBlockMutation custom) {
payload.slice(NativeBlockState.class).set(x, key.y(), z, custom.state().placementBaseState());
payload.slice(Identifier.class).set(x, key.y(), z, Identifier.fromString(custom.state().deferredPlacementKey()));
} else if (mutation instanceof ObjectDestinationTransaction.SetMutation set) {
if (key.type() == NativeBlockState.class) {
payload.slice(Identifier.class).set(x, key.y(), z, null);
}
payload.slice(key.type()).set(x, key.y(), z, set.value());
}
}
return ObjectContinuationBundle.encode(payload);
}
private static LinkedHashMap<ObjectContinuationBundle.ChunkPosition, List<ObjectDestinationTransaction.Mutation>> group(
List<ObjectDestinationTransaction.Mutation> mutations
) {
LinkedHashMap<ObjectContinuationBundle.ChunkPosition, List<ObjectDestinationTransaction.Mutation>> grouped = new LinkedHashMap<>();
for (ObjectDestinationTransaction.Mutation mutation : mutations) {
ObjectContinuationBundle.ChunkPosition destination = new ObjectContinuationBundle.ChunkPosition(mutation.x() >> 4, mutation.z() >> 4);
grouped.computeIfAbsent(destination, ignored -> new ArrayList<>()).add(mutation);
}
return grouped;
}
private static ObjectContinuationBundle.Bounds bounds(List<ObjectDestinationTransaction.Mutation> mutations) {
int minimumX = Integer.MAX_VALUE;
int minimumZ = Integer.MAX_VALUE;
int maximumX = Integer.MIN_VALUE;
int maximumZ = Integer.MIN_VALUE;
for (ObjectDestinationTransaction.Mutation mutation : mutations) {
minimumX = Math.min(minimumX, mutation.x());
minimumZ = Math.min(minimumZ, mutation.z());
maximumX = Math.max(maximumX, mutation.x());
maximumZ = Math.max(maximumZ, mutation.z());
}
return new ObjectContinuationBundle.Bounds(minimumX, minimumZ, maximumX, maximumZ);
}
int mutationWeight() { int mutationWeight() {
return mutationWeight; return mutationWeight;
} }
private record Placement(int ordinal, ObjectContinuationBundle.Bounds bounds,
List<ObjectContinuationBundle.ChunkPosition> touched,
Map<ObjectContinuationBundle.ChunkPosition, List<ObjectDestinationTransaction.Mutation>> fragments) {
}
private static long destinationKey(int chunkX, int chunkZ) { private static long destinationKey(int chunkX, int chunkZ) {
return ((long) chunkX << 32) | (chunkZ & 0xffffffffL); return ((long) chunkX << 32) | (chunkZ & 0xffffffffL);
} }
@@ -125,10 +125,10 @@ public final class DimensionTerrainContext implements DataProvider {
engine.getDimension(), engine.getDimension(),
engine.getData(), engine.getData(),
engine.getHeight(), engine.getHeight(),
natural ? complex.getUnblendedNaturalHeightStream() : complex.getHeightStream(), natural ? complex.getNaturalHeightStream() : complex.getHeightStream(),
natural ? complex.getNaturalSlopeStream() : complex.getSlopeStream(), natural ? complex.getNaturalSlopeStream() : complex.getSlopeStream(),
fluidHeightStream, fluidHeightStream,
natural ? complex.getUnblendedNaturalTrueBiomeStream() : complex.getTrueBiomeStream(), natural ? complex.getNaturalTrueBiomeStream() : complex.getTrueBiomeStream(),
complex.getRegionStream(), complex.getRegionStream(),
complex.getRockStream(), complex.getRockStream(),
fluidBlockSampler, fluidBlockSampler,
@@ -47,7 +47,6 @@ import art.arcane.iris.structure.placement.IrisStructure;
import art.arcane.iris.world.IrisWorld; import art.arcane.iris.world.IrisWorld;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell; import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveStorage; import art.arcane.iris.generation.hydrology.cave.HydrologyCaveStorage;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.nativelib.terrain.NativeBiome; import art.arcane.volmlib.nativelib.terrain.NativeBiome;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState; import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KList;
@@ -68,20 +67,15 @@ import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterCavern; import art.arcane.volmlib.util.matter.MatterCavern;
import art.arcane.iris.generation.concurrent.BurstExecutor; import art.arcane.iris.generation.concurrent.BurstExecutor;
import art.arcane.iris.generation.concurrent.MultiBurst; import art.arcane.iris.generation.concurrent.MultiBurst;
import art.arcane.volmlib.util.scheduling.ChronoLatch;
import art.arcane.iris.world.task.J;
import art.arcane.volmlib.util.stream.ProceduralStream; import art.arcane.volmlib.util.stream.ProceduralStream;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.awt.Color; import java.awt.Color;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.Locale; import java.util.Locale;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
@@ -496,10 +490,6 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
@BlockCoordinates @BlockCoordinates
default int getHeight(int x, int z, boolean ignoreFluid) { default int getHeight(int x, int z, boolean ignoreFluid) {
OptionalInt resolved = getComplex().resolvedTerrainHeight(x, z, ignoreFluid);
if (resolved.isPresent()) {
return resolved.getAsInt();
}
DimensionStackContext dimensionStackContext = getDimensionStackContext(); DimensionStackContext dimensionStackContext = getDimensionStackContext();
if (dimensionStackContext != null) { if (dimensionStackContext != null) {
return ignoreFluid return ignoreFluid
@@ -18,17 +18,31 @@
package art.arcane.iris.generation.runtime; package art.arcane.iris.generation.runtime;
import art.arcane.iris.generation.stage.IrisBiomeActuator;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.generation.mantle.EngineMantle;
import art.arcane.iris.generation.context.ChunkContext;
import art.arcane.iris.generation.context.IrisContext;
import art.arcane.volmlib.util.documentation.BlockCoordinates;
import art.arcane.volmlib.util.hunk.Hunk;
import art.arcane.iris.generation.concurrent.BurstExecutor; import art.arcane.iris.generation.concurrent.BurstExecutor;
import art.arcane.iris.generation.concurrent.MultiBurst; import art.arcane.iris.generation.concurrent.MultiBurst;
import art.arcane.iris.generation.context.ChunkContext;
import art.arcane.iris.generation.context.IrisContext;
import art.arcane.iris.generation.mantle.EngineMantle;
import art.arcane.iris.generation.stage.IrisBiomeActuator;
import art.arcane.iris.generation.stage.IrisTransitionGeometryActuator;
import art.arcane.iris.integration.Identifier;
import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial;
import art.arcane.iris.world.storage.matter.TileWrapper;
import art.arcane.iris.world.history.FloatingBiomeOverlay;
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.world.history.SavedTerrainChunk;
import art.arcane.volmlib.nativelib.terrain.NativeBiome; import art.arcane.volmlib.nativelib.terrain.NativeBiome;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState; import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.documentation.BlockCoordinates;
import art.arcane.volmlib.util.hunk.Hunk;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterSlice;
import java.io.IOException;
import java.util.List;
import java.util.Map;
public interface EngineMode extends Staged { public interface EngineMode extends Staged {
KList<EngineStage> getTerrainStages(); KList<EngineStage> getTerrainStages();
@@ -199,12 +213,115 @@ public interface EngineMode extends Staged {
EngineStage[] stages = getStages().toArray(new EngineStage[0]); EngineStage[] stages = getStages().toArray(new EngineStage[0]);
try (IrisContext.Scope chunkScope = IrisContext.open(getEngine(), generationSessionId, ctx)) { try (IrisContext.Scope chunkScope = IrisContext.open(getEngine(), generationSessionId, ctx)) {
getComplex().getResolvedTerrain().generate(this, x, z, blocks, biomes, multicore, ctx); generateTerrain(x, z, blocks, biomes, multicore, ctx);
recordNaturalTerrain(x, z, blocks, biomes, ctx);
IrisBiomeActuator.publishNaturalMetadata(getEngine(), x, z, biomes, ctx); IrisBiomeActuator.publishNaturalMetadata(getEngine(), x, z, biomes, ctx);
ctx.beginContent(); ctx.beginContent();
List<Matter> continuations = restoreObjectContinuations(x, z);
for (EngineStage i : stages) { for (EngineStage i : stages) {
i.generate(x, z, blocks, biomes, multicore, ctx); i.generate(x, z, blocks, biomes, multicore, ctx);
} }
for (Matter continuation : continuations) {
restoreObjectMetadata(x, z, continuation);
MatterSlice<NativeBlockState> states = continuation.getSlice(NativeBlockState.class);
if (states != null) {
states.iterateSync((localX, y, localZ, state) -> blocks.set(localX, y, localZ, state));
}
}
}
}
private List<Matter> restoreObjectContinuations(int x, int z) {
if (!(getEngine() instanceof IrisEngine irisEngine)) {
return List.of();
}
GenerationHistoryRuntimeRouter router = irisEngine.getGenerationHistoryRuntimeRouter().orElse(null);
if (router == null) {
return List.of();
}
List<GenerationHistoryRuntimeRouter.ObjectContinuation> fragments = router.objectContinuations(
getComplex().getTransitionGenerationPlan(), x >> 4, z >> 4);
if (fragments.isEmpty()) {
return List.of();
}
try {
List<Matter> continuations = router.decodeObjectContinuations(fragments);
for (Matter continuation : continuations) {
if (continuation.getHeight() != getEngine().getHeight()) {
throw new IOException("Object continuation height does not match the world");
}
restoreObjectMetadata(x, z, continuation);
}
return continuations;
} catch (IOException failure) {
throw new IllegalStateException("Unable to restore object continuation at " + (x >> 4) + "," + (z >> 4), failure);
}
}
private void restoreObjectMetadata(int x, int z, Matter continuation) {
MantleChunk<Matter> chunk = getMantle().getMantle().getChunk(x >> 4, z >> 4).use();
try {
synchronized (chunk) {
MatterSlice<NativeBlockState> states = continuation.getSlice(NativeBlockState.class);
if (states != null) {
states.iterateSync((localX, y, localZ, state) -> {
Matter section = chunk.get(y >> 4);
if (section != null) {
clearObjectMetadata(section, localX, y & 15, localZ);
}
});
}
for (Map.Entry<Class<?>, MatterSlice<?>> entry : continuation.getSliceMap().entrySet()) {
entry.getValue().iterateSync((localX, y, localZ, value) ->
chunk.getOrCreate(y >> 4).slice(entry.getKey()).set(localX, y & 15, localZ, value));
}
}
} finally {
chunk.release();
}
}
private static void clearObjectMetadata(Matter section, int x, int y, int z) {
clearObjectMetadata(section.getSlice(Identifier.class), x, y, z);
clearObjectMetadata(section.getSlice(String.class), x, y, z);
clearObjectMetadata(section.getSlice(TreeBlockMaterial.class), x, y, z);
clearObjectMetadata(section.getSlice(TileWrapper.class), x, y, z);
}
private static void clearObjectMetadata(MatterSlice<?> slice, int x, int y, int z) {
if (slice != null) {
slice.set(x, y, z, null);
}
}
private void recordNaturalTerrain(int x, int z, Hunk<NativeBlockState> blocks,
Hunk<NativeBiome> biomes, ChunkContext context) {
if (!(getEngine() instanceof IrisEngine irisEngine)) {
return;
}
GenerationHistoryRuntimeRouter router = irisEngine.getGenerationHistoryRuntimeRouter().orElse(null);
if (router == null) {
return;
}
FloatingBiomeOverlay floating = context.getFloatingBiomes();
if (floating != null) {
floating.retainHighestSurfaces((localX, localZ) -> {
for (int y = blocks.getHeight() - 1; y >= 0; y--) {
NativeBlockState state = blocks.getRaw(localX, y, localZ);
if (state != null && !state.isAir() && !state.isFluid()) {
return y;
}
}
return -1;
});
}
try {
SavedTerrainChunk terrain = IrisTransitionGeometryActuator.capture(
x, z, blocks, biomes, getEngine().getMinHeight(), context, true);
router.recordNaturalTerrain(terrain);
router.recordFloatingBiomes(x >> 4, z >> 4, floating);
} catch (IOException failure) {
throw new IllegalStateException("Unable to capture natural terrain at " + (x >> 4) + "," + (z >> 4), failure);
} }
} }
@@ -30,7 +30,7 @@ import art.arcane.iris.generation.cache.Cache;
import art.arcane.iris.studio.generation.BiomeBuffetLayout; import art.arcane.iris.studio.generation.BiomeBuffetLayout;
import art.arcane.iris.world.history.GenerationBlend; import art.arcane.iris.world.history.GenerationBlend;
import art.arcane.iris.world.history.TransitionGenerationPlan; import art.arcane.iris.world.history.TransitionGenerationPlan;
import art.arcane.iris.world.history.TerrainBoundarySignature; import art.arcane.iris.world.history.TransitionDisplacementField;
import art.arcane.iris.generation.hydrology.HydrologyColumnLayer; import art.arcane.iris.generation.hydrology.HydrologyColumnLayer;
import art.arcane.iris.generation.hydrology.HydrologyColumnSample; import art.arcane.iris.generation.hydrology.HydrologyColumnSample;
import art.arcane.iris.generation.hydrology.HydrologyColumnSnapshot; import art.arcane.iris.generation.hydrology.HydrologyColumnSnapshot;
@@ -41,6 +41,8 @@ import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntimeContext;
import art.arcane.iris.generation.image.IrisImageMapApplication; import art.arcane.iris.generation.image.IrisImageMapApplication;
import art.arcane.iris.generation.image.IrisImageMapRuntime; import art.arcane.iris.generation.image.IrisImageMapRuntime;
import art.arcane.iris.generation.mantle.MantleHydrologyCaveVoxelView; import art.arcane.iris.generation.mantle.MantleHydrologyCaveVoxelView;
import art.arcane.iris.generation.mantle.ObjectContinuationBundle;
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.generation.terrain.InferredType; import art.arcane.iris.generation.terrain.InferredType;
import art.arcane.iris.generation.biome.IrisBiome; import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.biome.IrisBiomeGeneratorLink; import art.arcane.iris.generation.biome.IrisBiomeGeneratorLink;
@@ -99,7 +101,6 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.OptionalInt;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
@@ -175,7 +176,6 @@ public class IrisComplex implements DataProvider {
private final transient boolean reuseNaturalBaseBiome; private final transient boolean reuseNaturalBaseBiome;
private ProceduralStream<UUID> baseBiomeIDStream; private ProceduralStream<UUID> baseBiomeIDStream;
private ProceduralStream<IrisBiome> naturalTrueBiomeStream; private ProceduralStream<IrisBiome> naturalTrueBiomeStream;
private ProceduralStream<IrisBiome> unblendedNaturalTrueBiomeStream;
private ProceduralStream<IrisBiome> trueBiomeStream; private ProceduralStream<IrisBiome> trueBiomeStream;
private ProceduralStream<NativeBiome> trueBiomeDerivativeStream; private ProceduralStream<NativeBiome> trueBiomeDerivativeStream;
private ProceduralStream<Double> naturalHeightStream; private ProceduralStream<Double> naturalHeightStream;
@@ -188,8 +188,7 @@ public class IrisComplex implements DataProvider {
private transient Terrain3DRuntime terrain3D; private transient Terrain3DRuntime terrain3D;
private transient HydrologyBankTerrainRuntime hydrologyBanks3D; private transient HydrologyBankTerrainRuntime hydrologyBanks3D;
private ProceduralStream<Double> unblendedNaturalHeightStream; private ProceduralStream<Double> unblendedNaturalHeightStream;
private final ResolvedTerrainProvider resolvedTerrain; private final TransitionDisplacementField transitionDisplacement;
private ProceduralStream<Double> placementHeightStream;
private ProceduralStream<Double> heightStream; private ProceduralStream<Double> heightStream;
@Getter(AccessLevel.NONE) @Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE) @Setter(AccessLevel.NONE)
@@ -248,7 +247,6 @@ public class IrisComplex implements DataProvider {
biomeBoundsSamplingStep = engine.getDimension().getBiomeBoundsSamplingStep(); biomeBoundsSamplingStep = engine.getDimension().getBiomeBoundsSamplingStep();
this.detached = detached; this.detached = detached;
this.transitionGenerationPlan = transitionGenerationPlan; this.transitionGenerationPlan = transitionGenerationPlan;
this.resolvedTerrain = new ResolvedTerrainProvider(engine);
int cacheSize = noiseCacheSize(engine, IrisSettings.get().getPerformance().getNoiseCacheSize(), detached); int cacheSize = noiseCacheSize(engine, IrisSettings.get().getPerformance().getNoiseCacheSize(), detached);
IrisBiome emptyBiome = new IrisBiome().setInferredType(InferredType.CAVE); IrisBiome emptyBiome = new IrisBiome().setInferredType(InferredType.CAVE);
UUID focusUUID = UUID.nameUUIDFromBytes("focus".getBytes()); UUID focusUUID = UUID.nameUUIDFromBytes("focus".getBytes());
@@ -408,22 +406,21 @@ public class IrisComplex implements DataProvider {
unblendedNaturalHeightStream = terrain3DEnabled unblendedNaturalHeightStream = terrain3DEnabled
? GenerationStreams.cache2DDouble(ProceduralStream.ofDouble(this::sampleTerrain3DHeight), "unblendedNaturalHeightStream", engine, cacheSize) ? GenerationStreams.cache2DDouble(ProceduralStream.ofDouble(this::sampleTerrain3DHeight), "unblendedNaturalHeightStream", engine, cacheSize)
: baseTerrainHeightStream; : baseTerrainHeightStream;
naturalHeightStream = unblendedNaturalHeightStream; transitionDisplacement = transitionGenerationPlan == null ? null : new TransitionDisplacementField(
transitionGenerationPlan, new TransitionDisplacementField.Sources(
(x, z) -> unblendedNaturalHeightStream.getDouble(x, z), engine.getHeight()));
naturalHeightStream = transitionDisplacement == null ? unblendedNaturalHeightStream
: GenerationStreams.cache2DDouble(ProceduralStream.ofDouble((x, z) -> transitionDisplacement.height(
blockCoordinate(x), blockCoordinate(z), unblendedNaturalHeightStream.getDouble(x, z))),
"naturalHeightStream", engine, cacheSize);
naturalTrueBiomeStream = focusedBiomes != null ? GenerationStreams.cache2D(focusedBiomes, "naturalTrueBiomeStream-focus", engine, cacheSize) : GenerationStreams.cache2D(naturalHeightStream naturalTrueBiomeStream = focusedBiomes != null ? GenerationStreams.cache2D(focusedBiomes, "naturalTrueBiomeStream-focus", engine, cacheSize) : GenerationStreams.cache2D(naturalHeightStream
.convertAware2D((h, x, z) -> { .convertAware2D((h, x, z) -> {
IrisBiome mapped = imageMapRuntime.sampleBiome(x, z); IrisBiome mapped = imageMapRuntime.sampleBiome(x, z);
return mapped == null return mapped == null
? fixBiomeType(h, baseBiomeStream.get(x, z), regionStream.get(x, z), x, z, fluidHeight) ? fixBiomeType(h, baseBiomeStream.get(x, z), regionStream.get(x, z), x, z,
transitionFluidHeight(x, z, fluidHeight))
: mapped; : mapped;
}), "naturalTrueBiomeStream", engine, cacheSize); }), "naturalTrueBiomeStream", engine, cacheSize);
unblendedNaturalTrueBiomeStream = transitionGenerationPlan == null || focusedBiomes != null
? naturalTrueBiomeStream
: GenerationStreams.cache2D(unblendedNaturalHeightStream.convertAware2D((h, x, z) -> {
IrisBiome mapped = imageMapRuntime.sampleBiome(x, z);
return mapped == null
? fixBiomeType(h, baseBiomeStream.get(x, z), regionStream.get(x, z), x, z, fluidHeight)
: mapped;
}), "unblendedNaturalTrueBiomeStream", engine, cacheSize);
IrisHydrology configuredHydrology = engine.getDimension().getHydrology(); IrisHydrology configuredHydrology = engine.getDimension().getHydrology();
if (hydrologyActive(configuredHydrology)) { if (hydrologyActive(configuredHydrology)) {
hydrologyRuntime = new IrisHydrologyRuntime(new IrisHydrologyRuntimeContext( hydrologyRuntime = new IrisHydrologyRuntime(new IrisHydrologyRuntimeContext(
@@ -456,10 +453,9 @@ public class IrisComplex implements DataProvider {
ProceduralStream.ofDouble(this::resolveHydrologyTerrainHeight), cacheSize); ProceduralStream.ofDouble(this::resolveHydrologyTerrainHeight), cacheSize);
heightStream = ProceduralStream.ofDouble((x, z) -> terrainEngine.getPlatformHooks().isMainThread() heightStream = ProceduralStream.ofDouble((x, z) -> terrainEngine.getPlatformHooks().isMainThread()
? nonblockingTerrainHeight(x, z) : cachedHeightStream.getDouble(x, z)); ? nonblockingTerrainHeight(x, z) : cachedHeightStream.getDouble(x, z));
placementHeightStream = ProceduralStream.ofDouble(this::samplePlacementHeight); roundedHeighteightStream = GenerationStreams.contextInjecting(heightStream, engine, (c, x, z) -> c.getHeight().getDouble(x, z))
roundedHeighteightStream = GenerationStreams.contextInjecting(placementHeightStream, engine, (c, x, z) -> c.getHeight().getDouble(x, z))
.round(); .round();
slopeStream = GenerationStreams.contextInjecting(placementHeightStream, engine, (c, x, z) -> c.getHeight().getDouble(x, z)) slopeStream = GenerationStreams.contextInjecting(heightStream, engine, (c, x, z) -> c.getHeight().getDouble(x, z))
.slope(3); .slope(3);
naturalSlopeStream = GenerationStreams.cache2DDouble(naturalHeightStream.slope(3), "naturalSlopeStream", engine, cacheSize); naturalSlopeStream = GenerationStreams.cache2DDouble(naturalHeightStream.slope(3), "naturalSlopeStream", engine, cacheSize);
// Terrain3D column heights resolve the surface biome, so the shore band reads the pre-terrain3D slope or it recurses. // Terrain3D column heights resolve the surface biome, so the shore band reads the pre-terrain3D slope or it recurses.
@@ -534,7 +530,7 @@ public class IrisComplex implements DataProvider {
List<NoiseCacheCapacity> capacities = new ArrayList<>(5); List<NoiseCacheCapacity> capacities = new ArrayList<>(5);
int additionalChunks = HydrologyNoiseCacheBudget.MAXIMUM_CHUNKS; int additionalChunks = HydrologyNoiseCacheBudget.MAXIMUM_CHUNKS;
for (ProceduralStream<?> stream : new ProceduralStream<?>[]{ for (ProceduralStream<?> stream : new ProceduralStream<?>[]{
baseBiomeStream, baseTerrainHeightStream, unblendedNaturalHeightStream, regionStream, bridgeStream}) { baseBiomeStream, baseTerrainHeightStream, unblendedNaturalHeightStream, naturalHeightStream, regionStream, bridgeStream}) {
if (!(stream instanceof CachedStream2D<?> || stream instanceof CachedDoubleStream2D) if (!(stream instanceof CachedStream2D<?> || stream instanceof CachedDoubleStream2D)
|| selected.put(stream, Boolean.TRUE) != null) { || selected.put(stream, Boolean.TRUE) != null) {
continue; continue;
@@ -641,9 +637,7 @@ public class IrisComplex implements DataProvider {
} }
private double sampleNaturalTerrainHeight(Engine engine, double x, double z) { private double sampleNaturalTerrainHeight(Engine engine, double x, double z) {
return terrain3D != null && terrain3D.active() return naturalHeightStream.getDouble(x, z);
? sampleTerrain3DHeight(x, z)
: sampleUnblendedNaturalTerrainHeight(x, z);
} }
private double sampleTerrain3DHeight(double x, double z) { private double sampleTerrain3DHeight(double x, double z) {
@@ -672,6 +666,10 @@ public class IrisComplex implements DataProvider {
return null; return null;
} }
Terrain3DColumn column = terrain3D.column(x, z); Terrain3DColumn column = terrain3D.column(x, z);
if (transitionDisplacement != null && column.shaped()) {
int offset = (int) Math.round(naturalHeightStream.getDouble(x, z)) - column.topY();
column = column.displaced(offset, terrainEngine.getHeight());
}
return column.shaped() ? column : null; return column.shaped() ? column : null;
} }
@@ -740,7 +738,7 @@ public class IrisComplex implements DataProvider {
private double nearbyTerrainSurfaceHeight(int x, int surfaceY, int z) { private double nearbyTerrainSurfaceHeight(int x, int surfaceY, int z) {
Terrain3DColumn column = terrainColumn(x, z); Terrain3DColumn column = terrainColumn(x, z);
return column == null ? getPlacementHeightStream().getDouble(x, z) : column.nearestSurfaceY(surfaceY); return column == null ? getHeightStream().getDouble(x, z) : column.nearestSurfaceY(surfaceY);
} }
private double sampleUnblendedNaturalTerrainHeight(double x, double z) { private double sampleUnblendedNaturalTerrainHeight(double x, double z) {
@@ -783,17 +781,18 @@ public class IrisComplex implements DataProvider {
if (biome == null || region == null) { if (biome == null || region == null) {
return biome; return biome;
} }
double shoreTop = shoreBandTop(height, region, x, z, fluidHeight, shoreSlopeStream); double localFluidHeight = transitionFluidHeight(x, z, fluidHeight);
if (height >= fluidHeight - 1 && height <= shoreTop && !biome.isShore()) { double shoreTop = shoreBandTop(height, region, x, z, localFluidHeight, shoreSlopeStream);
if (height >= localFluidHeight - 1 && height <= shoreTop && !biome.isShore()) {
return sampleInferredBiome(region, InferredType.SHORE, x, z); return sampleInferredBiome(region, InferredType.SHORE, x, z);
} }
if (height > shoreTop && !biome.isLand()) { if (height > shoreTop && !biome.isLand()) {
return sampleInferredBiome(region, InferredType.LAND, x, z); return sampleInferredBiome(region, InferredType.LAND, x, z);
} }
if (height < fluidHeight && !biome.isAquatic()) { if (height < localFluidHeight && !biome.isAquatic()) {
return sampleInferredBiome(region, InferredType.SEA, x, z); return sampleInferredBiome(region, InferredType.SEA, x, z);
} }
if (height == fluidHeight && !biome.isShore()) { if (height == localFluidHeight && !biome.isShore()) {
return sampleInferredBiome(region, InferredType.SHORE, x, z); return sampleInferredBiome(region, InferredType.SHORE, x, z);
} }
return biome; return biome;
@@ -955,6 +954,16 @@ public class IrisComplex implements DataProvider {
} }
public NativeBlockState resolveSurfaceFluid(double x, double z) { public NativeBlockState resolveSurfaceFluid(double x, double z) {
if (transitionDisplacement != null) {
String historical = transitionDisplacement.fluidStateKey(blockCoordinate(x), blockCoordinate(z));
if (historical != null && !historical.isEmpty()) {
NativeBlockState fluid = IrisPlatforms.get().registries().blockOrNull(historical);
if (fluid == null || !fluid.isFluid()) {
throw new IllegalStateException("Historical surface fluid is unavailable: " + historical);
}
return fluid;
}
}
HydrologyColumnLayer layer = surfaceFluidLayer(x, z); HydrologyColumnLayer layer = surfaceFluidLayer(x, z);
if (layer != null) { if (layer != null) {
return resolveHydrologyFluid(layer.profileKey(), x, z); return resolveHydrologyFluid(layer.profileKey(), x, z);
@@ -1023,7 +1032,15 @@ public class IrisComplex implements DataProvider {
private double resolveHydrologyFluidSurface(double x, double z) { private double resolveHydrologyFluidSurface(double x, double z) {
HydrologyColumnLayer layer = surfaceFluidLayer(x, z); HydrologyColumnLayer layer = surfaceFluidLayer(x, z);
return layer == null ? fluidHeight : layer.fluidHeadY(); if (layer != null) {
return layer.fluidHeadY();
}
return transitionFluidHeight(x, z, fluidHeight);
}
private double transitionFluidHeight(double x, double z, double nativeHead) {
return transitionDisplacement == null ? nativeHead
: transitionDisplacement.fluidHeight(blockCoordinate(x), blockCoordinate(z), nativeHead);
} }
public HydrologyColumnSample sampleHydrologyColumn(double x, double z) { public HydrologyColumnSample sampleHydrologyColumn(double x, double z) {
@@ -1034,27 +1051,55 @@ public class IrisComplex implements DataProvider {
return hydrologyRuntime.sample(x, z).orElse(null); return hydrologyRuntime.sample(x, z).orElse(null);
} }
double hydrologyWeight = transitionHydrologyWeight(x, z); double hydrologyWeight = transitionHydrologyWeight(x, z);
if (hydrologyWeight == 0D) {
return null;
}
HydrologyColumnSample sample = hydrologyRuntime.sample(x, z).orElse(null); HydrologyColumnSample sample = hydrologyRuntime.sample(x, z).orElse(null);
if (sample == null || hydrologyWeight == 1D) { if (sample == null) {
return sample; return sample;
} }
return taperHydrologySample(sample, hydrologyWeight); return transitionHydrologySample(sample, hydrologyWeight, transitionDisplacement);
} }
private HydrologyColumnSnapshot sampleHydrologySnapshot(int x, int z) { private HydrologyColumnSnapshot sampleHydrologySnapshot(int x, int z) {
if (hydrologyRuntime == null) { if (hydrologyRuntime == null) {
return HydrologyColumnSnapshot.ready(null); return HydrologyColumnSnapshot.ready(null);
} }
double weight = transitionHydrologyWeight(x, z);
if (weight == 0D) {
return HydrologyColumnSnapshot.ready(null);
}
HydrologyColumnSnapshot snapshot = hydrologyRuntime.sampleSnapshot(x, z); HydrologyColumnSnapshot snapshot = hydrologyRuntime.sampleSnapshot(x, z);
return !snapshot.available() || snapshot.column() == null || weight == 1D ? snapshot return !snapshot.available() || snapshot.column() == null ? snapshot
: HydrologyColumnSnapshot.ready(taperHydrologySample(snapshot.column(), weight)); : HydrologyColumnSnapshot.ready(transitionHydrologySample(
snapshot.column(), transitionHydrologyWeight(x, z), transitionDisplacement));
}
static HydrologyColumnSample transitionHydrologySample(
HydrologyColumnSample sample, double weight, TransitionDisplacementField transitionDisplacement
) {
HydrologyColumnSample tapered = taperHydrologySample(sample, weight);
if (transitionDisplacement == null
|| transitionDisplacement.sample(sample.x(), sample.z()).newTerrainWeight() == 1D) {
return tapered;
}
ArrayList<HydrologyColumnLayer> layers = new ArrayList<>(tapered.layers().size());
int seaLevel = (int) Math.round(transitionDisplacement.fluidHeight(sample.x(), sample.z(), tapered.seaLevel()));
for (int index = 0; index < tapered.layers().size(); index++) {
HydrologyColumnLayer layer = tapered.layers().get(index);
if (layer.feature().type().isUnderground() || layer.feature().type().isDeepFluid()) {
layers.add(layer);
continue;
}
HydrologyColumnLayer original = sample.layers().get(index);
int head = (int) Math.round(transitionDisplacement.fluidHeight(sample.x(), sample.z(), layer.fluidHeadY()));
if ((tapered.naturalHeight() < seaLevel || tapered.naturalHeight() == seaLevel && head > seaLevel)
&& (original.terrainOwned() || original.fluidOwned() || original.grading() || original.shore())) {
continue;
}
boolean dryChannel = original.channel() && layer.bedY() > head;
layers.add(new HydrologyColumnLayer(layer.feature(), layer.bedY(),
head, head, original.channel() && !dryChannel, layer.shore(), layer.grading(), original.connectedFluid() && !dryChannel,
original.fallingFluid() && !dryChannel, original.receivingPool() && !dryChannel,
layer.terrainOwned(), original.fluidOwned() && !dryChannel, layer.oceanApron(),
layer.profileKey(), layer.surfaceBiomeKey(), layer.mouthBiomeKey(), layer.shoreBiomeKey(),
layer.bankBiomeKey(), layer.floodedCaveBiomeKey()));
}
return new HydrologyColumnSample(tapered.x(), tapered.z(), tapered.naturalHeight(), Math.max(tapered.seaLevel(), seaLevel),
tapered.ocean(), tapered.parentBiomeKey(), layers);
} }
static HydrologyColumnSample taperHydrologySample( static HydrologyColumnSample taperHydrologySample(
@@ -1067,6 +1112,9 @@ public class IrisComplex implements DataProvider {
} }
ArrayList<HydrologyColumnLayer> taperedLayers = new ArrayList<>(sample.layers().size()); ArrayList<HydrologyColumnLayer> taperedLayers = new ArrayList<>(sample.layers().size());
for (HydrologyColumnLayer layer : sample.layers()) { for (HydrologyColumnLayer layer : sample.layers()) {
if (hydrologyWeight == 0D && !layer.feature().type().isUnderground() && !layer.feature().type().isDeepFluid()) {
continue;
}
taperedLayers.add(taperHydrologyLayer(layer, sample.naturalHeight(), hydrologyWeight)); taperedLayers.add(taperHydrologyLayer(layer, sample.naturalHeight(), hydrologyWeight));
} }
return new HydrologyColumnSample( return new HydrologyColumnSample(
@@ -1087,19 +1135,21 @@ public class IrisComplex implements DataProvider {
if (layer.feature().type().isUnderground() || layer.feature().type().isDeepFluid()) { if (layer.feature().type().isUnderground() || layer.feature().type().isDeepFluid()) {
return layer; return layer;
} }
int bed = GenerationBlend.interpolateHeight(naturalHeight, layer.bedY(), hydrologyWeight);
boolean dryChannel = layer.channel() && bed > layer.fluidHeadY();
return new HydrologyColumnLayer( return new HydrologyColumnLayer(
layer.feature(), layer.feature(),
GenerationBlend.interpolateHeight(naturalHeight, layer.bedY(), hydrologyWeight), bed,
GenerationBlend.interpolateHeight(naturalHeight, layer.fluidHeadY(), hydrologyWeight), layer.fluidHeadY(),
GenerationBlend.interpolateHeight(naturalHeight, layer.ceilingY(), hydrologyWeight), layer.ceilingY(),
layer.channel(), layer.channel() && !dryChannel,
layer.shore(), layer.shore(),
layer.grading(), layer.grading(),
layer.connectedFluid(), layer.connectedFluid() && !dryChannel,
layer.fallingFluid(), layer.fallingFluid() && !dryChannel,
layer.receivingPool(), layer.receivingPool() && !dryChannel,
layer.terrainOwned(), layer.terrainOwned(),
layer.fluidOwned(), layer.fluidOwned() && !dryChannel,
layer.oceanApron(), layer.oceanApron(),
layer.profileKey(), layer.profileKey(),
layer.surfaceBiomeKey(), layer.surfaceBiomeKey(),
@@ -1114,14 +1164,14 @@ public class IrisComplex implements DataProvider {
} }
private double transitionHydrologyWeight(double x, double z) { private double transitionHydrologyWeight(double x, double z) {
return transitionGenerationPlan == null return transitionDisplacement == null
? 1D ? 1D
: transitionGenerationPlan.hydrologyWeightAt(blockCoordinate(x), blockCoordinate(z)); : transitionDisplacement.seamWeight(blockCoordinate(x), blockCoordinate(z));
} }
/** Whether the column's hydrology can be sampled without waiting for a plan (see Engine.answersFromNaturalTerrain). */ /** Whether the column's hydrology can be sampled without waiting for a plan (see Engine.answersFromNaturalTerrain). */
public boolean isHydrologyPlanned(int x, int z) { public boolean isHydrologyPlanned(int x, int z) {
if (hydrologyRuntime == null || transitionHydrologyWeight(x, z) == 0D) { if (hydrologyRuntime == null) {
return true; return true;
} }
if (!hydrologyRuntime.isPlanned(x, z)) { if (!hydrologyRuntime.isPlanned(x, z)) {
@@ -1144,23 +1194,7 @@ public class IrisComplex implements DataProvider {
} }
public ProceduralStream<Double> getHeightStream() { public ProceduralStream<Double> getHeightStream() {
return placementHeightStream; return heightStream;
}
public Optional<TerrainBoundarySignature> resolvedTerrainColumn(int blockX, int blockZ) {
if (transitionGenerationPlan == null || isNaturalTerrainContext()
|| !transitionGenerationPlan.hasTransitionAtChunk(blockX >> 4, blockZ >> 4)) {
return Optional.empty();
}
return Optional.of(resolvedTerrain.column(blockX, blockZ));
}
public OptionalInt resolvedTerrainHeight(int blockX, int blockZ, boolean ignoreFluid) {
if (transitionGenerationPlan == null || isNaturalTerrainContext()
|| !transitionGenerationPlan.hasTransitionAtChunk(blockX >> 4, blockZ >> 4)) {
return OptionalInt.empty();
}
return OptionalInt.of(resolvedTerrain.height(blockX, blockZ, ignoreFluid));
} }
boolean isNaturalTerrainContext() { boolean isNaturalTerrainContext() {
@@ -1170,11 +1204,6 @@ public class IrisComplex implements DataProvider {
&& context.getChunkContext().isNaturalTerrain(); && context.getChunkContext().isNaturalTerrain();
} }
private double samplePlacementHeight(double x, double z) {
OptionalInt resolved = resolvedTerrainHeight(blockCoordinate(x), blockCoordinate(z), true);
return resolved.isPresent() ? resolved.getAsInt() : heightStream.getDouble(x, z);
}
public boolean isHistoricalChunk(int chunkX, int chunkZ) { public boolean isHistoricalChunk(int chunkX, int chunkZ) {
return transitionGenerationPlan != null return transitionGenerationPlan != null
&& transitionGenerationPlan.boundary().isHistoricalChunk(chunkX, chunkZ); && transitionGenerationPlan.boundary().isHistoricalChunk(chunkX, chunkZ);
@@ -1214,8 +1243,16 @@ public class IrisComplex implements DataProvider {
int maximumX, int maximumX,
int maximumZ int maximumZ
) { ) {
return transitionGenerationPlan == null if (transitionGenerationPlan == null) {
|| transitionGenerationPlan.allowsNewFootprint(minimumX, minimumZ, maximumX, maximumZ); return true;
}
if (!transitionGenerationPlan.allowsNewFootprint(minimumX, minimumZ, maximumX, maximumZ)) {
return false;
}
GenerationHistoryRuntimeRouter router = terrainEngine instanceof IrisEngine irisEngine
? irisEngine.getGenerationHistoryRuntimeRouter().orElse(null) : null;
return router == null || router.allowsObjectFootprint(transitionGenerationPlan,
new ObjectContinuationBundle.Bounds(minimumX, minimumZ, maximumX, maximumZ));
} }
public Optional<String> historicalPhysicalBiomeKeyAt(int blockX, int blockY, int blockZ) { public Optional<String> historicalPhysicalBiomeKeyAt(int blockX, int blockY, int blockZ) {
@@ -1248,7 +1285,8 @@ public class IrisComplex implements DataProvider {
return mapped; return mapped;
} }
double terrainHeight = naturalHeightStream.getDouble(x, z); double terrainHeight = naturalHeightStream.getDouble(x, z);
return fixBiomeType(terrainHeight, baseBiomeStream.get(x, z), regionStream.get(x, z), (double) x, (double) z, fluidHeight); return fixBiomeType(terrainHeight, baseBiomeStream.get(x, z), regionStream.get(x, z), (double) x, (double) z,
transitionFluidHeight(x, z, fluidHeight));
} }
private HydrologyColumnLayer surfaceLayer(double x, double z) { private HydrologyColumnLayer surfaceLayer(double x, double z) {
@@ -1336,7 +1374,7 @@ public class IrisComplex implements DataProvider {
regionStream.get(x, z), regionStream.get(x, z),
x, x,
z, z,
fluidHeight transitionFluidHeight(x, z, fluidHeight)
); );
} }
@@ -2094,6 +2132,8 @@ public class IrisComplex implements DataProvider {
hydrologyRuntime.close(); hydrologyRuntime.close();
} }
releaseHydrologyNoiseCaches(); releaseHydrologyNoiseCaches();
resolvedTerrain.clear(); if (transitionDisplacement != null) {
transitionDisplacement.clear();
}
} }
} }
@@ -54,6 +54,9 @@ import art.arcane.volmlib.util.hunk.Hunk;
import art.arcane.volmlib.util.mantle.flag.MantleFlag; import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import art.arcane.volmlib.util.math.M; import art.arcane.volmlib.util.math.M;
import art.arcane.volmlib.util.matter.MatterStructurePOI; import art.arcane.volmlib.util.matter.MatterStructurePOI;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.iris.generation.mantle.ObjectContinuationBundle;
import art.arcane.volmlib.util.scheduling.ChronoLatch; import art.arcane.volmlib.util.scheduling.ChronoLatch;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import lombok.AccessLevel; import lombok.AccessLevel;
@@ -1478,7 +1481,18 @@ public class IrisEngine implements Engine {
boolean skipRealFlag = platformHooks.shouldBypassMantleStages(this); boolean skipRealFlag = platformHooks.shouldBypassMantleStages(this);
if (!skipRealFlag) { if (!skipRealFlag) {
getMantle().getMantle().flag(x >> 4, z >> 4, MantleFlag.REAL, true); MantleChunk<Matter> chunk = getMantle().getMantle().getChunk(x >> 4, z >> 4).use();
try {
synchronized (chunk) {
Matter section = chunk.get(0);
if (section != null) {
section.deleteSlice(ObjectContinuationBundle.class);
}
chunk.flag(MantleFlag.REAL, true);
}
} finally {
chunk.release();
}
} }
getMetrics().getTotal().put(p.getMilliseconds()); getMetrics().getTotal().put(p.getMilliseconds());
generated.incrementAndGet(); generated.incrementAndGet();
@@ -1,261 +0,0 @@
package art.arcane.iris.generation.runtime;
import art.arcane.iris.generation.stage.IrisTransitionGeometryActuator;
import art.arcane.iris.world.history.BoundaryColumnGeometry;
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.world.history.FloatingBiomeOverlay;
import art.arcane.iris.world.history.SavedTerrainChunk;
import art.arcane.iris.world.history.TerrainBoundarySignature;
import art.arcane.iris.world.history.TransitionGenerationPlan;
import art.arcane.iris.world.history.TransitionFluidContainment;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.volmlib.nativelib.terrain.NativeBiome;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.generation.context.ChunkContext;
import art.arcane.iris.generation.context.IrisContext;
import art.arcane.volmlib.util.hunk.Hunk;
import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
public final class ResolvedTerrainProvider {
private static final int MAXIMUM_CACHED_CHUNKS = 64;
private static final int MAXIMUM_HEIGHT_CHUNKS = 4_096;
private final Engine engine;
private final LinkedHashMap<Long, ResolvedTerrain> completed = new LinkedHashMap<>(64, 0.75F, true);
private final ConcurrentHashMap<Long, CompletableFuture<ResolvedTerrain>> pending = new ConcurrentHashMap<>();
private final LinkedHashMap<Long, ResolvedTerrain> rawCompleted = new LinkedHashMap<>(64, 0.75F, true);
private final ConcurrentHashMap<Long, CompletableFuture<ResolvedTerrain>> rawPending = new ConcurrentHashMap<>();
private final LinkedHashMap<Long, int[]> heights = new LinkedHashMap<>(64, 0.75F, true);
public ResolvedTerrainProvider(Engine engine) {
this.engine = engine;
}
public TerrainBoundarySignature column(int blockX, int blockZ) {
return resolve(blockX >> 4, blockZ >> 4).terrain().column(blockX, blockZ);
}
public int height(int blockX, int blockZ, boolean ignoreFluid) {
long key = chunkKey(blockX >> 4, blockZ >> 4);
synchronized (heights) {
int[] cached = heights.get(key);
if (cached != null) {
return cached[(ignoreFluid ? 256 : 0) + (blockX & 15) * 16 + (blockZ & 15)];
}
}
TerrainBoundarySignature column = resolve(blockX >> 4, blockZ >> 4).terrain().column(blockX, blockZ);
return ignoreFluid ? column.oceanFloorHeight() : column.surfaceHeight();
}
public void generate(EngineMode mode, int x, int z, Hunk<NativeBlockState> blocks,
Hunk<NativeBiome> biomes, boolean multicore, ChunkContext context) {
SavedTerrainChunk terrain;
if (context.getComplex().getTransitionGenerationPlan() != null
&& context.getComplex().getTransitionGenerationPlan().hasTransitionAtChunk(x >> 4, z >> 4)) {
ResolvedTerrain resolved = resolve(x >> 4, z >> 4);
terrain = resolved.terrain();
context.setFloatingBiomes(resolved.floatingBiomes());
copy(terrain, blocks, biomes, context);
} else {
mode.generateTerrain(x, z, blocks, biomes, multicore, context);
terrain = capture(x, z, blocks, biomes, context, true);
}
if (engine instanceof IrisEngine irisEngine) {
GenerationHistoryRuntimeRouter router = irisEngine.getGenerationHistoryRuntimeRouter().orElse(null);
if (router != null) {
router.recordNaturalTerrain(terrain.boundaryOnly());
router.recordFloatingBiomes(x >> 4, z >> 4, context.getFloatingBiomes());
}
}
}
public void clear() {
synchronized (completed) {
completed.clear();
}
synchronized (rawCompleted) {
rawCompleted.clear();
}
synchronized (heights) {
heights.clear();
}
}
private ResolvedTerrain resolve(int chunkX, int chunkZ) {
return resolve(chunkX, chunkZ, false);
}
private ResolvedTerrain resolve(int chunkX, int chunkZ, boolean raw) {
LinkedHashMap<Long, ResolvedTerrain> cache = raw ? rawCompleted : completed;
ConcurrentHashMap<Long, CompletableFuture<ResolvedTerrain>> loads = raw ? rawPending : pending;
long key = chunkKey(chunkX, chunkZ);
synchronized (cache) {
ResolvedTerrain cached = cache.get(key);
if (cached != null) {
return cached;
}
}
CompletableFuture<ResolvedTerrain> result = new CompletableFuture<>();
CompletableFuture<ResolvedTerrain> existing = loads.putIfAbsent(key, result);
if (existing != null) {
return existing.join();
}
try {
synchronized (cache) {
ResolvedTerrain published = cache.get(key);
if (published != null) {
result.complete(published);
return published;
}
}
ResolvedTerrain terrain = raw ? compute(chunkX, chunkZ) : contain(chunkX, chunkZ);
if (!raw) {
cacheHeights(key, terrain.terrain());
}
synchronized (cache) {
cache.put(key, terrain);
while (cache.size() > MAXIMUM_CACHED_CHUNKS) {
cache.pollFirstEntry();
}
}
result.complete(terrain);
return terrain;
} catch (RuntimeException | Error failure) {
result.completeExceptionally(failure);
throw failure;
} finally {
loads.remove(key, result);
}
}
private static long chunkKey(int chunkX, int chunkZ) {
return ((long) chunkX << 32) | (chunkZ & 0xffffffffL);
}
private void cacheHeights(long key, SavedTerrainChunk terrain) {
int[] values = new int[512];
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
TerrainBoundarySignature column = terrain.column((terrain.chunkX() << 4) + x,
(terrain.chunkZ() << 4) + z);
int index = x * 16 + z;
values[index] = column.surfaceHeight();
values[256 + index] = column.oceanFloorHeight();
}
}
synchronized (heights) {
heights.put(key, values);
while (heights.size() > MAXIMUM_HEIGHT_CHUNKS) {
heights.pollFirstEntry();
}
}
}
private ResolvedTerrain contain(int chunkX, int chunkZ) {
ResolvedTerrain raw = resolve(chunkX, chunkZ, true);
TransitionGenerationPlan plan = engine.getComplex().getTransitionGenerationPlan();
if (plan == null || !plan.hasTransitionAtChunk(chunkX, chunkZ)) {
return raw;
}
try {
return new ResolvedTerrain(TransitionFluidContainment.contain(raw.terrain(), plan,
(x, z) -> rawColumn(plan, raw.terrain(), x, z)), raw.floatingBiomes());
} catch (IOException failure) {
throw new IllegalStateException("Unable to contain transition fluids at " + chunkX + "," + chunkZ, failure);
}
}
private BoundaryColumnGeometry rawColumn(TransitionGenerationPlan plan, SavedTerrainChunk current, int x, int z)
throws IOException {
if (plan.isHistoricalBlock(x, z)) {
BoundaryColumnGeometry geometry = plan.terrainSignatures().signatureAt(x, z)
.orElseThrow(() -> new IOException("Missing saved terrain boundary column at " + x + "," + z))
.geometry();
if (geometry.minimumY() != engine.getMinHeight() || geometry.height() != engine.getHeight()) {
throw new IOException("Saved terrain boundary column has the wrong vertical layout at " + x + "," + z);
}
return geometry;
}
return (x >> 4) == current.chunkX() && (z >> 4) == current.chunkZ()
? current.column(x, z).geometry() : resolve(x >> 4, z >> 4, true).terrain().column(x, z).geometry();
}
private ResolvedTerrain compute(int chunkX, int chunkZ) {
int x = Math.multiplyExact(chunkX, 16);
int z = Math.multiplyExact(chunkZ, 16);
try (GenerationHistoryRuntimeRouter.CoordinateScope runtimeScope = engine instanceof IrisEngine irisEngine
? irisEngine.openGenerationHistoryCoordinateScope(x, z) : null) {
IrisContext active = IrisContext.get();
long sessionId = active == null ? 0L : active.getGenerationSessionId();
ChunkContext context = new ChunkContext(x, z, engine.getComplex(), sessionId, true,
ChunkContext.PrefillPlan.NATURAL_TERRAIN, engine.getMetrics(), engine.getDimensionStackContext());
context.beginSpeculativeTerrain();
Hunk<NativeBlockState> blocks = Hunk.newArrayHunk(16, engine.getHeight(), 16);
Hunk<NativeBiome> biomes = Hunk.newArrayHunk(16, engine.getHeight(), 16);
try (IrisContext.Scope ignored = IrisContext.open(engine, sessionId, context)) {
engine.getMode().generateTerrain(x, z, blocks, biomes, false, context);
return new ResolvedTerrain(capture(x, z, blocks, biomes, context, false), context.getFloatingBiomes());
}
} catch (IOException failure) {
throw new IllegalStateException("Unable to scope natural terrain at " + chunkX + "," + chunkZ, failure);
}
}
private SavedTerrainChunk capture(int x, int z, Hunk<NativeBlockState> blocks,
Hunk<NativeBiome> biomes, ChunkContext context, boolean boundaryOnly) {
FloatingBiomeOverlay floating = context.getFloatingBiomes();
if (floating != null) {
floating.retainHighestSurfaces((localX, localZ) -> highestSolid(blocks, localX, localZ));
}
try {
return IrisTransitionGeometryActuator.capture(x, z, blocks, biomes, engine.getMinHeight(), context, boundaryOnly);
} catch (IOException failure) {
throw new IllegalStateException("Unable to capture natural terrain at " + (x >> 4) + "," + (z >> 4), failure);
}
}
private static int highestSolid(Hunk<NativeBlockState> blocks, int localX, int localZ) {
for (int y = blocks.getHeight() - 1; y >= 0; y--) {
NativeBlockState state = blocks.getRaw(localX, y, localZ);
if (state != null && !state.isAir() && !state.isFluid()) {
return y;
}
}
return -1;
}
private static void copy(SavedTerrainChunk terrain, Hunk<NativeBlockState> blocks,
Hunk<NativeBiome> biomes, ChunkContext context) {
PlatformRegistries registries = IrisPlatforms.get().registries();
Map<String, NativeBlockState> states = new HashMap<>();
Map<String, NativeBiome> physicalBiomes = new HashMap<>();
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
TerrainBoundarySignature column = terrain.column((terrain.chunkX() << 4) + x, (terrain.chunkZ() << 4) + z);
BoundaryColumnGeometry geometry = column.geometry();
for (int offset = 0; offset < blocks.getHeight(); offset++) {
String stateKey = geometry.voxelAt(geometry.minimumY() + offset).stateKey();
NativeBlockState state = states.computeIfAbsent(stateKey, registries::blockOrNull);
if (state == null) {
throw new IllegalStateException("Resolved terrain requires unavailable block state " + stateKey);
}
blocks.setRaw(x, offset, z, state);
String biomeKey = column.biomeAtSample(offset / 4);
biomes.setRaw(x, offset, z, physicalBiomes.computeIfAbsent(biomeKey, registries::biome));
}
context.setTerrainHeight(x, z, column.oceanFloorHeight());
}
}
}
private record ResolvedTerrain(SavedTerrainChunk terrain, FloatingBiomeOverlay floatingBiomes) {
}
}
@@ -64,7 +64,7 @@ public class IrisBiomeActuator extends EngineAssignedActuator<NativeBiome> {
int depth = h.getDepth(); int depth = h.getDepth();
int height = h.getHeight(); int height = h.getHeight();
Engine engine = getEngine(); Engine engine = getEngine();
Mantle<Matter> mantle = context.isSpeculativeTerrain() ? null : engine.getMantle().getMantle(); Mantle<Matter> mantle = engine.getMantle().getMantle();
ChunkedDataCache<IrisBiome> biomeCache = context.getBiome(); ChunkedDataCache<IrisBiome> biomeCache = context.getBiome();
IrisComplex complex = context.getComplex(); IrisComplex complex = context.getComplex();
DimensionStackContext dimensionStackContext = engine.getDimensionStackContext(); DimensionStackContext dimensionStackContext = engine.getDimensionStackContext();
@@ -109,7 +109,7 @@ public class IrisBiomeActuator extends EngineAssignedActuator<NativeBiome> {
public static void publishNaturalMetadata(Engine engine, int x, int z, Hunk<NativeBiome> biomes, public static void publishNaturalMetadata(Engine engine, int x, int z, Hunk<NativeBiome> biomes,
ChunkContext context) { ChunkContext context) {
if (context.isSpeculativeTerrain() || !context.getComplex().allowsMantleChunkWrite(x >> 4, z >> 4)) { if (!context.getComplex().allowsMantleChunkWrite(x >> 4, z >> 4)) {
return; return;
} }
Mantle<Matter> mantle = engine.getMantle().getMantle(); Mantle<Matter> mantle = engine.getMantle().getMantle();
@@ -146,7 +146,7 @@ public class IrisBiomeActuator extends EngineAssignedActuator<NativeBiome> {
IrisComplex complex IrisComplex complex
) { ) {
TransitionGenerationPlan transitionPlan = complex.getTransitionGenerationPlan(); TransitionGenerationPlan transitionPlan = complex.getTransitionGenerationPlan();
if (transitionPlan == null) { if (transitionPlan == null || !transitionPlan.hasTransitionAtChunk(worldX >> 4, worldZ >> 4)) {
return; return;
} }
TransitionGenerationPlan.TerrainSample terrainSample = transitionPlan.terrainSampleAt(worldX, worldZ); TransitionGenerationPlan.TerrainSample terrainSample = transitionPlan.terrainSampleAt(worldX, worldZ);
@@ -82,8 +82,7 @@ public final class IrisDimensionStackActuator extends EngineAssignedActuator<Nat
if (getEngine().getDimensionStackContext() == null) { if (getEngine().getDimensionStackContext() == null) {
return; return;
} }
MantleChunk<Matter> mantleChunk = context.isSpeculativeTerrain() ? null MantleChunk<Matter> mantleChunk = getEngine().getMantle().getMantle().getChunk(x >> 4, z >> 4).use();
: getEngine().getMantle().getMantle().getChunk(x >> 4, z >> 4).use();
try { try {
MetadataCleaner metadata = new MetadataCleaner(mantleChunk, blocks.getHeight()); MetadataCleaner metadata = new MetadataCleaner(mantleChunk, blocks.getHeight());
for (int localX = 0; localX < blocks.getWidth(); localX++) { for (int localX = 0; localX < blocks.getWidth(); localX++) {
@@ -431,9 +431,7 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Nativ
} }
context.setNaturalBiome(wx & 15, y, wz & 15, selected); context.setNaturalBiome(wx & 15, y, wz & 15, selected);
context.floatingBiomes(chunkHeight).record(wx & 15, y, wz & 15, matter.identity()); context.floatingBiomes(chunkHeight).record(wx & 15, y, wz & 15, matter.identity());
if (!context.isSpeculativeTerrain()) { getEngine().getMantle().getMantle().set(wx, y, wz, BiomeInjectMatter.get(selected.key()));
getEngine().getMantle().getMantle().set(wx, y, wz, BiomeInjectMatter.get(selected.key()));
}
} }
} catch (Throwable e) { } catch (Throwable e) {
IrisLogging.reportError(e); IrisLogging.reportError(e);
@@ -149,7 +149,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
boolean riverOwned = padRiverBed && hydrologyTerrain != null && hydrologyTerrain.terrainOwned(); boolean riverOwned = padRiverBed && hydrologyTerrain != null && hydrologyTerrain.terrainOwned();
IrisRiverMaterialConfig roleMaterial = hydrologyRoleMaterial( IrisRiverMaterialConfig roleMaterial = hydrologyRoleMaterial(
hydrologyTerrain, bedMaterial, shoreMaterial, bankMaterial); hydrologyTerrain, bedMaterial, shoreMaterial, bankMaterial);
NativeBlockState fluid = hydrologyFluid == null NativeBlockState fluid = hydrologyFluid == null || complex.getTransitionDisplacement() != null
? complex.resolveSurfaceFluid(realX, realZ) ? complex.resolveSurfaceFluid(realX, realZ)
: complex.resolveHydrologyFluid(hydrologyFluid.profileKey(), realX, realZ); : complex.resolveHydrologyFluid(hydrologyFluid.profileKey(), realX, realZ);
NativeBlockState rock = rockCache.get(xf, zf); NativeBlockState rock = rockCache.get(xf, zf);
@@ -5,7 +5,6 @@ import art.arcane.iris.generation.runtime.EngineAssignedComponent;
import art.arcane.iris.generation.runtime.EngineStage; import art.arcane.iris.generation.runtime.EngineStage;
import art.arcane.iris.world.history.BoundaryColumnGeometry; import art.arcane.iris.world.history.BoundaryColumnGeometry;
import art.arcane.iris.world.history.BoundaryGeometryInfluence; import art.arcane.iris.world.history.BoundaryGeometryInfluence;
import art.arcane.iris.world.history.GenerationBlend;
import art.arcane.iris.world.history.SavedTerrainChunk; import art.arcane.iris.world.history.SavedTerrainChunk;
import art.arcane.iris.world.history.TransitionGenerationPlan; import art.arcane.iris.world.history.TransitionGenerationPlan;
import art.arcane.iris.world.history.TransitionGeometryBlender; import art.arcane.iris.world.history.TransitionGeometryBlender;
@@ -70,9 +69,7 @@ public final class IrisTransitionGeometryActuator extends EngineAssignedComponen
if (blended == current) { if (blended == current) {
continue; continue;
} }
double expectedFloor = GenerationBlend.interpolate( double expectedFloor = context.getRoundedHeight(localX, localZ);
plan.terrainSampleAt(x + localX, z + localZ).historicalOceanFloorHeight(),
context.getRoundedHeight(localX, localZ), influence.newTerrainWeight());
int floor = blended.surfaceOffsetNear(expectedFloor); int floor = blended.surfaceOffsetNear(expectedFloor);
List<BoundaryColumnGeometry.Voxel> voxels = blended.voxels(); List<BoundaryColumnGeometry.Voxel> voxels = blended.voxels();
for (int offset = 0; offset < voxels.size(); offset++) { for (int offset = 0; offset < voxels.size(); offset++) {
@@ -86,6 +86,30 @@ public final class Terrain3DColumn {
return nearest; return nearest;
} }
public Terrain3DColumn displaced(int offset, int height) {
if (height < 1) {
throw new IllegalArgumentException("Terrain column height must be positive");
}
if (offset == 0) {
return this;
}
double displacedBase = Math.clamp(baseHeight + offset, 0D, height - 1D);
int[] displaced = new int[boundaries.length];
int count = 0;
for (int index = 0; index < boundaries.length; index += 2) {
long minimum = index == 0 && boundaries[index] == 0 ? 0L : (long) boundaries[index] + offset;
long maximum = (long) boundaries[index + 1] + offset;
if (minimum >= height || maximum < 0L) {
continue;
}
displaced[count++] = (int) Math.max(0L, minimum);
displaced[count++] = (int) Math.min(height - 1L, maximum);
}
return count == 0 ? unshaped(displacedBase, height)
: new Terrain3DColumn(displacedBase, Math.clamp((long) minY + offset, 0, height),
shaped, Arrays.copyOf(displaced, count));
}
@Override @Override
public boolean equals(Object value) { public boolean equals(Object value) {
return this == value || value instanceof Terrain3DColumn other return this == value || value instanceof Terrain3DColumn other
@@ -98,7 +98,8 @@ public final class BukkitStructureStagePolicy implements StructureStagePolicy<
@Override @Override
public NativeBlockPositionPredicate protectedPositions(Engine context) { public NativeBlockPositionPredicate protectedPositions(Engine context) {
return GenerationWritePolicy.protectedPositions(context.getDimension().getStaticObjectLayer(context.getData()), return GenerationWritePolicy.protectedPositions(context.getDimension().getStaticObjectLayer(context.getData())
.forTransition(context),
context.getDimensionStackContext(), context.getMinHeight()); context.getDimensionStackContext(), context.getMinHeight());
} }
@@ -8,7 +8,6 @@ import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.platform.generation.BukkitChunkGenerator; import art.arcane.iris.platform.generation.BukkitChunkGenerator;
import art.arcane.iris.studio.generation.JigsawStudioGenerator; import art.arcane.iris.studio.generation.JigsawStudioGenerator;
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter; import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.world.history.TerrainBoundarySignature;
import art.arcane.iris.world.history.TerrainNativeBlockKeys; import art.arcane.iris.world.history.TerrainNativeBlockKeys;
import art.arcane.volmlib.nativelib.terrain.NativeBlockColumn; import art.arcane.volmlib.nativelib.terrain.NativeBlockColumn;
import art.arcane.volmlib.nativelib.terrain.NativeTerrainColumnPolicy; import art.arcane.volmlib.nativelib.terrain.NativeTerrainColumnPolicy;
@@ -99,8 +98,7 @@ public final class BukkitTerrainColumnPolicy implements NativeTerrainColumnPolic
@Override @Override
public Optional<? extends NativeBlockColumn> resolvedColumn() { public Optional<? extends NativeBlockColumn> resolvedColumn() {
return engine.getComplex().resolvedTerrainColumn(query.blockX(), query.blockZ()) return Optional.empty();
.map(TerrainBoundarySignature::geometry);
} }
@Override @Override
@@ -94,7 +94,8 @@ public class ImportedFeaturePolicy implements NativeImportedFeaturePolicy<IrisDi
@Override @Override
public Placement placement() { public Placement placement() {
IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData()); IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData())
.forTransition(engine);
int minimumY = engine.getMinHeight(); int minimumY = engine.getMinHeight();
DimensionStackContext stack = engine.getDimensionStackContext(); DimensionStackContext stack = engine.getDimensionStackContext();
if (staticObjects.isEmpty() && stack == null) { if (staticObjects.isEmpty() && stack == null) {
@@ -22,9 +22,7 @@ import art.arcane.iris.generation.block.TileData;
import art.arcane.iris.pack.loading.IrisData; import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.world.history.TerrainBoundarySignature;
import java.util.Optional;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState; import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@@ -61,11 +59,6 @@ public interface IObjectPlacer {
return getFluidHeight(); return getFluidHeight();
} }
int coordinateShift = getFluidHeight() - engine.getDimension().getFluidHeight(); int coordinateShift = getFluidHeight() - engine.getDimension().getFluidHeight();
Optional<TerrainBoundarySignature> resolved = engine.getComplex().resolvedTerrainColumn(x, z);
if (resolved.isPresent()) {
return resolved.get().fluidHeight().isPresent()
? coordinateShift + resolved.get().fluidHeight().getAsInt() : -1;
}
return coordinateShift + (int) Math.round( return coordinateShift + (int) Math.round(
engine.getComplex().getRiverWaterSurfaceStream().get(x, z) engine.getComplex().getRiverWaterSurfaceStream().get(x, z)
); );
@@ -1,12 +1,18 @@
package art.arcane.iris.structure.object; package art.arcane.iris.structure.object;
import art.arcane.iris.generation.block.TileData; import art.arcane.iris.generation.block.TileData;
import art.arcane.iris.generation.mantle.ObjectContinuationBundle;
import art.arcane.iris.generation.mantle.ObjectContinuationPersistence;
import art.arcane.iris.world.storage.matter.IrisMatterSupport;
import art.arcane.iris.generation.terrain.IrisDimension; import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.pack.value.IrisPosition; import art.arcane.iris.pack.value.IrisPosition;
import art.arcane.iris.integration.Identifier; import art.arcane.iris.integration.Identifier;
import art.arcane.iris.pack.loading.IrisData; import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial; import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState; import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.hunk.Hunk; import art.arcane.volmlib.util.hunk.Hunk;
@@ -15,29 +21,44 @@ import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk; import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.matter.Matter; import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.IrisMatter;
import art.arcane.volmlib.util.matter.MatterMarker; import art.arcane.volmlib.util.matter.MatterMarker;
import art.arcane.volmlib.util.matter.MatterSlice; import art.arcane.volmlib.util.matter.MatterSlice;
import art.arcane.volmlib.util.matter.MatterStructurePOI; import art.arcane.volmlib.util.matter.MatterStructurePOI;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.function.Supplier;
/** /**
* Compiles fixed placements once per dimension snapshot and writes only the current chunk's cells. * Compiles fixed placements once per dimension snapshot and writes only the current chunk's cells.
* Stored Y coordinates are offsets from the dimension floor; configured positions use absolute world Y. * Stored Y coordinates are offsets from the dimension floor; configured positions use absolute world Y.
*/ */
public final class IrisStaticObjectLayer { public final class IrisStaticObjectLayer {
private static final IrisStaticObjectLayer EMPTY = new IrisStaticObjectLayer(Map.of()); private static final IrisStaticObjectLayer EMPTY = new IrisStaticObjectLayer(Map.of(), List.of());
private static final List<Class<?>> REPLACED_METADATA = List.of( private static final List<Class<?>> REPLACED_METADATA = List.of(
TileWrapper.class, Identifier.class, String.class, TreeBlockMaterial.class, TileWrapper.class, Identifier.class, String.class, TreeBlockMaterial.class,
MatterMarker.class, MatterStructurePOI.class); MatterMarker.class, MatterStructurePOI.class);
private final Map<Long, Chunk> chunks; private final Map<Long, Chunk> chunks;
private final List<Placement> placements;
private final Map<Long, List<Placement>> placementsByChunk;
private volatile TransitionLayer transitionLayer;
private IrisStaticObjectLayer(Map<Long, Chunk> chunks) { private IrisStaticObjectLayer(Map<Long, Chunk> chunks, List<Placement> placements) {
this.chunks = chunks; this.chunks = chunks;
this.placements = placements;
Map<Long, List<Placement>> indexed = new HashMap<>();
for (Placement placement : placements) {
for (long chunk : placement.chunks().keySet()) {
indexed.computeIfAbsent(chunk, key -> new ArrayList<>()).add(placement);
}
}
indexed.replaceAll((chunk, entries) -> List.copyOf(entries));
this.placementsByChunk = Map.copyOf(indexed);
} }
public static IrisStaticObjectLayer compile(IrisDimension dimension, IrisData data) { public static IrisStaticObjectLayer compile(IrisDimension dimension, IrisData data) {
@@ -45,7 +66,7 @@ public final class IrisStaticObjectLayer {
if (dimension.getStaticObjects().isEmpty()) { if (dimension.getStaticObjects().isEmpty()) {
return EMPTY; return EMPTY;
} }
Compilation compilation = new Compilation(dimension.getMaxHeight() - dimension.getMinHeight()); List<Placement> placements = new ArrayList<>(dimension.getStaticObjects().size());
for (int index = 0; index < dimension.getStaticObjects().size(); index++) { for (int index = 0; index < dimension.getStaticObjects().size(); index++) {
IrisStaticObject entry = dimension.getStaticObjects().get(index); IrisStaticObject entry = dimension.getStaticObjects().get(index);
try { try {
@@ -65,15 +86,48 @@ public final class IrisStaticObjectLayer {
} else if (entry.isSmartBore()) { } else if (entry.isSmartBore()) {
object = object.copy(); object = object.copy();
} }
Compilation compilation = new Compilation(dimension.getMaxHeight() - dimension.getMinHeight());
IrisPosition position = entry.getPosition(); IrisPosition position = entry.getPosition();
object.place(position.getX(), position.getY() - dimension.getMinHeight(), object.place(position.getX(), position.getY() - dimension.getMinHeight(),
position.getZ(), compilation, entry.toPlacement(), new RNG(entry.getSeed()), data); position.getZ(), compilation, entry.toPlacement(), new RNG(entry.getSeed()), data);
placements.add(compilation.freeze(new ObjectContinuationBundle.PlacementKey(
ObjectContinuationBundle.Kind.STATIC, position.getX() >> 4, position.getZ() >> 4, index)));
} catch (RuntimeException failure) { } catch (RuntimeException failure) {
throw new IllegalArgumentException("Invalid staticObjects[" + index + "] in dimension '" throw new IllegalArgumentException("Invalid staticObjects[" + index + "] in dimension '"
+ dimension.getLoadKey() + "': " + failure.getMessage(), failure); + dimension.getLoadKey() + "': " + failure.getMessage(), failure);
} }
} }
return compilation.freeze(); return merge(placements);
}
public IrisStaticObjectLayer forTransition(Engine engine) {
if (engine == null || placements.isEmpty()) {
return this;
}
IrisComplex complex = engine.getComplex();
if (complex.getTransitionGenerationPlan() == null) {
return this;
}
GenerationHistoryRuntimeRouter router = engine instanceof IrisEngine irisEngine
? irisEngine.getGenerationHistoryRuntimeRouter().orElse(null) : null;
boolean cacheable = !(engine instanceof IrisEngine) || router != null;
TransitionLayer cached = transitionLayer;
if (cacheable && cached != null && cached.complex() == complex && cached.router() == router) {
return cached.layer();
}
List<Placement> accepted = new ArrayList<>(placements.size());
for (Placement placement : placements) {
Footprint footprint = placement.footprint();
if (footprint == null || complex.allowsNewGenerationFootprint(
footprint.minimumX(), footprint.minimumZ(), footprint.maximumX(), footprint.maximumZ())) {
accepted.add(placement);
}
}
IrisStaticObjectLayer filtered = accepted.size() == placements.size() ? this : merge(accepted);
if (cacheable) {
transitionLayer = new TransitionLayer(complex, router, filtered);
}
return filtered;
} }
public List<Block> blocks(int chunkX, int chunkZ) { public List<Block> blocks(int chunkX, int chunkZ) {
@@ -99,6 +153,7 @@ public final class IrisStaticObjectLayer {
if (blocks.isEmpty()) { if (blocks.isEmpty()) {
return; return;
} }
persistContinuations(engine, x >> 4, z >> 4, output.getHeight());
MantleChunk<Matter> chunk = engine.getMantle().getMantle().getChunk(x >> 4, z >> 4); MantleChunk<Matter> chunk = engine.getMantle().getMantle().getChunk(x >> 4, z >> 4);
chunk.use(); chunk.use();
try { try {
@@ -127,6 +182,58 @@ public final class IrisStaticObjectLayer {
} }
} }
private void persistContinuations(Engine engine, int chunkX, int chunkZ, int height) {
for (Placement placement : placementsByChunk.getOrDefault(chunkKey(chunkX, chunkZ), List.of())) {
if (placement.chunks().size() < 2) {
continue;
}
Map<ObjectContinuationBundle.ChunkPosition, Supplier<Matter>> fragments = new HashMap<>();
for (Map.Entry<Long, Chunk> destination : placement.chunks().entrySet()) {
long key = destination.getKey();
fragments.put(new ObjectContinuationBundle.ChunkPosition((int) (key >> 32), (int) key),
() -> continuationPayload(destination.getValue(), height));
}
Footprint footprint = placement.footprint();
ObjectContinuationPersistence.persist(engine.getMantle().getMantle(), placement.key(),
new ObjectContinuationBundle.Bounds(footprint.minimumX(), footprint.minimumZ(),
footprint.maximumX(), footprint.maximumZ()), fragments);
}
}
private static Matter continuationPayload(Chunk chunk, int height) {
IrisMatterSupport.ensureRegistered();
Matter payload = new IrisMatter(16, height, 16);
for (Block block : chunk.ordered()) {
ObjectContinuationPersistence.put(payload, block.x(), block.y(), block.z(), block.state());
if (block.tile() != null) {
ObjectContinuationPersistence.put(payload, block.x(), block.y(), block.z(), block.tile().clone());
}
}
return payload;
}
private static IrisStaticObjectLayer merge(List<Placement> placements) {
if (placements.isEmpty()) {
return EMPTY;
}
Map<Long, Map<Integer, Block>> combined = new HashMap<>();
for (Placement placement : placements) {
for (Map.Entry<Long, Chunk> chunk : placement.chunks().entrySet()) {
combined.computeIfAbsent(chunk.getKey(), key -> new HashMap<>()).putAll(chunk.getValue().blocks());
}
}
return new IrisStaticObjectLayer(freezeChunks(combined), List.copyOf(placements));
}
private static Map<Long, Chunk> freezeChunks(Map<Long, Map<Integer, Block>> chunks) {
Map<Long, Chunk> compiled = new HashMap<>(chunks.size());
for (Map.Entry<Long, Map<Integer, Block>> chunk : chunks.entrySet()) {
Map<Integer, Block> blocks = Map.copyOf(chunk.getValue());
compiled.put(chunk.getKey(), new Chunk(blocks, List.copyOf(blocks.values())));
}
return Map.copyOf(compiled);
}
private static long chunkKey(int x, int z) { private static long chunkKey(int x, int z) {
return ((long) x << 32) | (z & 0xffffffffL); return ((long) x << 32) | (z & 0xffffffffL);
} }
@@ -137,21 +244,30 @@ public final class IrisStaticObjectLayer {
private record Chunk(Map<Integer, Block> blocks, List<Block> ordered) { private record Chunk(Map<Integer, Block> blocks, List<Block> ordered) {
} }
private record Footprint(int minimumX, int minimumZ, int maximumX, int maximumZ) {
}
private record Placement(ObjectContinuationBundle.PlacementKey key, Map<Long, Chunk> chunks, Footprint footprint) {
}
private record TransitionLayer(IrisComplex complex, GenerationHistoryRuntimeRouter router, IrisStaticObjectLayer layer) {
}
private static final class Compilation implements IObjectPlacer { private static final class Compilation implements IObjectPlacer {
private final int height; private final int height;
private final Map<Long, Map<Integer, Block>> chunks = new HashMap<>(); private final Map<Long, Map<Integer, Block>> chunks = new HashMap<>();
private int minimumX = Integer.MAX_VALUE;
private int minimumZ = Integer.MAX_VALUE;
private int maximumX = Integer.MIN_VALUE;
private int maximumZ = Integer.MIN_VALUE;
private Compilation(int height) { private Compilation(int height) {
this.height = height; this.height = height;
} }
private IrisStaticObjectLayer freeze() { private Placement freeze(ObjectContinuationBundle.PlacementKey key) {
Map<Long, Chunk> compiled = new HashMap<>(chunks.size()); return new Placement(key, freezeChunks(chunks), chunks.isEmpty() ? null
for (Map.Entry<Long, Map<Integer, Block>> chunk : chunks.entrySet()) { : new Footprint(minimumX, minimumZ, maximumX, maximumZ));
Map<Integer, Block> blocks = Map.copyOf(chunk.getValue());
compiled.put(chunk.getKey(), new Chunk(blocks, List.copyOf(blocks.values())));
}
return new IrisStaticObjectLayer(Map.copyOf(compiled));
} }
@Override @Override
@@ -166,6 +282,10 @@ public final class IrisStaticObjectLayer {
if (state.isCustom() && (state.deferredPlacementKey() == null || state.placementBaseState() == null)) { if (state.isCustom() && (state.deferredPlacementKey() == null || state.placementBaseState() == null)) {
throw new IllegalArgumentException("Custom object block has no placement data: " + state.key()); throw new IllegalArgumentException("Custom object block has no placement data: " + state.key());
} }
minimumX = Math.min(minimumX, x);
minimumZ = Math.min(minimumZ, z);
maximumX = Math.max(maximumX, x);
maximumZ = Math.max(maximumZ, z);
Map<Integer, Block> chunk = chunks.computeIfAbsent(chunkKey(x >> 4, z >> 4), key -> new HashMap<>()); Map<Integer, Block> chunk = chunks.computeIfAbsent(chunkKey(x >> 4, z >> 4), key -> new HashMap<>());
chunk.put(blockKey(x, y, z), new Block(x & 15, y, z & 15, state, null)); chunk.put(blockKey(x, y, z), new Block(x & 15, y, z & 15, state, null));
} }
@@ -41,6 +41,7 @@ import art.arcane.iris.pack.value.IrisPosition;
import art.arcane.iris.generation.terrain.IrisRegion; import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.structure.nativegen.NativeStructureSuppression; import art.arcane.iris.structure.nativegen.NativeStructureSuppression;
import art.arcane.iris.structure.object.ObjectPlaceMode; import art.arcane.iris.structure.object.ObjectPlaceMode;
import art.arcane.iris.structure.object.IrisObjectVacuum;
import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.math.RNG;
import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Cache;
@@ -589,7 +590,7 @@ public final class IrisStructureLocator {
"assembled pieces exceed the configured structure or world bounds")) { "assembled pieces exceed the configured structure or world bounds")) {
return null; return null;
} }
if (!allowsResolvedFootprint(engine, placement, pieces)) { if (!allowsResolvedFootprint(engine, placement, structure, pieces)) {
return null; return null;
} }
return new ResolvedPlacement(placement, selectedKey, structure, pieces, rng, originX, baseY, originZ, exactY); return new ResolvedPlacement(placement, selectedKey, structure, pieces, rng, originX, baseY, originZ, exactY);
@@ -597,7 +598,7 @@ public final class IrisStructureLocator {
public static boolean allowsResolvedFootprint(Engine engine, ResolvedPlacement resolved) { public static boolean allowsResolvedFootprint(Engine engine, ResolvedPlacement resolved) {
Objects.requireNonNull(resolved, "resolved placement"); Objects.requireNonNull(resolved, "resolved placement");
return allowsResolvedFootprint(engine, resolved.placement(), resolved.pieces()); return allowsResolvedFootprint(engine, resolved.placement(), resolved.structure(), resolved.pieces());
} }
public static boolean requirePlacementOutput(IrisStructurePlacement placement, String structureKey, public static boolean requirePlacementOutput(IrisStructurePlacement placement, String structureKey,
@@ -909,6 +910,7 @@ public final class IrisStructureLocator {
private static boolean allowsResolvedFootprint( private static boolean allowsResolvedFootprint(
Engine engine, Engine engine,
IrisStructurePlacement placement, IrisStructurePlacement placement,
IrisStructure structure,
KList<PlacedStructurePiece> pieces KList<PlacedStructurePiece> pieces
) { ) {
int[] bounds = computeBounds(pieces); int[] bounds = computeBounds(pieces);
@@ -921,6 +923,10 @@ public final class IrisStructureLocator {
|| mode == StructureTerrainMode.BORE || mode == StructureTerrainMode.BORE
? Math.max(0, terrain.getHorizontalPadding()) ? Math.max(0, terrain.getHorizontalPadding())
: 0; : 0;
if (pieces.size() == 1 && !placement.isAnchoredUnderground()
&& IrisObjectVacuum.isVacuumMode(structure.getPlaceMode())) {
padding = Math.max(padding, IrisObjectVacuum.resolveRadius(structure.getPlaceMode(), null));
}
return engine.getComplex().allowsNewGenerationFootprint( return engine.getComplex().allowsNewGenerationFootprint(
saturatedOffset(bounds[0], -padding), saturatedOffset(bounds[0], -padding),
saturatedOffset(bounds[2], -padding), saturatedOffset(bounds[2], -padding),
@@ -58,23 +58,6 @@ public final class GenerationBlend {
return (int) Math.round(historicalHeight + ((double) newHeight - historicalHeight) * newEpochWeight); return (int) Math.round(historicalHeight + ((double) newHeight - historicalHeight) * newEpochWeight);
} }
public static boolean usesHistoricalMaterial(int blockX, int blockY, int blockZ, double newWeight) {
validateWeight(newWeight);
if (newWeight == 0D) {
return true;
}
if (newWeight == 1D) {
return false;
}
long hash = Math.floorDiv(blockX, 8) * 341873128712L
^ Math.floorDiv(blockY, 8) * 132897987541L
^ Math.floorDiv(blockZ, 8) * 42317861L;
hash = (hash ^ (hash >>> 30)) * 0xbf58476d1ce4e5b9L;
hash = (hash ^ (hash >>> 27)) * 0x94d049bb133111ebL;
hash ^= hash >>> 31;
return newWeight < (hash >>> 11) * 0x1.0p-53;
}
private static void validateWeight(double weight) { private static void validateWeight(double weight) {
if (!Double.isFinite(weight) || weight < 0D || weight > 1D) { if (!Double.isFinite(weight) || weight < 0D || weight > 1D) {
throw new IllegalArgumentException("Blend weight must be finite and between zero and one"); throw new IllegalArgumentException("Blend weight must be finite and between zero and one");
@@ -29,6 +29,7 @@ import java.util.concurrent.ArrayBlockingQueue;
public final class GenerationHistory { public final class GenerationHistory {
private static final int MAXIMUM_CACHED_BOUNDARIES = 4; private static final int MAXIMUM_CACHED_BOUNDARIES = 4;
private static final int MAXIMUM_CACHED_TERRAIN_SIGNATURES = 4; private static final int MAXIMUM_CACHED_TERRAIN_SIGNATURES = 4;
private static final int MAXIMUM_CACHED_TRANSITION_PLANS = 4;
private static final int MAXIMUM_SEMANTIC_BATCH = 32; private static final int MAXIMUM_SEMANTIC_BATCH = 32;
private static final int MAXIMUM_PENDING_SEMANTIC_CLAIMS = 128; private static final int MAXIMUM_PENDING_SEMANTIC_CLAIMS = 128;
@@ -44,6 +45,7 @@ public final class GenerationHistory {
private final GenerationAdmission admission; private final GenerationAdmission admission;
private final Map<Long, GenerationBoundary> boundaryCache; private final Map<Long, GenerationBoundary> boundaryCache;
private final Map<Long, TerrainBoundarySignatureStore.Snapshot> terrainSignatureCache; private final Map<Long, TerrainBoundarySignatureStore.Snapshot> terrainSignatureCache;
private final Map<Long, TransitionGenerationPlan> transitionPlanCache;
private final ArrayBlockingQueue<PendingSemanticClaim> pendingSemanticClaims = private final ArrayBlockingQueue<PendingSemanticClaim> pendingSemanticClaims =
new ArrayBlockingQueue<>(MAXIMUM_PENDING_SEMANTIC_CLAIMS); new ArrayBlockingQueue<>(MAXIMUM_PENDING_SEMANTIC_CLAIMS);
@@ -68,6 +70,7 @@ public final class GenerationHistory {
this.admission = new GenerationAdmission(paths.dimensionRoot()); this.admission = new GenerationAdmission(paths.dimensionRoot());
this.boundaryCache = boundedCache(MAXIMUM_CACHED_BOUNDARIES); this.boundaryCache = boundedCache(MAXIMUM_CACHED_BOUNDARIES);
this.terrainSignatureCache = boundedCache(MAXIMUM_CACHED_TERRAIN_SIGNATURES); this.terrainSignatureCache = boundedCache(MAXIMUM_CACHED_TERRAIN_SIGNATURES);
this.transitionPlanCache = boundedCache(MAXIMUM_CACHED_TRANSITION_PLANS);
validateReferencedState(publication, true); validateReferencedState(publication, true);
} }
@@ -872,6 +875,10 @@ public final class GenerationHistory {
} }
public synchronized TransitionGenerationPlan transitionPlan(long activationId) throws IOException { public synchronized TransitionGenerationPlan transitionPlan(long activationId) throws IOException {
TransitionGenerationPlan cached = transitionPlanCache.get(activationId);
if (cached != null) {
return cached;
}
GenerationActivation activation = requireActivation(activationId); GenerationActivation activation = requireActivation(activationId);
if (activation.isInitial()) { if (activation.isInitial()) {
throw new IllegalArgumentException("The initial generation activation has no transition plan."); throw new IllegalArgumentException("The initial generation activation has no transition plan.");
@@ -888,7 +895,7 @@ public final class GenerationHistory {
GenerationBoundary boundary = boundary(activationId); GenerationBoundary boundary = boundary(activationId);
TerrainBoundarySignatureStore.Snapshot terrainSnapshot = terrainSignatures(activationId); TerrainBoundarySignatureStore.Snapshot terrainSnapshot = terrainSignatures(activationId);
requireTransitionSnapshotIdentities(activation, boundary, terrainSnapshot); requireTransitionSnapshotIdentities(activation, boundary, terrainSnapshot);
return new TransitionGenerationPlan( TransitionGenerationPlan plan = new TransitionGenerationPlan(
new TransitionGenerationPlan.Specification( new TransitionGenerationPlan.Specification(
activation.activationId(), activation.activationId(),
requireEpoch(parent.epochId()).epochId(), requireEpoch(parent.epochId()).epochId(),
@@ -901,6 +908,8 @@ public final class GenerationHistory {
boundary, boundary,
terrainSnapshot terrainSnapshot
); );
transitionPlanCache.put(activationId, plan);
return plan;
} }
public synchronized int explicitChunkCount() { public synchronized int explicitChunkCount() {
@@ -10,14 +10,21 @@ import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.generation.runtime.IrisEngineMantle; import art.arcane.iris.generation.runtime.IrisEngineMantle;
import art.arcane.iris.generation.runtime.EngineTarget; import art.arcane.iris.generation.runtime.EngineTarget;
import art.arcane.iris.generation.runtime.GenerationTransitionGate; import art.arcane.iris.generation.runtime.GenerationTransitionGate;
import art.arcane.iris.generation.mantle.ObjectContinuationBundle;
import art.arcane.iris.world.storage.matter.IrisMatterContext;
import art.arcane.iris.generation.terrain.IrisDimension; import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.volmlib.util.mantle.MantleRegionFiles;
import art.arcane.volmlib.util.mantle.runtime.Mantle; import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.mantle.runtime.TectonicPlate;
import art.arcane.volmlib.util.matter.Matter; import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterSlice;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Iterator; import java.util.Iterator;
import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -32,6 +39,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
private final IrisEngine engine; private final IrisEngine engine;
private final GenerationHistory history; private final GenerationHistory history;
private final SavedBiomeRuntime biomes; private final SavedBiomeRuntime biomes;
private final HistoricalObjectContinuations objectContinuations;
private final GenerationBoundarySignatureSampler signatureSampler; private final GenerationBoundarySignatureSampler signatureSampler;
private final ActivationRuntimeFactory runtimeFactory; private final ActivationRuntimeFactory runtimeFactory;
private final LinkedHashMap<Long, RuntimeCacheEntry> bindings; private final LinkedHashMap<Long, RuntimeCacheEntry> bindings;
@@ -65,6 +73,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
this.operationDepth = ThreadLocal.withInitial(() -> 0); this.operationDepth = ThreadLocal.withInitial(() -> 0);
this.scopedRoute = new ThreadLocal<>(); this.scopedRoute = new ThreadLocal<>();
this.biomes = new SavedBiomeRuntime(engine, history); this.biomes = new SavedBiomeRuntime(engine, history);
this.objectContinuations = new HistoricalObjectContinuations(history, this::readObjectContinuationRegion);
GenerationActivation active = history.activeActivation(); GenerationActivation active = history.activeActivation();
GenerationEpoch epoch = requireEpoch(active); GenerationEpoch epoch = requireEpoch(active);
@@ -96,6 +105,41 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
return biomes; return biomes;
} }
public List<ObjectContinuation> objectContinuations(TransitionGenerationPlan plan, int chunkX, int chunkZ) {
return objectContinuations.at(plan, chunkX, chunkZ);
}
public List<Matter> decodeObjectContinuations(List<ObjectContinuation> continuations) throws IOException {
ArrayList<Matter> decoded = new ArrayList<>(continuations.size());
enterOperation();
try {
int index = 0;
while (index < continuations.size()) {
long activationId = continuations.get(index).activationId();
GenerationActivation activation = history.manifest().activation(activationId)
.orElseThrow(() -> new IOException("Missing object continuation activation " + activationId));
SavedMantleAccess access = acquireSavedMantle(activation, requireEpoch(activation));
try (IrisMatterContext.Scope scope = IrisMatterContext.open(access.data())) {
do {
decoded.add(continuations.get(index++).fragment().decode());
} while (index < continuations.size() && continuations.get(index).activationId() == activationId);
} finally {
access.release().run();
}
}
return List.copyOf(decoded);
} finally {
leaveOperation();
}
}
public record ObjectContinuation(long activationId, ObjectContinuationBundle.Fragment fragment) {
}
public boolean allowsObjectFootprint(TransitionGenerationPlan plan, ObjectContinuationBundle.Bounds bounds) {
return objectContinuations.allowsFootprint(plan, bounds.minimumX(), bounds.minimumZ(), bounds.maximumX(), bounds.maximumZ());
}
public static GenerationHistoryRuntimeRouter attach( public static GenerationHistoryRuntimeRouter attach(
IrisEngine engine, IrisEngine engine,
GenerationHistory history, GenerationHistory history,
@@ -461,6 +505,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
} catch (Throwable biomeFailure) { } catch (Throwable biomeFailure) {
failure = appendFailure(failure, biomeFailure); failure = appendFailure(failure, biomeFailure);
} }
objectContinuations.clear();
try { try {
retireBindings(retired); retireBindings(retired);
} catch (Throwable retirementFailure) { } catch (Throwable retirementFailure) {
@@ -493,6 +538,45 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
return signatureSampler.open(engine); return signatureSampler.open(engine);
} }
private Map<Long, ObjectContinuationBundle> readObjectContinuationRegion(
GenerationActivation activation, int regionX, int regionZ
) throws IOException {
Path root = history.paths().activationMantleRoot(activation.activationId());
if (!MantleRegionFiles.fileForRegion(root.toFile(), Mantle.key(regionX, regionZ), false).isFile()) {
return Map.of();
}
enterOperation();
try {
SavedMantleAccess access = acquireSavedMantle(activation, requireEpoch(activation));
try {
Mantle<Matter> mantle = access.mantle();
MantleChunk<Matter> anchor = mantle.getChunk(regionX << 5, regionZ << 5).use();
try {
TectonicPlate<Matter> plate = mantle.getLoadedRegions().get(Mantle.key(regionX, regionZ));
HashMap<Long, ObjectContinuationBundle> bundles = new HashMap<>();
for (int x = 0; x < 32; x++) {
for (int z = 0; z < 32; z++) {
MantleChunk<Matter> chunk = plate.get(x, z);
Matter section = chunk == null ? null : chunk.get(0);
MatterSlice<ObjectContinuationBundle> slice = section == null ? null : section.getSlice(ObjectContinuationBundle.class);
ObjectContinuationBundle bundle = slice == null ? null : slice.get(0, 0, 0);
if (bundle != null && !bundle.fragments().isEmpty()) {
bundles.put(ChunkGenerationOwnership.packChunk((regionX << 5) + x, (regionZ << 5) + z), bundle);
}
}
}
return Map.copyOf(bundles);
} finally {
anchor.release();
}
} finally {
access.release().run();
}
} finally {
leaveOperation();
}
}
private SavedMantleAccess acquireSavedMantle(GenerationActivation activation, GenerationEpoch epoch) throws IOException { private SavedMantleAccess acquireSavedMantle(GenerationActivation activation, GenerationEpoch epoch) throws IOException {
SavedMantleEntry saved = null; SavedMantleEntry saved = null;
RuntimeCacheEntry runtime; RuntimeCacheEntry runtime;
@@ -526,7 +610,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
if (runtime != null) { if (runtime != null) {
RuntimeLease lease = awaitRuntime(runtime, activation.activationId()); RuntimeLease lease = awaitRuntime(runtime, activation.activationId());
try (IrisEngine.GenerationRuntimeScope ignored = engine.openGenerationRuntimeScope(lease.binding())) { try (IrisEngine.GenerationRuntimeScope ignored = engine.openGenerationRuntimeScope(lease.binding())) {
return new SavedMantleAccess(engine.getMantle().getMantle(), lease::close); return new SavedMantleAccess(engine.getMantle().getMantle(), engine.getData(), lease::close);
} catch (Throwable failure) { } catch (Throwable failure) {
lease.close(); lease.close();
throw failure; throw failure;
@@ -552,7 +636,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
entry.leases--; entry.leases--;
throw propagate(entry.failure, "Unable to load saved mantle for activation " + entry.activationId + "."); throw propagate(entry.failure, "Unable to load saved mantle for activation " + entry.activationId + ".");
} }
return new SavedMantleAccess(entry.mantle, () -> releaseSavedMantle(entry)); return new SavedMantleAccess(entry.mantle, entry.data, () -> releaseSavedMantle(entry));
} finally { } finally {
stateLock.unlock(); stateLock.unlock();
} }
@@ -1064,7 +1148,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
} }
} }
private record SavedMantleAccess(Mantle<Matter> mantle, Runnable release) { private record SavedMantleAccess(Mantle<Matter> mantle, IrisData data, Runnable release) {
} }
private record SavedChunkResources(GenerationHistory.GenerationStage stage, private record SavedChunkResources(GenerationHistory.GenerationStage stage,
@@ -112,7 +112,7 @@ final class GenerationKernelV1 implements GenerationKernelRegistry.RuntimeFactor
public void registerStaticObjects(IrisEngine engine, EngineMode mode) { public void registerStaticObjects(IrisEngine engine, EngineMode mode) {
IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData()); IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData());
mode.registerStage((x, z, blocks, biomes, multicore, context) -> mode.registerStage((x, z, blocks, biomes, multicore, context) ->
staticObjects.apply(engine, x, z, blocks)); staticObjects.forTransition(engine).apply(engine, x, z, blocks));
} }
@Override @Override
@@ -208,8 +208,8 @@ public final class GenerationSemanticCapture {
} }
private static boolean hasFullHydrologyWeight(IrisComplex complex, int blockX, int blockZ) { private static boolean hasFullHydrologyWeight(IrisComplex complex, int blockX, int blockZ) {
TransitionGenerationPlan transition = complex.getTransitionGenerationPlan(); TransitionDisplacementField displacement = complex.getTransitionDisplacement();
return transition == null || transition.hydrologyWeightAt(blockX, blockZ) == 1D; return displacement == null || displacement.seamWeight(blockX, blockZ) == 1D;
} }
private static void captureCave( private static void captureCave(
@@ -205,8 +205,8 @@ public final class GenerationSemanticQueries {
} }
private static boolean allowsActiveHydrologyPrediction(Engine engine, int blockX, int blockZ) { private static boolean allowsActiveHydrologyPrediction(Engine engine, int blockX, int blockZ) {
TransitionGenerationPlan transition = engine.getComplex().getTransitionGenerationPlan(); TransitionDisplacementField displacement = engine.getComplex().getTransitionDisplacement();
if (transition != null && transition.hydrologyWeightAt(blockX, blockZ) != 1D) { if (displacement != null && displacement.seamWeight(blockX, blockZ) != 1D) {
return false; return false;
} }
return allowsActivePrediction( return allowsActivePrediction(
@@ -0,0 +1,120 @@
package art.arcane.iris.world.history;
import art.arcane.iris.generation.mantle.ObjectContinuationBundle;
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter.ObjectContinuation;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
final class HistoricalObjectContinuations {
private static final Comparator<ObjectContinuation> PLACEMENT_ORDER = Comparator
.comparingLong(ObjectContinuation::activationId)
.thenComparing(continuation -> continuation.fragment().key().kind())
.thenComparingInt(continuation -> continuation.fragment().key().sourceChunkX())
.thenComparingInt(continuation -> continuation.fragment().key().sourceChunkZ())
.thenComparingInt(continuation -> continuation.fragment().key().ordinal());
private static final long MAXIMUM_BYTES = 32L * 1024L * 1024L;
private final GenerationHistory history;
private final RegionReader reader;
private final Cache<RegionKey, Region> regions = Caffeine.newBuilder()
.maximumWeight(MAXIMUM_BYTES)
.weigher((RegionKey key, Region region) -> region.weight())
.build();
HistoricalObjectContinuations(GenerationHistory history, RegionReader reader) {
this.history = history;
this.reader = reader;
}
List<ObjectContinuation> at(TransitionGenerationPlan plan, int chunkX, int chunkZ) {
if (plan == null || plan.boundary().isHistoricalChunk(chunkX, chunkZ)) {
return List.of();
}
RegionKey key = new RegionKey(plan.activationId(), chunkX >> 5, chunkZ >> 5);
Region region = regions.get(key, ignored -> load(plan, key));
return region.chunks().getOrDefault(ChunkGenerationOwnership.packChunk(chunkX, chunkZ), List.of());
}
boolean allowsFootprint(TransitionGenerationPlan plan, int minimumX, int minimumZ, int maximumX, int maximumZ) {
for (int chunkX = minimumX >> 4; chunkX <= maximumX >> 4; chunkX++) {
for (int chunkZ = minimumZ >> 4; chunkZ <= maximumZ >> 4; chunkZ++) {
for (ObjectContinuation continuation : at(plan, chunkX, chunkZ)) {
ObjectContinuationBundle.Bounds bounds = continuation.fragment().bounds();
if (bounds.minimumX() <= maximumX && bounds.maximumX() >= minimumX
&& bounds.minimumZ() <= maximumZ && bounds.maximumZ() >= minimumZ) {
return false;
}
}
}
}
return true;
}
void clear() {
regions.invalidateAll();
}
private Region load(TransitionGenerationPlan plan, RegionKey key) {
HashMap<Long, List<ObjectContinuation>> chunks = new HashMap<>();
long weight = 256L;
try {
for (GenerationActivation activation : history.manifest().activations()) {
if (activation.activationId() >= key.activationId()) {
continue;
}
Map<Long, ObjectContinuationBundle> stored = reader.read(activation, key.x(), key.z());
for (Map.Entry<Long, ObjectContinuationBundle> entry : stored.entrySet()) {
int chunkX = ChunkGenerationOwnership.chunkX(entry.getKey());
int chunkZ = ChunkGenerationOwnership.chunkZ(entry.getKey());
if (plan.boundary().isHistoricalChunk(chunkX, chunkZ)) {
continue;
}
for (ObjectContinuationBundle.Fragment fragment : entry.getValue().fragments()) {
if (!intersectsSavedPlacement(plan, activation.activationId(), fragment)) {
continue;
}
chunks.computeIfAbsent(entry.getKey(), ignored -> new ArrayList<>()).add(new ObjectContinuation(activation.activationId(), fragment));
weight += 128L + fragment.encodedSize() + fragment.touchedChunks().size() * 16L;
}
}
}
} catch (IOException failure) {
throw new UncheckedIOException("Unable to read pending objects for transition " + key.activationId(), failure);
}
chunks.replaceAll((coordinate, fragments) -> {
fragments.sort(PLACEMENT_ORDER);
return List.copyOf(fragments);
});
return new Region(Map.copyOf(chunks), (int) Math.min(Integer.MAX_VALUE, weight));
}
private boolean intersectsSavedPlacement(TransitionGenerationPlan plan, long activationId,
ObjectContinuationBundle.Fragment fragment) {
for (ObjectContinuationBundle.ChunkPosition chunk : fragment.touchedChunks()) {
if (plan.boundary().isHistoricalChunk(chunk.x(), chunk.z())
&& history.resolveActivation(chunk.x(), chunk.z()).activationId() == activationId) {
return true;
}
}
return false;
}
@FunctionalInterface
interface RegionReader {
Map<Long, ObjectContinuationBundle> read(GenerationActivation activation, int regionX, int regionZ) throws IOException;
}
private record RegionKey(long activationId, int x, int z) {
}
private record Region(Map<Long, List<ObjectContinuation>> chunks, int weight) {
}
}
@@ -1,5 +1,7 @@
package art.arcane.iris.world.history; package art.arcane.iris.world.history;
import art.arcane.iris.world.storage.Durability;
import java.io.BufferedInputStream; import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
@@ -225,14 +227,14 @@ public final class SavedBiomeStore {
} }
} }
if (!unpublished) { if (!unpublished) {
output.getChannel().force(true); Durability.force(output.getChannel());
} }
appendCompleted = true; appendCompleted = true;
} catch (IOException failure) { } catch (IOException failure) {
try { try {
output.setLength(offset); output.setLength(offset);
if (!unpublished) { if (!unpublished) {
output.getChannel().force(true); Durability.force(output.getChannel());
} }
} catch (IOException rollback) { } catch (IOException rollback) {
failure.addSuppressed(rollback); failure.addSuppressed(rollback);
@@ -467,7 +469,7 @@ public final class SavedBiomeStore {
while (header.hasRemaining()) { while (header.hasRemaining()) {
output.write(header); output.write(header);
} }
output.force(true); Durability.force(output);
} }
Files.move(temporary, region.path, StandardCopyOption.ATOMIC_MOVE); Files.move(temporary, region.path, StandardCopyOption.ATOMIC_MOVE);
forceDirectory(directory); forceDirectory(directory);
@@ -477,11 +479,11 @@ public final class SavedBiomeStore {
} }
private static void forceDirectory(Path path) throws IOException { private static void forceDirectory(Path path) throws IOException {
if (File.separatorChar == '\\') { if (File.separatorChar == '\\' || !Durability.enabled()) {
return; return;
} }
try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) { try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
channel.force(true); Durability.force(channel);
} catch (UnsupportedOperationException failure) { } catch (UnsupportedOperationException failure) {
throw new IOException("Saved biome directory cannot be durability-synced: " + path, failure); throw new IOException("Saved biome directory cannot be durability-synced: " + path, failure);
} }
@@ -498,7 +500,7 @@ public final class SavedBiomeStore {
transfer(input, output, region.offsets[slot], region.lengths[slot] + 8L); transfer(input, output, region.offsets[slot], region.lengths[slot] + 8L);
} }
} }
output.force(true); Durability.force(output);
} }
Files.move(temporary, region.path, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); Files.move(temporary, region.path, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
synchronized (regions) { synchronized (regions) {
@@ -591,7 +593,7 @@ public final class SavedBiomeStore {
private static void truncateTail(RandomAccessFile file, long length) throws IOException { private static void truncateTail(RandomAccessFile file, long length) throws IOException {
file.setLength(length); file.setLength(length);
file.getChannel().force(true); Durability.force(file.getChannel());
} }
private static int checksum(byte[] bytes) { private static int checksum(byte[] bytes) {
@@ -48,6 +48,7 @@ import java.util.PriorityQueue;
import java.util.TreeSet; import java.util.TreeSet;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionException;
import java.util.function.Consumer;
import java.util.concurrent.atomic.LongAdder; import java.util.concurrent.atomic.LongAdder;
import java.util.zip.CRC32; import java.util.zip.CRC32;
@@ -243,11 +244,6 @@ public final class TerrainBoundarySignatureStore {
long[] cellKeys = new long[cellCount]; long[] cellKeys = new long[cellCount];
Long2ObjectOpenHashMap<CellReference> cells = new Long2ObjectOpenHashMap<>(cellCount); Long2ObjectOpenHashMap<CellReference> cells = new Long2ObjectOpenHashMap<>(cellCount);
LinkedHashMap<String, List<TerrainBoundarySignature>> validated = new LinkedHashMap<>(
MAXIMUM_CACHED_SHARDS,
0.75F,
true
);
int countedEntries = 0; int countedEntries = 0;
long previousCellKey = 0L; long previousCellKey = 0L;
for (int index = 0; index < cellCount; index++) { for (int index = 0; index < cellCount; index++) {
@@ -266,19 +262,6 @@ public final class TerrainBoundarySignatureStore {
throw invalid(snapshot, "truncated shard hash"); throw invalid(snapshot, "truncated shard hash");
} }
String hash = HexFormat.of().formatHex(hashBytes); String hash = HexFormat.of().formatHex(hashBytes);
List<TerrainBoundarySignature> signatures = validated.get(hash);
if (signatures == null) {
Path shard = directory.resolve(shardFileName(hash));
requireRegularFile(shard, "Terrain boundary shard");
signatures = CellShard.read(shard, hash, cellX, cellZ);
validated.put(hash, signatures);
trim(validated);
} else {
signatures = relocate(signatures, cellX, cellZ);
}
if (signatures.size() != count) {
throw invalid(snapshot, "terrain signature shard count does not match its catalog entry");
}
countedEntries = Math.addExact(countedEntries, count); countedEntries = Math.addExact(countedEntries, count);
cellKeys[index] = cellKey; cellKeys[index] = cellKey;
cells.put(cellKey, new CellReference(hash, count)); cells.put(cellKey, new CellReference(hash, count));
@@ -603,33 +586,6 @@ public final class TerrainBoundarySignatureStore {
} }
} }
private static List<TerrainBoundarySignature> relocate(
List<TerrainBoundarySignature> source,
int cellX,
int cellZ
) {
int minimumX = Math.multiplyExact(cellX, CELL_SIZE);
int minimumZ = Math.multiplyExact(cellZ, CELL_SIZE);
ArrayList<TerrainBoundarySignature> relocated = new ArrayList<>(source.size());
for (TerrainBoundarySignature signature : source) {
int localX = Math.floorMod(signature.blockX(), CELL_SIZE);
int localZ = Math.floorMod(signature.blockZ(), CELL_SIZE);
relocated.add(new TerrainBoundarySignature(
new TerrainBoundarySignature.Column(
minimumX + localX,
minimumZ + localZ,
signature.surfaceHeight(),
signature.oceanFloorHeight(),
signature.fluidHeight(),
signature.upperCeilingDepth()
),
signature.samples(),
signature.geometry()
));
}
return List.copyOf(relocated);
}
private static void trim(LinkedHashMap<?, ?> cache) { private static void trim(LinkedHashMap<?, ?> cache) {
while (cache.size() > MAXIMUM_CACHED_SHARDS) { while (cache.size() > MAXIMUM_CACHED_SHARDS) {
Iterator<?> entries = cache.entrySet().iterator(); Iterator<?> entries = cache.entrySet().iterator();
@@ -776,6 +732,14 @@ public final class TerrainBoundarySignatureStore {
} }
} }
void forEachSignature(BlockBounds bounds, Consumer<TerrainBoundarySignature> consumer) {
try {
source.forEachSignature(bounds, consumer);
} catch (IOException error) {
throw new IllegalStateException("Unable to query terrain boundary signatures", error);
}
}
boolean intersectsTerrainBand(BlockBounds footprint, int width) { boolean intersectsTerrainBand(BlockBounds footprint, int width) {
try { try {
return source.intersectsTerrainBand(footprint, width); return source.intersectsTerrainBand(footprint, width);
@@ -1045,6 +1009,25 @@ public final class TerrainBoundarySignatureStore {
}); });
} }
private void forEachSignature(BlockBounds bounds, Consumer<TerrainBoundarySignature> consumer) throws IOException {
visitSuperCells(bounds, superKey -> {
for (long cellKey : cellsBySuperCell.get(superKey)) {
BlockBounds cell = BlockBounds.cell(cellKey, CELL_SIZE);
if (cell.maximumX() < bounds.minimumX() || cell.minimumX() > bounds.maximumX()
|| cell.maximumZ() < bounds.minimumZ() || cell.minimumZ() > bounds.maximumZ()) {
continue;
}
for (TerrainBoundarySignature signature : load(cellKey, cells.get(cellKey))) {
if (signature.blockX() >= bounds.minimumX() && signature.blockX() <= bounds.maximumX()
&& signature.blockZ() >= bounds.minimumZ() && signature.blockZ() <= bounds.maximumZ()) {
consumer.accept(signature);
}
}
}
return false;
});
}
private boolean visitSuperCells(BlockBounds bounds, SuperCellVisitor visitor) throws IOException { private boolean visitSuperCells(BlockBounds bounds, SuperCellVisitor visitor) throws IOException {
int size = CELL_SIZE * SUPER_CELL_SIZE; int size = CELL_SIZE * SUPER_CELL_SIZE;
int minimumX = saturatedFloorDiv(bounds.minimumX(), size); int minimumX = saturatedFloorDiv(bounds.minimumX(), size);
@@ -0,0 +1,93 @@
package art.arcane.iris.world.history;
import java.util.concurrent.atomic.AtomicLongArray;
import java.util.concurrent.atomic.AtomicReferenceArray;
final class TransitionBandIndex {
private static final int MAXIMUM_CACHED_REGIONS = 1_024;
private final GenerationBoundary boundary;
private final TransitionBoundarySampler sampler;
private final AtomicReferenceArray<Region> regions = new AtomicReferenceArray<>(MAXIMUM_CACHED_REGIONS);
private final ThreadLocal<Region> lastRegion = new ThreadLocal<>();
TransitionBandIndex(GenerationBoundary boundary, TransitionBoundarySampler sampler) {
this.boundary = boundary;
this.sampler = sampler;
}
boolean contains(int chunkX, int chunkZ) {
long key = GenerationBoundary.packChunk(chunkX >> 5, chunkZ >> 5);
Region region = lastRegion.get();
if (region == null || region.key != key) {
region = region(key);
lastRegion.set(region);
}
int index = ((chunkZ & 31) << 5) | (chunkX & 31);
int word = index >>> 5;
int shift = (index & 31) << 1;
long state = (region.states.get(word) >>> shift) & 3L;
if (state != 0L) {
return state == 3L;
}
synchronized (region) {
long states = region.states.get(word);
state = (states >>> shift) & 3L;
if (state == 0L) {
state = intersects(chunkX, chunkZ) ? 3L : 1L;
region.states.set(word, states | (state << shift));
}
return state == 3L;
}
}
int cachedRegionCount() {
int count = 0;
for (int index = 0; index < regions.length(); index++) {
if (regions.get(index) != null) {
count++;
}
}
return count;
}
private Region region(long key) {
long mixed = key;
mixed = (mixed ^ (mixed >>> 33)) * 0xff51afd7ed558ccdL;
mixed = (mixed ^ (mixed >>> 33)) * 0xc4ceb9fe1a85ec53L;
int slot = (int) (mixed ^ (mixed >>> 33)) & (MAXIMUM_CACHED_REGIONS - 1);
Region replacement = null;
while (true) {
Region current = regions.get(slot);
if (current != null && current.key == key) {
return current;
}
if (replacement == null) {
replacement = new Region(key);
}
if (regions.compareAndSet(slot, current, replacement)) {
return replacement;
}
}
}
private boolean intersects(int chunkX, int chunkZ) {
if (boundary.isHistoricalChunk(chunkX, chunkZ)) {
return false;
}
int minimumX = Math.multiplyExact(chunkX, GenerationBoundary.CHUNK_SIZE);
int minimumZ = Math.multiplyExact(chunkZ, GenerationBoundary.CHUNK_SIZE);
return sampler.intersectsTerrainBand(minimumX, minimumZ,
Math.addExact(minimumX, GenerationBoundary.CHUNK_SIZE - 1),
Math.addExact(minimumZ, GenerationBoundary.CHUNK_SIZE - 1));
}
private static final class Region {
private final long key;
private final AtomicLongArray states = new AtomicLongArray(32);
private Region(long key) {
this.key = key;
}
}
}
@@ -59,13 +59,14 @@ final class TransitionBoundarySampler {
BoundaryGeometryInfluence geometryAt(int blockX, int blockZ) { BoundaryGeometryInfluence geometryAt(int blockX, int blockZ) {
CandidateIndex candidates = chunkCache(blockX, blockZ).candidates; CandidateIndex candidates = chunkCache(blockX, blockZ).candidates;
Nearest nearest = candidates.nearest(blockX, blockZ, square(terrainWidth)); int seamWidth = Math.min(4, terrainWidth);
if (nearest.count == 0 || nearest.distancesSquared[0] >= square(terrainWidth)) { Nearest nearest = candidates.nearest(blockX, blockZ, square(seamWidth));
if (nearest.count == 0 || nearest.distancesSquared[0] >= square(seamWidth)) {
return BoundaryGeometryInfluence.none(); return BoundaryGeometryInfluence.none();
} }
double distance = Math.sqrt(nearest.distancesSquared[0]); double distance = Math.sqrt(nearest.distancesSquared[0]);
double weight = GenerationBlend.newEpochWeight(Math.max(0D, distance - 1D), double weight = GenerationBlend.newEpochWeight(Math.max(0D, distance - 1D),
Math.max(1, terrainWidth - 1)); Math.max(1, seamWidth - 1));
int count = nearest.distancesSquared[0] <= 1D ? 1 : nearest.count; int count = nearest.distancesSquared[0] <= 1D ? 1 : nearest.count;
double total = 0D; double total = 0D;
for (int index = 0; index < count; index++) { for (int index = 0; index < count; index++) {
@@ -77,10 +78,7 @@ final class TransitionBoundarySampler {
nearest.signatures[index].geometry(), nearest.signatures[index].geometry(),
(1D / Math.max(1D, nearest.distancesSquared[index])) / total)); (1D / Math.max(1D, nearest.distancesSquared[index])) / total));
} }
int openingDepth = Math.min(8, Math.max(1, terrainWidth / 4)); return new BoundaryGeometryInfluence(weight, weight, contributions);
double openingWeight = GenerationBlend.newEpochWeight(Math.max(0D, distance - openingDepth),
Math.max(1, terrainWidth - openingDepth));
return new BoundaryGeometryInfluence(weight, openingWeight, contributions);
} }
boolean intersectsTerrainBand(int minimumX, int minimumZ, int maximumX, int maximumZ) { boolean intersectsTerrainBand(int minimumX, int minimumZ, int maximumX, int maximumZ) {
@@ -0,0 +1,252 @@
package art.arcane.iris.world.history;
import java.util.LinkedHashMap;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
public final class TransitionDisplacementField {
private static final int MAXIMUM_CACHED_CHUNKS = 256;
private static final int LOCAL_SUPPORT = 8;
private static final Sample NONE = new Sample(0D, Double.MAX_VALUE, 1D, 0D, 0D, 0D, null);
private final TransitionGenerationPlan plan;
private final Sources sources;
private final LinkedHashMap<Long, ChunkField> completed = new LinkedHashMap<>(64, 0.75F, true);
private final ConcurrentHashMap<Long, CompletableFuture<ChunkField>> pending = new ConcurrentHashMap<>();
private final ThreadLocal<ChunkField> local = new ThreadLocal<>();
public TransitionDisplacementField(TransitionGenerationPlan plan, Sources sources) {
this.plan = Objects.requireNonNull(plan);
this.sources = Objects.requireNonNull(sources);
}
public double height(int x, int z, double nativeHeight) {
double displacement = sample(x, z).displacement();
return displacement == 0D ? nativeHeight : Math.clamp(nativeHeight + displacement, 0D, sources.height() - 1D);
}
public double fluidHeight(int x, int z, double nativeHead) {
Sample sample = sample(x, z);
if (sample.newTerrainWeight() == 1D) {
return nativeHead;
}
double historicalHead = sample.wetWeight() * sample.wetHeight()
+ (1D - sample.wetWeight()) * Math.min(sample.dryHeight(), nativeHead);
return GenerationBlend.interpolate(historicalHead, nativeHead, sample.newTerrainWeight());
}
public String fluidStateKey(int x, int z) {
Sample sample = sample(x, z);
return sample.newTerrainWeight() < 0.5D ? sample.fluidStateKey() : null;
}
public double seamWeight(int x, int z) {
Sample sample = sample(x, z);
return sample.newTerrainWeight() == 1D ? 1D
: GenerationBlend.newEpochWeight(Math.max(0D, sample.distance() - 1D), 16);
}
public Sample sample(int x, int z) {
int chunkX = x >> 4;
int chunkZ = z >> 4;
if (!plan.hasTransitionAtChunk(chunkX, chunkZ)) {
return NONE;
}
long key = ChunkGenerationOwnership.packChunk(chunkX, chunkZ);
ChunkField field = local.get();
if (field == null || field.key() != key) {
field = chunk(chunkX, chunkZ, key);
local.set(field);
}
return field.samples()[(x & 15) * 16 + (z & 15)];
}
public void clear() {
synchronized (completed) {
completed.clear();
}
local.remove();
}
private ChunkField chunk(int chunkX, int chunkZ, long key) {
synchronized (completed) {
ChunkField cached = completed.get(key);
if (cached != null) {
return cached;
}
}
CompletableFuture<ChunkField> result = new CompletableFuture<>();
CompletableFuture<ChunkField> existing = pending.putIfAbsent(key, result);
if (existing != null) {
return existing.join();
}
try {
synchronized (completed) {
ChunkField cached = completed.get(key);
if (cached != null) {
result.complete(cached);
return cached;
}
}
ChunkField field = build(chunkX, chunkZ, key);
synchronized (completed) {
completed.put(key, field);
while (completed.size() > MAXIMUM_CACHED_CHUNKS) {
completed.pollFirstEntry();
}
}
result.complete(field);
return field;
} catch (RuntimeException | Error failure) {
result.completeExceptionally(failure);
throw failure;
} finally {
pending.remove(key, result);
}
}
private ChunkField build(int chunkX, int chunkZ, long key) {
int minimumX = Math.multiplyExact(chunkX, 16);
int minimumZ = Math.multiplyExact(chunkZ, 16);
Accumulator[] columns = new Accumulator[256];
double maximumSupport = 0D;
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
TransitionGenerationPlan.TerrainSample terrain = plan.terrainSampleAt(minimumX + x, minimumZ + z);
double distance = terrain.distanceToHistoricalTerrain();
double support = Math.min(plan.widthBlocks(), distance + LOCAL_SUPPORT);
double weight = terrain.hasHistoricalSignature()
? GenerationBlend.newEpochWeight(Math.max(0D, distance - 1D), Math.max(1, plan.widthBlocks() - 1))
: 1D;
columns[x * 16 + z] = new Accumulator(minimumX + x, minimumZ + z, distance, support, weight);
if (weight < 1D) {
maximumSupport = Math.max(maximumSupport, support);
}
}
}
long radius = (long) Math.ceil(maximumSupport);
plan.terrainSignatures().forEachSignature(new TerrainBoundarySignatureStore.BlockBounds(
minimumX - radius, minimumZ - radius, minimumX + 15L + radius, minimumZ + 15L + radius),
signature -> accumulate(columns, signature));
Sample[] samples = new Sample[256];
for (int index = 0; index < samples.length; index++) {
samples[index] = columns[index].finish();
}
return new ChunkField(key, samples);
}
private void accumulate(Accumulator[] columns, TerrainBoundarySignature signature) {
double residual = Double.NaN;
String fluidKey = null;
boolean wet = signature.fluidHeight().isPresent();
for (Accumulator column : columns) {
double weight = column.weight(signature.blockX(), signature.blockZ());
if (weight == 0D) {
continue;
}
if (Double.isNaN(residual)) {
double nativeHeight = sources.heights().height(signature.blockX(), signature.blockZ());
if (!Double.isFinite(nativeHeight)) {
throw new IllegalStateException("Transition anchor has a nonfinite native height");
}
residual = signature.oceanFloorHeight() - nativeHeight;
if (wet) {
int y = signature.geometry().minimumY() + signature.fluidHeight().getAsInt();
fluidKey = signature.geometry().voxelAt(y).fluidStateKey();
}
}
column.total += weight;
column.displacement += weight * residual;
if (wet) {
column.wetTotal += weight;
column.wetHeight += weight * signature.fluidHeight().getAsInt();
column.minimumWetHeight = Math.min(column.minimumWetHeight, signature.fluidHeight().getAsInt());
column.maximumWetHeight = Math.max(column.maximumWetHeight, signature.fluidHeight().getAsInt());
if (weight > column.fluidKeyWeight) {
column.fluidKeyWeight = weight;
column.fluidKey = fluidKey;
}
} else {
column.dryHeight += weight * signature.oceanFloorHeight();
column.minimumDryHeight = Math.min(column.minimumDryHeight, signature.oceanFloorHeight());
column.maximumDryHeight = Math.max(column.maximumDryHeight, signature.oceanFloorHeight());
}
}
}
public record Sources(HeightSource heights, int height) {
public Sources {
Objects.requireNonNull(heights);
if (height < 1) {
throw new IllegalArgumentException("Transition world height must be positive");
}
}
}
@FunctionalInterface
public interface HeightSource {
double height(int x, int z);
}
public record Sample(double displacement, double distance, double newTerrainWeight, double wetWeight,
double wetHeight, double dryHeight, String fluidStateKey) {
}
private record ChunkField(long key, Sample[] samples) {
}
private static final class Accumulator {
private final int x;
private final int z;
private final double distance;
private final double support;
private final double newWeight;
private double total;
private double displacement;
private double wetTotal;
private double wetHeight;
private double dryHeight;
private double minimumWetHeight = Double.POSITIVE_INFINITY;
private double maximumWetHeight = Double.NEGATIVE_INFINITY;
private double minimumDryHeight = Double.POSITIVE_INFINITY;
private double maximumDryHeight = Double.NEGATIVE_INFINITY;
private double fluidKeyWeight;
private String fluidKey;
private Accumulator(int x, int z, double distance, double support, double newWeight) {
this.x = x;
this.z = z;
this.distance = distance;
this.support = support;
this.newWeight = newWeight;
}
private double weight(int anchorX, int anchorZ) {
if (newWeight == 1D) {
return 0D;
}
double dx = (double) anchorX - x;
double dz = (double) anchorZ - z;
double squared = dx * dx + dz * dz;
if (squared >= support * support) {
return 0D;
}
double q = Math.sqrt(squared) / support;
double remainder = 1D - q;
return remainder * remainder * remainder * remainder * (1D + 4D * q)
/ Math.max(1D, squared * squared);
}
private Sample finish() {
if (total == 0D) {
return NONE;
}
double dryTotal = total - wetTotal;
return new Sample((1D - newWeight) * displacement / total, distance, newWeight,
Math.clamp(wetTotal / total, 0D, 1D), wetTotal == 0D ? 0D
: Math.clamp(wetHeight / wetTotal, minimumWetHeight, maximumWetHeight),
dryTotal <= 0D ? 0D : Math.clamp(dryHeight / dryTotal, minimumDryHeight, maximumDryHeight), fluidKey);
}
}
}
@@ -1,149 +0,0 @@
package art.arcane.iris.world.history;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.OptionalInt;
import java.util.Set;
public final class TransitionFluidContainment {
private static final Set<String> BANK_MATERIALS = Set.of(
"minecraft:stone", "minecraft:deepslate", "minecraft:granite", "minecraft:diorite",
"minecraft:andesite", "minecraft:tuff", "minecraft:calcite", "minecraft:bedrock",
"minecraft:dirt", "minecraft:coarse_dirt", "minecraft:grass_block", "minecraft:podzol",
"minecraft:mycelium", "minecraft:clay", "minecraft:terracotta", "minecraft:netherrack",
"minecraft:basalt", "minecraft:smooth_basalt", "minecraft:blackstone", "minecraft:end_stone",
"minecraft:obsidian", "minecraft:crying_obsidian");
private static final BoundaryColumnGeometry.Voxel STONE = new BoundaryColumnGeometry.Voxel(
"minecraft:stone", BoundaryColumnGeometry.Phase.SOLID, "", false);
private TransitionFluidContainment() {
}
public static SavedTerrainChunk contain(SavedTerrainChunk raw, TransitionGenerationPlan plan, ColumnSource source)
throws IOException {
List<BoundaryColumnGeometry> columns = new ArrayList<>(256);
boolean changed = false;
for (int localX = 0; localX < 16; localX++) {
for (int localZ = 0; localZ < 16; localZ++) {
int x = Math.addExact(Math.multiplyExact(raw.chunkX(), 16), localX);
int z = Math.addExact(Math.multiplyExact(raw.chunkZ(), 16), localZ);
BoundaryColumnGeometry original = raw.column(x, z).geometry();
BoundaryColumnGeometry corrected = plan.isHistoricalBlock(x, z) || plan.newEpochWeightAt(x, z) == 1D
? original : containColumn(x, z, original, source);
columns.add(corrected);
changed |= corrected != original;
}
}
if (!changed) {
return raw;
}
BoundaryColumnGeometry first = columns.getFirst();
return SavedTerrainChunk.capture(raw.chunkX(), raw.chunkZ(), first.minimumY(), first.height(),
raw.nativeStatus(), new ContainedSource(raw, columns));
}
private static BoundaryColumnGeometry containColumn(int x, int z, BoundaryColumnGeometry original,
ColumnSource source) throws IOException {
List<BoundaryColumnGeometry.Voxel> voxels = original.voxels();
List<BoundaryColumnGeometry.Voxel> corrected = null;
BoundaryColumnGeometry.Voxel[] banks = null;
BoundaryColumnGeometry[] neighbors = null;
for (int offset = 0; offset < voxels.size(); offset++) {
BoundaryColumnGeometry.Voxel voxel = voxels.get(offset);
if (voxel.phase() != BoundaryColumnGeometry.Phase.FLUID || voxel.protectedContent()) {
continue;
}
if (neighbors == null) {
neighbors = new BoundaryColumnGeometry[]{
source.column(Math.subtractExact(x, 1), z), source.column(Math.addExact(x, 1), z),
source.column(x, Math.subtractExact(z, 1)), source.column(x, Math.addExact(z, 1))};
}
int worldY = Math.addExact(original.minimumY(), offset);
if (!exposed(original, neighbors, worldY)) {
continue;
}
if (corrected == null) {
corrected = new ArrayList<>(voxels);
banks = nearestDrySolids(voxels);
}
corrected.set(offset, banks[offset]);
}
return corrected == null ? original : BoundaryColumnGeometry.fromVoxels(original.minimumY(), corrected);
}
private static boolean exposed(BoundaryColumnGeometry original, BoundaryColumnGeometry[] neighbors, int worldY) {
if (original.voxelAt(Math.subtractExact(worldY, 1)).phase() == BoundaryColumnGeometry.Phase.AIR) {
return true;
}
for (BoundaryColumnGeometry neighbor : neighbors) {
if (neighbor.voxelAt(worldY).phase() == BoundaryColumnGeometry.Phase.AIR) {
return true;
}
}
return false;
}
private static BoundaryColumnGeometry.Voxel[] nearestDrySolids(List<BoundaryColumnGeometry.Voxel> voxels) {
BoundaryColumnGeometry.Voxel[] materials = new BoundaryColumnGeometry.Voxel[voxels.size()];
int[] distances = new int[voxels.size()];
BoundaryColumnGeometry.Voxel nearest = STONE;
int last = -voxels.size() - 1;
for (int offset = 0; offset < voxels.size(); offset++) {
if (drySolid(voxels.get(offset))) {
nearest = voxels.get(offset);
last = offset;
}
materials[offset] = nearest;
distances[offset] = offset - last;
}
last = voxels.size() * 2;
nearest = null;
for (int offset = voxels.size() - 1; offset >= 0; offset--) {
if (drySolid(voxels.get(offset))) {
nearest = voxels.get(offset);
last = offset;
}
if (nearest != null && last - offset < distances[offset]) {
materials[offset] = nearest;
}
}
return materials;
}
private static boolean drySolid(BoundaryColumnGeometry.Voxel voxel) {
String state = voxel.stateKey();
int properties = state.indexOf('[');
String key = properties < 0 ? state : state.substring(0, properties);
return voxel.phase() == BoundaryColumnGeometry.Phase.SOLID
&& voxel.fluidStateKey().isEmpty() && !voxel.protectedContent() && BANK_MATERIALS.contains(key);
}
@FunctionalInterface
public interface ColumnSource {
BoundaryColumnGeometry column(int blockX, int blockZ) throws IOException;
}
private record ContainedSource(SavedTerrainChunk raw, List<BoundaryColumnGeometry> columns)
implements SavedTerrainChunk.VoxelSource {
@Override
public OptionalInt groundSurface(int localX, int localZ) {
return OptionalInt.of(signature(localX, localZ).oceanFloorHeight());
}
@Override
public BoundaryColumnGeometry.Voxel voxel(int localX, int worldY, int localZ) {
return columns.get(localX * 16 + localZ).voxelAt(worldY);
}
@Override
public String biome(int localX, int worldY, int localZ) {
TerrainBoundarySignature signature = signature(localX, localZ);
return signature.biomeAtSample((worldY - signature.samples().layout().minimumY()) / 4);
}
private TerrainBoundarySignature signature(int x, int z) {
return raw.column((raw.chunkX() << 4) + x, (raw.chunkZ() << 4) + z);
}
}
}
@@ -8,6 +8,8 @@ public final class TransitionGenerationPlan {
private final GenerationBoundary boundary; private final GenerationBoundary boundary;
private final TerrainBoundarySignatureStore.Snapshot terrainSignatures; private final TerrainBoundarySignatureStore.Snapshot terrainSignatures;
private final TransitionBoundarySampler terrainSampler; private final TransitionBoundarySampler terrainSampler;
private final TransitionBandIndex terrainBand;
private final TerrainSample newTerrain;
public TransitionGenerationPlan( public TransitionGenerationPlan(
Specification specification, Specification specification,
@@ -34,6 +36,8 @@ public final class TransitionGenerationPlan {
specification.widthBlocks(), specification.widthBlocks(),
terrainSignatures terrainSignatures
); );
terrainBand = new TransitionBandIndex(boundary, terrainSampler);
newTerrain = TerrainSample.newTerrain(specification.widthBlocks());
} }
public long activationId() { public long activationId() {
@@ -93,18 +97,11 @@ public final class TransitionGenerationPlan {
} }
public boolean hasTransitionAtChunk(int chunkX, int chunkZ) { public boolean hasTransitionAtChunk(int chunkX, int chunkZ) {
if (boundary.isHistoricalChunk(chunkX, chunkZ)) { return terrainBand.contains(chunkX, chunkZ);
return false;
}
int minimumX = Math.multiplyExact(chunkX, GenerationBoundary.CHUNK_SIZE);
int minimumZ = Math.multiplyExact(chunkZ, GenerationBoundary.CHUNK_SIZE);
return terrainSampler.intersectsTerrainBand(minimumX, minimumZ,
Math.addExact(minimumX, GenerationBoundary.CHUNK_SIZE - 1),
Math.addExact(minimumZ, GenerationBoundary.CHUNK_SIZE - 1));
} }
public BoundaryGeometryInfluence geometryAt(int blockX, int blockZ) { public BoundaryGeometryInfluence geometryAt(int blockX, int blockZ) {
if (boundary.isHistoricalBlock(blockX, blockZ)) { if (!terrainBand.contains(blockX >> 4, blockZ >> 4)) {
return BoundaryGeometryInfluence.none(); return BoundaryGeometryInfluence.none();
} }
return terrainSampler.geometryAt(blockX, blockZ); return terrainSampler.geometryAt(blockX, blockZ);
@@ -114,7 +111,7 @@ public final class TransitionGenerationPlan {
if (boundary.isHistoricalBlock(blockX, blockZ)) { if (boundary.isHistoricalBlock(blockX, blockZ)) {
return TerrainSample.historicalTerrain(); return TerrainSample.historicalTerrain();
} }
return terrainSampler.sample(blockX, blockZ); return terrainBand.contains(blockX >> 4, blockZ >> 4) ? terrainSampler.sample(blockX, blockZ) : newTerrain;
} }
public Optional<String> historicalPhysicalBiomeKeyAt(int blockX, int blockY, int blockZ) { public Optional<String> historicalPhysicalBiomeKeyAt(int blockX, int blockY, int blockZ) {
@@ -128,12 +125,7 @@ public final class TransitionGenerationPlan {
TerrainSample sample TerrainSample sample
) { ) {
Objects.requireNonNull(sample, "Terrain sample"); Objects.requireNonNull(sample, "Terrain sample");
if (sample.newEpochWeight() == 1D) { return sample.distanceToHistoricalTerrain() < Math.min(4, widthBlocks())
return Optional.empty();
}
double weight = GenerationBlend.newEpochWeight(
Math.max(0D, sample.distanceToHistoricalTerrain() - 1D), Math.max(1, widthBlocks() - 1));
return GenerationBlend.usesHistoricalMaterial(blockX, blockY, blockZ, weight)
? sample.historicalPhysicalBiomeKeyAt(blockY) : Optional.empty(); ? sample.historicalPhysicalBiomeKeyAt(blockY) : Optional.empty();
} }
@@ -5,11 +5,8 @@ import java.util.List;
import java.util.Objects; import java.util.Objects;
public final class TransitionGeometryBlender { public final class TransitionGeometryBlender {
private static final int MAXIMUM_OPENING_HEIGHT = 64;
private static final BoundaryColumnGeometry.Voxel AIR = new BoundaryColumnGeometry.Voxel( private static final BoundaryColumnGeometry.Voxel AIR = new BoundaryColumnGeometry.Voxel(
"minecraft:air", BoundaryColumnGeometry.Phase.AIR, "", false); "minecraft:air", BoundaryColumnGeometry.Phase.AIR, "", false);
private static final BoundaryColumnGeometry.Voxel FLUID_BARRIER = new BoundaryColumnGeometry.Voxel(
"minecraft:obsidian", BoundaryColumnGeometry.Phase.SOLID, "", false);
private TransitionGeometryBlender() { private TransitionGeometryBlender() {
} }
@@ -49,8 +46,7 @@ public final class TransitionGeometryBlender {
boolean changed = false; boolean changed = false;
for (int offset = 0; offset < currentGeometry.height(); offset++) { for (int offset = 0; offset < currentGeometry.height(); offset++) {
BoundaryColumnGeometry.Voxel value = blendVoxel(current, historical, offset, BoundaryColumnGeometry.Voxel value = blendVoxel(current, historical, offset,
influence.newTerrainWeight(), influence.openingWeight(), GenerationBlend.usesHistoricalMaterial(blockX, influence.newTerrainWeight());
currentGeometry.minimumY() + offset, blockZ, influence.newTerrainWeight()));
result.add(value); result.add(value);
changed |= !value.equals(current.voxelAt(offset)); changed |= !value.equals(current.voxelAt(offset));
} }
@@ -61,88 +57,58 @@ public final class TransitionGeometryBlender {
ColumnProfile current, ColumnProfile current,
List<WeightedProfile> historical, List<WeightedProfile> historical,
int offset, int offset,
double currentWeight, double currentWeight
double openingWeight,
boolean historicalMaterial
) { ) {
BoundaryColumnGeometry.Voxel currentVoxel = current.voxelAt(offset); BoundaryColumnGeometry.Voxel currentVoxel = current.voxelAt(offset);
if (currentVoxel.protectedContent()) { if (currentVoxel.protectedContent() || offset == 0) {
return currentVoxel; return currentVoxel;
} }
boolean historicalOpening = false;
double solid = 0D; double solid = 0D;
double fluid = 0D;
double total = 0D; double total = 0D;
double solidMaterialWeight = -1D;
double fluidMaterialWeight = -1D;
BoundaryColumnGeometry.Voxel oldSolid = null; BoundaryColumnGeometry.Voxel oldSolid = null;
BoundaryColumnGeometry.Voxel oldFluid = null; BoundaryColumnGeometry.Voxel oldFluid = null;
double fluidWeight = 0D;
for (WeightedProfile contribution : historical) { for (WeightedProfile contribution : historical) {
ColumnProfile profile = contribution.profile(); ColumnProfile profile = contribution.profile();
if (profile.voxelAt(offset).protectedContent()) { BoundaryColumnGeometry.Voxel oldVoxel = profile.voxelAt(offset);
if (oldVoxel.protectedContent()) {
continue; continue;
} }
historicalOpening |= profile.enclosedOpeningAt(offset); total += contribution.weight();
double weight = contribution.weight(); solid += profile.solid.distanceAt(offset) * contribution.weight();
total += weight; if (oldSolid == null) {
solid += profile.solid.distanceAt(offset) * weight;
fluid += profile.fluid.distanceAt(offset) * weight;
if (profile.solid.materialAt(offset) != null && weight > solidMaterialWeight) {
oldSolid = profile.solid.materialAt(offset); oldSolid = profile.solid.materialAt(offset);
solidMaterialWeight = weight;
} }
if (profile.fluid.materialAt(offset) != null && weight > fluidMaterialWeight) { if (oldVoxel.phase() == BoundaryColumnGeometry.Phase.FLUID
oldFluid = profile.fluid.materialAt(offset); && profile.geometry.hasSolidAbove(profile.geometry.minimumY() + offset)
fluidMaterialWeight = weight; && contribution.weight() > fluidWeight) {
oldFluid = oldVoxel;
fluidWeight = contribution.weight();
} }
} }
if (total == 0D) { if (total == 0D) {
return currentVoxel; return currentVoxel;
} }
double blendedSolid = GenerationBlend.interpolate(solid / total, double blendedSolid = GenerationBlend.interpolate(solid / total,
current.solid.distanceAt(offset), historicalOpening ? openingWeight : currentWeight); current.solid.distanceAt(offset), currentWeight);
if (blendedSolid > 0D) { if (blendedSolid > 0D) {
return selectMaterial(oldSolid, current.solid.materialAt(offset), historicalMaterial); BoundaryColumnGeometry.Voxel material = current.solid.materialAt(offset);
return material != null ? material : oldSolid != null ? oldSolid : currentVoxel;
} }
double blendedFluid = GenerationBlend.interpolate(fluid / total, if (currentVoxel.phase() == BoundaryColumnGeometry.Phase.FLUID) {
current.fluid.distanceAt(offset), historicalOpening ? openingWeight : currentWeight); return currentVoxel;
if (blendedFluid > 0D) {
BoundaryColumnGeometry.Voxel newFluid = current.fluid.materialAt(offset);
if (oldFluid != null && newFluid != null && !fluidFamily(oldFluid).equals(fluidFamily(newFluid))) {
return FLUID_BARRIER;
}
return selectMaterial(oldFluid, newFluid, currentWeight <= 0.5D);
} }
return AIR; return oldFluid != null && currentWeight < 0.5D ? oldFluid : AIR;
}
private static String fluidFamily(BoundaryColumnGeometry.Voxel voxel) {
String key = voxel.fluidStateKey();
int properties = key.indexOf('[');
return properties < 0 ? key : key.substring(0, properties);
}
private static BoundaryColumnGeometry.Voxel selectMaterial(
BoundaryColumnGeometry.Voxel historical,
BoundaryColumnGeometry.Voxel current,
boolean historicalMaterial
) {
if (historical == null) {
return current == null ? AIR : current;
}
return current == null || historicalMaterial ? historical : current;
} }
private static final class ColumnProfile { private static final class ColumnProfile {
private final BoundaryColumnGeometry geometry; private final BoundaryColumnGeometry geometry;
private final PhaseCursor solid; private final PhaseCursor solid;
private final PhaseCursor fluid;
private int run; private int run;
private ColumnProfile(BoundaryColumnGeometry geometry) { private ColumnProfile(BoundaryColumnGeometry geometry) {
this.geometry = geometry; this.geometry = geometry;
solid = new PhaseCursor(geometry, BoundaryColumnGeometry.Phase.SOLID); solid = new PhaseCursor(geometry, BoundaryColumnGeometry.Phase.SOLID);
fluid = new PhaseCursor(geometry, BoundaryColumnGeometry.Phase.FLUID);
} }
private BoundaryColumnGeometry.Voxel voxelAt(int offset) { private BoundaryColumnGeometry.Voxel voxelAt(int offset) {
@@ -152,12 +118,6 @@ public final class TransitionGeometryBlender {
return geometry.runVoxel(run); return geometry.runVoxel(run);
} }
private boolean enclosedOpeningAt(int offset) {
solid.advanceTo(offset);
return !solid.occupied && solid.start > 0 && solid.end < geometry.height()
&& solid.end - solid.start <= MAXIMUM_OPENING_HEIGHT
&& !voxelAt(offset).protectedContent();
}
} }
private static final class PhaseCursor { private static final class PhaseCursor {
@@ -40,6 +40,7 @@ public final class IrisMatterSupport {
} }
IrisMatter.registerSliceType(new EntityMatter()); IrisMatter.registerSliceType(new EntityMatter());
IrisMatter.registerSliceType(new ObjectContinuationMatter());
IrisMatter.registerSliceType("art.arcane.iris.core.link.Identifier", new IdentifierMatter()); IrisMatter.registerSliceType("art.arcane.iris.core.link.Identifier", new IdentifierMatter());
IrisMatter.registerSliceType("art.arcane.iris.engine.framework.NativeStructureOwnershipBundle", new NativeStructureOwnershipMatter()); IrisMatter.registerSliceType("art.arcane.iris.engine.framework.NativeStructureOwnershipBundle", new NativeStructureOwnershipMatter());
IrisMatter.registerSliceType(new PlatformBlockMatter()); IrisMatter.registerSliceType(new PlatformBlockMatter());
@@ -0,0 +1,36 @@
package art.arcane.iris.world.storage.matter;
import art.arcane.iris.generation.mantle.ObjectContinuationBundle;
import art.arcane.volmlib.util.data.palette.Palette;
import art.arcane.volmlib.util.matter.Sliced;
import art.arcane.volmlib.util.matter.slices.RawMatter;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
@Sliced
public final class ObjectContinuationMatter extends RawMatter<ObjectContinuationBundle> {
public ObjectContinuationMatter() {
this(1, 1, 1);
}
public ObjectContinuationMatter(int width, int height, int depth) {
super(width, height, depth, ObjectContinuationBundle.class);
}
@Override
public Palette<ObjectContinuationBundle> getGlobalPalette() {
return null;
}
@Override
public void writeNode(ObjectContinuationBundle bundle, DataOutputStream output) throws IOException {
bundle.write(output);
}
@Override
public ObjectContinuationBundle readNode(DataInputStream input) throws IOException {
return ObjectContinuationBundle.read(input);
}
}
@@ -22,9 +22,17 @@ import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.block.TileData; import art.arcane.iris.generation.block.TileData;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState; import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import org.junit.Test; import org.junit.Test;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.IrisMatter;
import art.arcane.iris.structure.object.IObjectPlacer;
import art.arcane.iris.world.storage.matter.IrisMatterSupport;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame; import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertNotNull;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.inOrder;
@@ -37,7 +45,8 @@ public class FloatingObjectPlacementTransactionTest {
@Test @Test
public void unsupportedFinalTerrainRejectsBlocksTilesAndMarkersTogether() { public void unsupportedFinalTerrainRejectsBlocksTilesAndMarkersTogether() {
IslandObjectPlacer delegate = createPlacer(false); IslandObjectPlacer delegate = createPlacer(false);
FloatingObjectPlacementTransaction transaction = new FloatingObjectPlacementTransaction(delegate); FloatingObjectPlacementTransaction transaction = new FloatingObjectPlacementTransaction(delegate,
new ObjectContinuationBundle.PlacementKey(ObjectContinuationBundle.Kind.FLOATING, 0, 0, 0));
NativeBlockState first = mock(NativeBlockState.class); NativeBlockState first = mock(NativeBlockState.class);
NativeBlockState second = mock(NativeBlockState.class); NativeBlockState second = mock(NativeBlockState.class);
TileData tile = mock(TileData.class); TileData tile = mock(TileData.class);
@@ -63,7 +72,8 @@ public class FloatingObjectPlacementTransactionTest {
@Test @Test
public void supportedNeighborChunkCommitsBlocksTilesAndMarkers() { public void supportedNeighborChunkCommitsBlocksTilesAndMarkers() {
IslandObjectPlacer delegate = createPlacer(true); IslandObjectPlacer delegate = createPlacer(true);
FloatingObjectPlacementTransaction transaction = new FloatingObjectPlacementTransaction(delegate); FloatingObjectPlacementTransaction transaction = new FloatingObjectPlacementTransaction(delegate,
new ObjectContinuationBundle.PlacementKey(ObjectContinuationBundle.Kind.FLOATING, 0, 0, 0));
NativeBlockState first = mock(NativeBlockState.class); NativeBlockState first = mock(NativeBlockState.class);
NativeBlockState second = mock(NativeBlockState.class); NativeBlockState second = mock(NativeBlockState.class);
TileData tile = mock(TileData.class); TileData tile = mock(TileData.class);
@@ -86,10 +96,59 @@ public class FloatingObjectPlacementTransactionTest {
verify(delegate).set(16, 101, 8, second); verify(delegate).set(16, 101, 8, second);
} }
@Test
@SuppressWarnings("unchecked")
public void freelyFloatingObjectsPersistBothPendingFragments() {
IrisMatterSupport.ensureRegistered();
IObjectPlacer delegate = mock(IObjectPlacer.class);
Engine engine = mock(Engine.class);
EngineMantle engineMantle = mock(EngineMantle.class);
Mantle<Matter> mantle = mock(Mantle.class);
MantleChunk<Matter> left = mock(MantleChunk.class);
MantleChunk<Matter> right = mock(MantleChunk.class);
Matter leftSection = new IrisMatter(16, 16, 16);
Matter rightSection = new IrisMatter(16, 16, 16);
when(delegate.getEngine()).thenReturn(engine);
when(engine.getHeight()).thenReturn(128);
when(engine.getMantle()).thenReturn(engineMantle);
when(engineMantle.getMantle()).thenReturn(mantle);
when(mantle.getChunk(0, 0)).thenReturn(left);
when(mantle.getChunk(1, 0)).thenReturn(right);
when(left.use()).thenReturn(left);
when(right.use()).thenReturn(right);
when(left.getOrCreate(0)).thenReturn(leftSection);
when(right.getOrCreate(0)).thenReturn(rightSection);
NativeBlockState state = mock(NativeBlockState.class);
when(state.key()).thenReturn("minecraft:oak_log");
ObjectContinuationBundle.PlacementKey key = new ObjectContinuationBundle.PlacementKey(ObjectContinuationBundle.Kind.FLOATING, 0, 0, 7);
FloatingObjectPlacementTransaction transaction = new FloatingObjectPlacementTransaction(delegate, key);
transaction.set(15, 101, 8, state);
transaction.set(16, 101, 8, state);
transaction.setData(16, 101, 8, "floating-tree@7");
assertEquals(FloatingObjectPlacementTransaction.CommitResult.COMMITTED, transaction.commit());
for (Matter section : new Matter[]{leftSection, rightSection}) {
ObjectContinuationBundle bundle = section.<ObjectContinuationBundle>getSlice(ObjectContinuationBundle.class).get(0, 0, 0);
assertNotNull(bundle);
assertEquals(key, bundle.fragments().getFirst().key());
assertEquals(2, bundle.fragments().getFirst().touchedChunks().size());
}
}
@SuppressWarnings("unchecked")
private IslandObjectPlacer createPlacer(boolean supportNeighborChunk) { private IslandObjectPlacer createPlacer(boolean supportNeighborChunk) {
IslandObjectPlacer placer = mock(IslandObjectPlacer.class); IslandObjectPlacer placer = mock(IslandObjectPlacer.class);
Engine engine = mock(Engine.class); Engine engine = mock(Engine.class);
when(engine.getHeight()).thenReturn(384); when(engine.getHeight()).thenReturn(384);
EngineMantle engineMantle = mock(EngineMantle.class);
Mantle<Matter> mantle = mock(Mantle.class);
MantleChunk<Matter> chunk = mock(MantleChunk.class);
when(engine.getMantle()).thenReturn(engineMantle);
when(engineMantle.getMantle()).thenReturn(mantle);
when(mantle.getChunk(anyInt(), anyInt())).thenReturn(chunk);
when(chunk.use()).thenReturn(chunk);
when(chunk.isFlagged(MantleFlag.REAL)).thenReturn(true);
when(placer.getEngine()).thenReturn(engine); when(placer.getEngine()).thenReturn(engine);
when(placer.canWriteObjectBlock(anyInt(), anyInt(), anyInt())).thenAnswer(invocation -> { when(placer.canWriteObjectBlock(anyInt(), anyInt(), anyInt())).thenAnswer(invocation -> {
int x = invocation.getArgument(0); int x = invocation.getArgument(0);
@@ -1,7 +1,5 @@
package art.arcane.iris.generation.mantle; package art.arcane.iris.generation.mantle;
import art.arcane.iris.world.history.BoundaryColumnGeometry;
import art.arcane.iris.generation.biome.IrisBiome; import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveAction; import art.arcane.iris.generation.hydrology.cave.HydrologyCaveAction;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell; import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
@@ -12,20 +10,6 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
public class MantleObjectComponentCaveAnchorTest { public class MantleObjectComponentCaveAnchorTest {
@Test
public void retainedCaveAnchorsUseResolvedFluidAndOccupancyWithoutRawCarveFacts() {
BoundaryColumnGeometry.Voxel air = new BoundaryColumnGeometry.Voxel(
"minecraft:air", BoundaryColumnGeometry.Phase.AIR, "", false);
BoundaryColumnGeometry.Voxel water = new BoundaryColumnGeometry.Voxel(
"minecraft:water[level=0]", BoundaryColumnGeometry.Phase.FLUID, "minecraft:water[level=0]", false);
BoundaryColumnGeometry.Voxel stone = new BoundaryColumnGeometry.Voxel(
"minecraft:stone", BoundaryColumnGeometry.Phase.SOLID, "", false);
assertTrue(MantleObjectComponent.acceptsResolvedCaveAnchor(false, air));
assertFalse(MantleObjectComponent.acceptsResolvedCaveAnchor(false, water));
assertTrue(MantleObjectComponent.acceptsResolvedCaveAnchor(true, water));
assertFalse(MantleObjectComponent.acceptsResolvedCaveAnchor(true, stone));
}
@Test @Test
public void biomeOwnedPlacementsRejectForeignCaveBands() { public void biomeOwnedPlacementsRejectForeignCaveBands() {
IrisBiome frozen = biome("carving/ice"); IrisBiome frozen = biome("carving/ice");
@@ -158,6 +158,17 @@ public class MantleWriterOverlayTest {
verify(identifierSlice, never()).set(anyInt(), anyInt(), anyInt(), any(Identifier.class)); verify(identifierSlice, never()).set(anyInt(), anyInt(), anyInt(), any(Identifier.class));
} }
@Test
public void forcedStructureCarvePublishesAirForTheContentStage() {
MatterCavern cavern = new MatterCavern(true, "", (byte) 3);
writer.setForcedCarve(X, Y, Z, cavern);
verify(blockSlice).set(X, Y, Z, platform.block());
verify(cavernSlice).set(X, Y, Z, cavern);
verify(identifierSlice).set(X, Y, Z, null);
}
@Test @Test
public void protectedHydrologyRejectsLaterBlockAndCavernWrites() { public void protectedHydrologyRejectsLaterBlockAndCavernWrites() {
NativeBlockState replacement = mock(NativeBlockState.class); NativeBlockState replacement = mock(NativeBlockState.class);
@@ -0,0 +1,271 @@
package art.arcane.iris.generation.mantle;
import art.arcane.iris.generation.block.TileData;
import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.integration.Identifier;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.pack.loading.IrisRegistrant;
import art.arcane.iris.pack.loading.ResourceLoader;
import art.arcane.iris.world.entity.IrisSpawner;
import art.arcane.iris.world.storage.matter.IrisMatterContext;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.iris.world.storage.matter.IrisMatterSupport;
import art.arcane.iris.world.storage.matter.TileWrapper;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.matter.IrisMatter;
import art.arcane.volmlib.util.matter.Matter;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import org.junit.ClassRule;
import org.junit.Test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.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.when;
public class ObjectContinuationTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
@Test
public void onlyAcceptedCrossingPlacementsLeaveFutureFragments() throws Exception {
Fixture fixture = new Fixture();
ObjectDestinationTransaction source = new ObjectDestinationTransaction(fixture.writer, 0, 0);
int checkpoint = source.beginObjectPlacement();
source.setData(1, 5, 1, "inside");
source.endObjectPlacement(checkpoint);
checkpoint = source.beginObjectPlacement();
source.setData(15, 5, 1, "crossing");
source.setData(16, 5, 1, "crossing");
source.endObjectPlacement(checkpoint);
checkpoint = source.beginObjectPlacement();
source.setData(32, 5, 1, "unrelated");
source.setData(48, 5, 1, "unrelated");
source.endObjectPlacement(checkpoint);
source.setData(17, 5, 1, "rejected");
ObjectSourcePlan plan = source.sourcePlanSince(0);
plan.persistContinuations(fixture.writer, 0, 0, 0, 0);
plan.persistContinuations(fixture.writer, 0, 0, 0, 0);
assertEquals(1, fixture.bundles.size());
ObjectContinuationBundle bundle = fixture.bundles.get(1);
assertEquals(1, bundle.fragments().size());
ObjectContinuationBundle.Fragment fragment = bundle.fragments().getFirst();
assertEquals(new ObjectContinuationBundle.PlacementKey(ObjectContinuationBundle.Kind.BIOME, 0, 0, 1), fragment.key());
assertEquals(List.of(new ObjectContinuationBundle.ChunkPosition(0, 0), new ObjectContinuationBundle.ChunkPosition(1, 0)), fragment.touchedChunks());
Matter decoded = fragment.decode();
assertEquals("crossing", decoded.getSlice(String.class).get(0, 5, 1));
assertNull(decoded.getSlice(String.class).get(1, 5, 1));
}
@Test
public void generatedDestinationsDoNotRetainContinuations() {
Fixture fixture = new Fixture();
when(fixture.chunk.isFlagged(MantleFlag.REAL)).thenReturn(true);
List<ObjectDestinationTransaction.Mutation> mutations = List.of(marker(-1, "left"), marker(0, "right"));
ObjectSourcePlan plan = new ObjectSourcePlan(mutations, List.of(mutations));
plan.persistContinuations(fixture.writer, -1, 0, -1, 0);
assertTrue(fixture.bundles.isEmpty());
}
@Test
public void nestedMatterRoundTripPreservesBlocksCustomIdentifiersTreesAndTiles() throws Exception {
IrisMatterSupport.ensureRegistered();
IrisPlatform previous = IrisPlatforms.isBound() ? IrisPlatforms.get() : null;
IrisPlatform platform = mock(IrisPlatform.class);
PlatformRegistries registries = mock(PlatformRegistries.class);
NativeBlockState base = mock(NativeBlockState.class);
NativeBlockState custom = mock(NativeBlockState.class);
NativeBlockState air = mock(NativeBlockState.class);
when(base.key()).thenReturn("minecraft:chest");
when(air.key()).thenReturn("minecraft:air");
when(custom.placementBaseState()).thenReturn(base);
when(custom.deferredPlacementKey()).thenReturn("test:custom_chest");
when(platform.registries()).thenReturn(registries);
when(registries.decodeBlockState("minecraft:chest")).thenReturn(base);
when(registries.decodeBlockState("minecraft:air")).thenReturn(air);
IrisPlatforms.unbind();
IrisPlatforms.bind(platform);
TileData.TileReader previousReader = TileData.bindPlatformReader(input -> new TileData(input.readUTF(),
new Gson().fromJson(input.readUTF(), new TypeToken<KMap<String, Object>>() { })));
try {
KMap<String, Object> properties = new KMap<>();
properties.put("CustomName", "continued chest");
TileWrapper tile = new TileWrapper(new TileData("minecraft:chest", properties));
List<ObjectDestinationTransaction.Mutation> mutations = List.of(
new ObjectDestinationTransaction.CustomBlockMutation(key(NativeBlockState.class), custom),
new ObjectDestinationTransaction.SetMutation(key(String.class), "tree@42"),
new ObjectDestinationTransaction.SetMutation(key(TreeBlockMaterial.class), TreeBlockMaterial.of("minecraft:oak_log")),
new ObjectDestinationTransaction.SetMutation(key(TileWrapper.class), tile),
new ObjectDestinationTransaction.SetMutation(new ObjectDestinationTransaction.DataKey(17, 5, 1, NativeBlockState.class), air));
ObjectContinuationBundle.Fragment fragment = new ObjectContinuationBundle.Fragment(
new ObjectContinuationBundle.PlacementKey(ObjectContinuationBundle.Kind.BIOME, 0, 0, 2), new ObjectContinuationBundle.Bounds(15, 1, 17, 1),
List.of(new ObjectContinuationBundle.ChunkPosition(0, 0), new ObjectContinuationBundle.ChunkPosition(1, 0)),
ObjectSourcePlan.encode(mutations, 64));
Matter outer = new IrisMatter(16, 16, 16);
outer.slice(ObjectContinuationBundle.class).set(0, 0, 0, new ObjectContinuationBundle(List.of(fragment)));
ByteArrayOutputStream output = new ByteArrayOutputStream();
outer.write(output);
Matter loaded = Matter.read(new ByteArrayInputStream(output.toByteArray()));
ObjectContinuationBundle saved = loaded.<ObjectContinuationBundle>getSlice(ObjectContinuationBundle.class).get(0, 0, 0);
Matter payload = saved.fragments().getFirst().decode();
assertSame(base, payload.getSlice(NativeBlockState.class).get(0, 5, 1));
assertSame(air, payload.getSlice(NativeBlockState.class).get(1, 5, 1));
assertEquals(Identifier.fromString("test:custom_chest"), payload.getSlice(Identifier.class).get(0, 5, 1));
assertEquals("tree@42", payload.getSlice(String.class).get(0, 5, 1));
assertEquals(TreeBlockMaterial.of("minecraft:oak_log"), payload.getSlice(TreeBlockMaterial.class).get(0, 5, 1));
assertEquals(tile, payload.getSlice(TileWrapper.class).get(0, 5, 1));
assertFalse(saved.fragments().getFirst().encodedSize() == 0);
} finally {
TileData.restorePlatformReader(previousReader);
IrisPlatforms.unbind();
if (previous != null) {
IrisPlatforms.bind(previous);
}
}
}
@Test
@SuppressWarnings("unchecked")
public void registryMetadataDecodesUsingItsOriginPackContext() throws Exception {
IrisSpawner spawner = new IrisSpawner();
spawner.setLoadKey("old-only");
IrisData origin = mock(IrisData.class);
IrisData current = mock(IrisData.class);
ResourceLoader<IrisSpawner> loader = mock(ResourceLoader.class);
when(loader.load("old-only")).thenReturn(spawner);
KMap<Class<? extends IrisRegistrant>, ResourceLoader<? extends IrisRegistrant>> loaders = new KMap<>();
loaders.put(IrisSpawner.class, loader);
when(origin.getLoaders()).thenReturn(loaders);
when(current.getLoaders()).thenReturn(new KMap<>());
ObjectContinuationBundle.Fragment fragment = new ObjectContinuationBundle.Fragment(
new ObjectContinuationBundle.PlacementKey(ObjectContinuationBundle.Kind.BIOME, 0, 0, 0), new ObjectContinuationBundle.Bounds(15, 1, 16, 1),
List.of(new ObjectContinuationBundle.ChunkPosition(0, 0), new ObjectContinuationBundle.ChunkPosition(1, 0)),
ObjectSourcePlan.encode(List.of(new ObjectDestinationTransaction.SetMutation(key(IrisSpawner.class), spawner)), 64));
try (IrisMatterContext.Scope outer = IrisMatterContext.open(current)) {
assertThrows(IOException.class, fragment::decode);
try (IrisMatterContext.Scope scope = IrisMatterContext.open(origin)) {
Matter restored = fragment.decode();
assertSame(spawner, restored.getSlice(IrisSpawner.class).get(0, 5, 1));
}
assertSame(current, IrisMatterContext.require());
}
}
@Test
public void malformedNestedPayloadCannotSilentlyDropObjectData() {
byte[] payload = ObjectSourcePlan.encode(List.of(marker(16, "tree@42")), 64);
for (int size : new int[]{payload.length - 1, payload.length + 1}) {
ObjectContinuationBundle.Fragment fragment = new ObjectContinuationBundle.Fragment(
new ObjectContinuationBundle.PlacementKey(ObjectContinuationBundle.Kind.BIOME, 0, 0, 0), new ObjectContinuationBundle.Bounds(15, 1, 16, 1),
List.of(new ObjectContinuationBundle.ChunkPosition(0, 0), new ObjectContinuationBundle.ChunkPosition(1, 0)),
Arrays.copyOf(payload, size));
assertThrows(IOException.class, fragment::decode);
}
}
@Test
@SuppressWarnings("unchecked")
public void directProducerSkipsRealAndDuplicatePayloadSuppliers() {
IrisMatterSupport.ensureRegistered();
Mantle<Matter> mantle = mock(Mantle.class);
MantleChunk<Matter> saved = mock(MantleChunk.class);
MantleChunk<Matter> future = mock(MantleChunk.class);
when(mantle.getChunk(0, 0)).thenReturn(saved);
when(mantle.getChunk(1, 0)).thenReturn(future);
when(saved.use()).thenReturn(saved);
when(future.use()).thenReturn(future);
when(saved.isFlagged(MantleFlag.REAL)).thenReturn(true);
Matter section = new IrisMatter(16, 16, 16);
when(future.getOrCreate(0)).thenReturn(section);
AtomicInteger supplied = new AtomicInteger();
Supplier<Matter> supplier = () -> {
supplied.incrementAndGet();
Matter payload = new IrisMatter(16, 64, 16);
payload.slice(String.class).set(0, 5, 1, "tree@1");
return payload;
};
Map<ObjectContinuationBundle.ChunkPosition, Supplier<Matter>> destinations = Map.of(
new ObjectContinuationBundle.ChunkPosition(0, 0), supplier,
new ObjectContinuationBundle.ChunkPosition(1, 0), supplier);
ObjectContinuationBundle.Bounds bounds = new ObjectContinuationBundle.Bounds(15, 1, 16, 1);
ObjectContinuationBundle.PlacementKey biome = new ObjectContinuationBundle.PlacementKey(ObjectContinuationBundle.Kind.BIOME, 0, 0, 0);
ObjectContinuationPersistence.persist(mantle, biome, bounds, destinations);
ObjectContinuationPersistence.persist(mantle, biome, bounds, destinations);
assertEquals(1, supplied.get());
ObjectContinuationBundle.PlacementKey structure = new ObjectContinuationBundle.PlacementKey(ObjectContinuationBundle.Kind.STRUCTURE, 0, 0, 0);
ObjectContinuationPersistence.persist(mantle, structure, bounds, destinations);
assertEquals(2, supplied.get());
assertEquals(2, section.<ObjectContinuationBundle>getSlice(ObjectContinuationBundle.class).get(0, 0, 0).fragments().size());
}
private static ObjectDestinationTransaction.DataKey key(Class<?> type) {
return new ObjectDestinationTransaction.DataKey(16, 5, 1, type);
}
private static ObjectDestinationTransaction.Mutation marker(int x, String marker) {
return new ObjectDestinationTransaction.SetMutation(new ObjectDestinationTransaction.DataKey(x, 5, 1, String.class), marker);
}
private static final class Fixture {
private final MantleWriter writer = mock(MantleWriter.class);
private final MantleChunk<Matter> chunk;
private final Map<Integer, ObjectContinuationBundle> bundles = new HashMap<>();
@SuppressWarnings("unchecked")
private Fixture() {
chunk = mock(MantleChunk.class);
Mantle<Matter> mantle = mock(Mantle.class);
Engine engine = mock(Engine.class);
when(writer.getMantle()).thenReturn(mantle);
when(mantle.getWorldHeight()).thenReturn(64);
when(writer.getEngine()).thenReturn(engine);
when(engine.getDimension()).thenReturn(mock(IrisDimension.class));
when(writer.acquireChunk(anyInt(), anyInt())).thenReturn(chunk);
when(writer.getDataIfPresent(anyInt(), anyInt(), anyInt(), any())).thenAnswer(call -> bundles.get(((Integer) call.getArgument(0)) >> 4));
doAnswer(call -> {
Runnable task = call.getArgument(2);
task.run();
return null;
}).when(writer).withChunkFence(anyInt(), anyInt(), any(Runnable.class));
doAnswer(call -> {
ObjectContinuationBundle bundle = call.getArgument(3);
bundles.put(((Integer) call.getArgument(0)) >> 4, bundle);
return null;
}).when(writer).setData(anyInt(), anyInt(), anyInt(), any(ObjectContinuationBundle.class));
}
}
}
@@ -4,8 +4,6 @@ import art.arcane.iris.integration.Identifier;
import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial; import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial;
import art.arcane.iris.generation.runtime.IrisComplex; import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.world.history.BoundaryColumnGeometry;
import art.arcane.iris.world.history.TerrainBoundarySignature;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveAction; import art.arcane.iris.generation.hydrology.cave.HydrologyCaveAction;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell; import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
import art.arcane.iris.generation.terrain.IrisDimension; import art.arcane.iris.generation.terrain.IrisDimension;
@@ -25,9 +23,7 @@ import org.junit.Test;
import org.mockito.InOrder; import org.mockito.InOrder;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Random; import java.util.Random;
@@ -468,30 +464,6 @@ public class ObjectDestinationTransactionTest {
} }
} }
@Test
public void historicalGeometryOwnsTransactionalCarvingOverCurrentDensity() {
try (TerrainFixture fixture = new TerrainFixture()) {
BoundaryColumnGeometry.Voxel solid = new BoundaryColumnGeometry.Voxel(
"minecraft:stone", BoundaryColumnGeometry.Phase.SOLID, "", false);
BoundaryColumnGeometry.Voxel air = new BoundaryColumnGeometry.Voxel(
"minecraft:air", BoundaryColumnGeometry.Phase.AIR, "", false);
List<BoundaryColumnGeometry.Voxel> voxels = new ArrayList<>(Collections.nCopies(16, solid));
voxels.set(2, air);
TerrainBoundarySignature historical = mock(TerrainBoundarySignature.class);
when(historical.geometry()).thenReturn(BoundaryColumnGeometry.fromVoxels(-64, voxels));
when(fixture.engine.getMinHeight()).thenReturn(-64);
when(fixture.complex.resolvedTerrainColumn(0, 0)).thenReturn(Optional.of(historical));
fixture.matter.<HydrologyCaveCell>slice(HydrologyCaveCell.class)
.set(0, 2, 0, HydrologyCaveCell.of(HydrologyCaveAction.SEAL_GUARD));
ObjectDestinationTransaction transaction = new ObjectDestinationTransaction(fixture.writer, 0, 0);
assertArrayEquals(new byte[]{0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
transaction.getCarvedColumn(0, 0, 16));
assertTrue(transaction.isCarved(0, 2, 0));
assertFalse(transaction.isCarved(0, 5, 0));
}
}
@Test @Test
public void failedCommitRestoresEveryTouchedDestinationCell() { public void failedCommitRestoresEveryTouchedDestinationCell() {
MantleWriter writer = writer(); MantleWriter writer = writer();
@@ -2,8 +2,10 @@ package art.arcane.iris.generation.mantle;
import art.arcane.iris.generation.runtime.IrisComplex; import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.world.history.BoundaryColumnGeometry; import art.arcane.iris.generation.runtime.EngineMode;
import art.arcane.iris.world.history.TerrainBoundarySignature; import art.arcane.volmlib.util.stream.ProceduralStream;
import art.arcane.volmlib.util.matter.MatterCavern;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.iris.spi.IrisPlatform; import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState; import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
@@ -21,26 +23,21 @@ import org.junit.Before;
import org.junit.ClassRule; import org.junit.ClassRule;
import org.junit.Test; import org.junit.Test;
import java.util.List;
import java.util.Optional;
import java.util.OptionalInt;
import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame; import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.CALLS_REAL_METHODS; import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
public class ResolvedTerrainPlacementTest { public class TerrainPlacementQueryTest {
@ClassRule @ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean(); public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
@@ -67,27 +64,19 @@ public class ResolvedTerrainPlacementTest {
IrisPlatforms.bind(platform); IrisPlatforms.bind(platform);
IrisMatterSupport.ensureRegistered(); IrisMatterSupport.ensureRegistered();
BoundaryColumnGeometry.Voxel solid = new BoundaryColumnGeometry.Voxel(
"minecraft:stone", BoundaryColumnGeometry.Phase.SOLID, "", false);
BoundaryColumnGeometry.Voxel open = new BoundaryColumnGeometry.Voxel(
"minecraft:air", BoundaryColumnGeometry.Phase.AIR, "", false);
TerrainBoundarySignature column = mock(TerrainBoundarySignature.class);
when(column.geometry()).thenReturn(BoundaryColumnGeometry.fromVoxels(-64,
List.of(solid, open, solid, open)));
when(column.oceanFloorHeight()).thenReturn(2);
when(column.surfaceHeight()).thenReturn(5);
when(column.fluidHeight()).thenReturn(OptionalInt.of(5));
IrisComplex complex = mock(IrisComplex.class); IrisComplex complex = mock(IrisComplex.class);
when(complex.resolvedTerrainColumn(anyInt(), anyInt())).thenReturn(Optional.of(column)); ProceduralStream<Integer> heights = mock(ProceduralStream.class);
when(complex.resolvedTerrainHeight(anyInt(), anyInt(), eq(true))) when(heights.get(anyDouble(), anyDouble())).thenReturn(2);
.thenReturn(OptionalInt.of(2)); when(complex.getRoundedHeighteightStream()).thenReturn(heights);
when(complex.resolvedTerrainHeight(anyInt(), anyInt(), eq(false))) when(complex.getRiverWaterSurfaceStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 5D));
.thenReturn(OptionalInt.of(5));
Engine engine = mock(Engine.class); Engine engine = mock(Engine.class);
when(engine.getMinHeight()).thenReturn(-64); when(engine.getMinHeight()).thenReturn(-64);
when(engine.getComplex()).thenReturn(complex); when(engine.getComplex()).thenReturn(complex);
mantle = mock(Mantle.class); mantle = mock(Mantle.class);
when(mantle.getLoadedRegions()).thenReturn(new KMap<>());
when(mantle.getWorldHeight()).thenReturn(4); when(mantle.getWorldHeight()).thenReturn(4);
when(mantle.get(0, 0, 0, NativeBlockState.class)).thenReturn(stone);
when(mantle.get(0, 1, 0, MatterCavern.class)).thenReturn(new MatterCavern(true, "", (byte) 0));
MantleChunk<Matter> chunk = mock(MantleChunk.class); MantleChunk<Matter> chunk = mock(MantleChunk.class);
when(mantle.getChunk(0, 0)).thenReturn(chunk); when(mantle.getChunk(0, 0)).thenReturn(chunk);
when(chunk.use()).thenReturn(chunk); when(chunk.use()).thenReturn(chunk);
@@ -105,7 +94,7 @@ public class ResolvedTerrainPlacementTest {
} }
@Test @Test
public void placementUsesResolvedSurfaceFluidAndEnclosedOccupancy() { public void placementUsesTerrainStreamsAndRecordedCarving() {
assertEquals(2, engineMantle.getHighest(0, 0, null, true)); assertEquals(2, engineMantle.getHighest(0, 0, null, true));
assertEquals(5, engineMantle.getHighest(0, 0, null, false)); assertEquals(5, engineMantle.getHighest(0, 0, null, false));
assertEquals(5, engineMantle.getFluidHeight(0, 0)); assertEquals(5, engineMantle.getFluidHeight(0, 0));
@@ -116,22 +105,27 @@ public class ResolvedTerrainPlacementTest {
} }
@Test @Test
public void scalarPlacementQueriesDoNotReloadColumnGeometry() { public void scalarPlacementQueriesDoNotGenerateTerrain() {
Engine engine = mock(Engine.class, CALLS_REAL_METHODS); Engine engine = mock(Engine.class, CALLS_REAL_METHODS);
IrisComplex complex = engineMantle.getComplex(); IrisComplex complex = engineMantle.getComplex();
doReturn(complex).when(engine).getComplex(); doReturn(complex).when(engine).getComplex();
doReturn(null).when(engine).getData();
doReturn(false).when(engine).answersFromNaturalTerrain(0, 0);
doReturn(engineMantle).when(engine).getMantle();
EngineMode mode = mock(EngineMode.class);
doReturn(mode).when(engine).getMode();
assertEquals(2, engine.getHeight(0, 0, true)); assertEquals(2, engine.getHeight(0, 0, true));
assertEquals(5, engine.getHeight(0, 0, false)); assertEquals(5, engine.getHeight(0, 0, false));
assertEquals(2, engineMantle.getHighest(0, 0, null, true)); assertEquals(2, engineMantle.getHighest(0, 0, null, true));
assertEquals(5, engineMantle.getHighest(0, 0, null, false)); assertEquals(5, engineMantle.getHighest(0, 0, null, false));
verify(complex, never()).resolvedTerrainColumn(anyInt(), anyInt()); verifyNoInteractions(mode);
} }
@Test @Test
public void writerPrerequisitesIgnoreContentWhileLiveOccupancyIncludesBlockOnlyFills() { public void writerPrerequisitesRetainNaturalCarvingBeforeContentFills() {
matter.<NativeBlockState>slice(NativeBlockState.class).set(0, 1, 0, stone); matter.<NativeBlockState>slice(NativeBlockState.class).set(0, 1, 0, stone);
matter.<PreObjectMatterCell>slice(PreObjectMatterCell.class).set(0, 1, 0, PreObjectMatterCell.block(null)); matter.<PreObjectMatterCell>slice(PreObjectMatterCell.class).set(0, 1, 0, PreObjectMatterCell.block(null).captureCavern(new MatterCavern(true, "", (byte) 0)));
try (MantleWriter writer = new MantleWriter(engineMantle, mantle, 0, 0, 0, false)) { try (MantleWriter writer = new MantleWriter(engineMantle, mantle, 0, 0, 0, false)) {
assertSame(air, writer.getPrerequisiteBlock(0, 1, 0)); assertSame(air, writer.getPrerequisiteBlock(0, 1, 0));
assertSame(stone, writer.get(0, 1, 0)); assertSame(stone, writer.get(0, 1, 0));
@@ -1,6 +1,10 @@
package art.arcane.iris.generation.runtime; package art.arcane.iris.generation.runtime;
import art.arcane.iris.generation.stage.IrisDimensionStackActuator; import art.arcane.iris.generation.stage.IrisDimensionStackActuator;
import art.arcane.iris.generation.mantle.EngineMantle;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.iris.generation.stage.IrisTerrainNormalActuator; import art.arcane.iris.generation.stage.IrisTerrainNormalActuator;
import art.arcane.iris.generation.biome.IrisBiome; import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.terrain.IrisDimension; import art.arcane.iris.generation.terrain.IrisDimension;
@@ -291,12 +295,19 @@ public class DimensionTerrain3DCompositionTest {
return blocks; return blocks;
} }
@SuppressWarnings("unchecked")
private Hunk<NativeBlockState> render(DimensionStackLayout layout, int height) { private Hunk<NativeBlockState> render(DimensionStackLayout layout, int height) {
Engine engine = mock(Engine.class); Engine engine = mock(Engine.class);
when(engine.getSeedManager()).thenReturn(mock(SeedManager.class)); when(engine.getSeedManager()).thenReturn(mock(SeedManager.class));
when(engine.getDimensionStackContext()).thenReturn(mock(DimensionStackContext.class)); when(engine.getDimensionStackContext()).thenReturn(mock(DimensionStackContext.class));
ChunkContext context = mock(ChunkContext.class); ChunkContext context = mock(ChunkContext.class);
when(context.isSpeculativeTerrain()).thenReturn(true); EngineMantle engineMantle = mock(EngineMantle.class);
Mantle<Matter> mantle = mock(Mantle.class);
MantleChunk<Matter> mantleChunk = mock(MantleChunk.class);
when(engine.getMantle()).thenReturn(engineMantle);
when(engineMantle.getMantle()).thenReturn(mantle);
when(mantle.getChunk(0, 0)).thenReturn(mantleChunk);
when(mantleChunk.use()).thenReturn(mantleChunk);
when(context.getDimensionStackLayout(0, 0)).thenReturn(layout); when(context.getDimensionStackLayout(0, 0)).thenReturn(layout);
Hunk<NativeBlockState> output = Hunk.newArrayHunk(1, height, 1); Hunk<NativeBlockState> output = Hunk.newArrayHunk(1, height, 1);
for (int y = 0; y < height; y++) { for (int y = 0; y < height; y++) {
@@ -0,0 +1,268 @@
package art.arcane.iris.generation.runtime;
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.world.history.FloatingBiomeOverlay;
import art.arcane.iris.world.history.TransitionGenerationPlan;
import art.arcane.iris.world.history.SavedTerrainChunk;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.volmlib.nativelib.terrain.NativeBiome;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.iris.generation.context.ChunkContext;
import art.arcane.iris.generation.context.IrisContext;
import art.arcane.volmlib.util.hunk.Hunk;
import art.arcane.volmlib.util.stream.ProceduralStream;
import art.arcane.volmlib.util.collection.KList;
import org.junit.ClassRule;
import org.junit.Test;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.ArgumentMatchers.anyInt;
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.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public final class EngineModeTerrainGenerationTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
@Test
public void transitionChunksGenerateOnceAndKeepCustomStatesInTheDestination() throws Exception {
Fixture fixture = new Fixture();
NativeBlockState custom = Fixture.block("itemsadder:rocks/ruby_ore", false);
when(custom.isCustom()).thenReturn(true);
when(custom.placementBaseState()).thenReturn(fixture.stone);
when(custom.deferredPlacementKey()).thenReturn("itemsadder:rocks/ruby_ore");
TransitionGenerationPlan plan = mock(TransitionGenerationPlan.class);
when(plan.hasTransitionAtChunk(anyInt(), anyInt())).thenReturn(true);
when(fixture.complex.getTransitionGenerationPlan()).thenReturn(plan);
fixture.terrainOverride = (x, z, blocks, biomes, multicore, context) -> {
for (int localX = 0; localX < 16; localX++) {
for (int localZ = 0; localZ < 16; localZ++) {
context.setTerrainHeight(localX, localZ, 2);
for (int y = 0; y < 16; y++) {
blocks.setRaw(localX, y, localZ, y == 2 ? custom : y < 2 ? fixture.stone : fixture.air);
biomes.setRaw(localX, y, localZ, fixture.biome);
}
}
}
};
AtomicInteger customWrites = new AtomicInteger();
AtomicReference<SavedTerrainChunk> receipt = new AtomicReference<>();
doAnswer(invocation -> {
receipt.set(invocation.getArgument(0));
return null;
}).when(fixture.router).recordNaturalTerrain(any());
Hunk<NativeBlockState> blocks = Hunk.<NativeBlockState>newArrayHunk(16, 16, 16).listen((x, y, z, state) -> {
if (state.isCustom()) {
customWrites.incrementAndGet();
}
});
fixture.mode.generate(-16, -32, blocks, Hunk.newArrayHunk(16, 16, 16), true, 41L);
assertEquals(Map.of("-1,-2", 1), fixture.computations);
assertEquals(256, customWrites.get());
assertSame(custom, blocks.getRaw(0, 2, 0));
assertEquals("itemsadder:rocks/ruby_ore", receipt.get().column(-16, -32).geometry().voxelAt(2).stateKey());
verify(fixture.engine, never()).openGenerationHistoryCoordinateScope(anyInt(), anyInt());
assertEquals(41L, fixture.lastSessionId);
assertNull(IrisContext.get());
}
@Test
public void actualGenerationPublishesFloatingIdentityWithItsNaturalReceipt() throws Exception {
Fixture fixture = new Fixture();
fixture.floatingIdentity = new FloatingBiomeOverlay.Identity("floating-child", "region");
AtomicReference<FloatingBiomeOverlay> recorded = new AtomicReference<>();
doAnswer(invocation -> {
recorded.set(invocation.getArgument(2));
return null;
}).when(fixture.router).recordFloatingBiomes(anyInt(), anyInt(), any());
fixture.mode.generate(0, 0, Hunk.newArrayHunk(16, 16, 16), Hunk.newArrayHunk(16, 16, 16), false, 41L);
assertEquals(Map.of("0,0", 1), fixture.computations);
assertEquals(fixture.floatingIdentity, recorded.get().volumeAt(0, 0, 0));
assertEquals(fixture.floatingIdentity, recorded.get().surfaceAt(0, 0));
}
@Test
public void failedTerrainLeavesTheCallerContextAndSkipsReceiptAndContent() {
Fixture fixture = new Fixture();
IllegalStateException failure = new IllegalStateException("Terrain failed");
fixture.terrainOverride = (x, z, blocks, biomes, multicore, context) -> {
throw failure;
};
EngineStage content = mock(EngineStage.class);
when(fixture.mode.getStages()).thenReturn(new KList<>(content));
ChunkContext caller = new ChunkContext(64, 64, fixture.complex, false, ChunkContext.PrefillPlan.NONE, null);
try (IrisContext.Scope ignored = IrisContext.open(fixture.engine, 73L, caller)) {
assertSame(failure, assertThrows(IllegalStateException.class, () -> fixture.mode.generate(
0, 0, Hunk.newArrayHunk(16, 16, 16), Hunk.newArrayHunk(16, 16, 16), false, 41L)));
assertSame(caller, IrisContext.require().getChunkContext());
}
verify(fixture.router, never()).recordNaturalTerrain(any());
verify(content, never()).generate(anyInt(), anyInt(), any(), any(), anyBoolean(), any());
assertNull(IrisContext.get());
}
@Test
public void actualGenerationRecordsNaturalTerrainBeforeContentMutatesBlocks() {
Fixture fixture = new Fixture();
AtomicReference<SavedTerrainChunk> recorded = new AtomicReference<>();
doAnswer(invocation -> {
assertEquals(List.of("terrain"), fixture.events);
assertTrue(IrisContext.require().getChunkContext().isNaturalTerrain());
recorded.set(invocation.getArgument(0));
fixture.events.add("receipt");
return null;
}).when(fixture.router).recordNaturalTerrain(any());
EngineStage content = (x, z, blocks, biomes, multicore, context) -> {
assertFalse(context.isNaturalTerrain());
assertEquals(List.of("terrain", "receipt"), fixture.events);
assertNotNull(recorded.get());
assertEquals("minecraft:stone", recorded.get().column(x, z).geometry().voxelAt(0).stateKey());
blocks.setRaw(0, 0, 0, fixture.air);
fixture.events.add("content");
};
when(fixture.mode.getStages()).thenReturn(new KList<>(content));
doCallRealMethod().when(fixture.mode).generate(anyInt(), anyInt(), any(), any(), anyBoolean(), anyLong());
Hunk<NativeBlockState> blocks = Hunk.newArrayHunk(16, 16, 16);
Hunk<NativeBiome> biomes = Hunk.newArrayHunk(16, 16, 16);
fixture.mode.generate(0, 0, blocks, biomes, false, 41L);
assertEquals(List.of("terrain", "receipt", "content"), fixture.events);
assertSame(fixture.air, blocks.getRaw(0, 0, 0));
assertEquals("minecraft:stone", recorded.get().column(0, 0).geometry().voxelAt(0).stateKey());
assertFalse(recorded.get().hasColumn(1, 1));
assertNull(IrisContext.get());
}
@Test
public void naturalContextGuardAppliesOnlyToItsOwnComplexUntilContentBegins() {
Fixture fixture = new Fixture();
IrisComplex other = mock(IrisComplex.class);
doCallRealMethod().when(fixture.complex).isNaturalTerrainContext();
doCallRealMethod().when(other).isNaturalTerrainContext();
ChunkContext context = new ChunkContext(0, 0, fixture.complex, false, ChunkContext.PrefillPlan.NONE, null);
assertFalse(fixture.complex.isNaturalTerrainContext());
try (IrisContext.Scope ignored = IrisContext.open(fixture.engine, 5L, context)) {
assertTrue(fixture.complex.isNaturalTerrainContext());
assertFalse(other.isNaturalTerrainContext());
context.beginContent();
assertFalse(fixture.complex.isNaturalTerrainContext());
}
assertFalse(fixture.complex.isNaturalTerrainContext());
}
private static final class Fixture {
private final IrisEngine engine = mock(IrisEngine.class);
private final IrisComplex complex = mock(IrisComplex.class);
private final EngineMode mode = mock(EngineMode.class);
private final GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
private final NativeBlockState air = block("minecraft:air", true);
private final NativeBlockState stone = block("minecraft:stone", false);
private final NativeBiome biome = mock(NativeBiome.class);
private final Map<String, Integer> computations = new HashMap<>();
private final List<String> events = new ArrayList<>();
private long lastSessionId;
private EngineStage terrainOverride;
private FloatingBiomeOverlay.Identity floatingIdentity;
@SuppressWarnings("unchecked")
private Fixture() {
when(engine.getComplex()).thenReturn(complex);
when(engine.getHeight()).thenReturn(16);
when(engine.getMinHeight()).thenReturn(0);
when(engine.getMode()).thenReturn(mode);
when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
EnginePlatformHooks hooks = mock(EnginePlatformHooks.class);
when(engine.getPlatformHooks()).thenReturn(hooks);
when(hooks.shouldDisableChunkContextCache(engine)).thenReturn(true);
when(mode.getEngine()).thenReturn(engine);
when(mode.getComplex()).thenReturn(complex);
when(mode.getStages()).thenReturn(new KList<>());
doCallRealMethod().when(mode).generate(anyInt(), anyInt(), any(), any(), anyBoolean(), anyLong());
when(biome.key()).thenReturn("minecraft:plains");
ProceduralStream<Double> height = mock(ProceduralStream.class);
when(height.getDouble(anyDouble(), anyDouble())).thenReturn(3D);
ProceduralStream<IrisBiome> biomes = mock(ProceduralStream.class);
ProceduralStream<IrisRegion> regions = mock(ProceduralStream.class);
ProceduralStream<NativeBlockState> materials = mock(ProceduralStream.class);
when(materials.get(anyDouble(), anyDouble())).thenReturn(stone);
when(complex.getRawHeightStream()).thenReturn(height);
when(complex.getTrueBiomeStream()).thenReturn(biomes);
when(complex.getCaveBiomeStream()).thenReturn(biomes);
when(complex.getRegionStream()).thenReturn(regions);
when(complex.getRockStream()).thenReturn(materials);
when(complex.getFluidStream()).thenReturn(materials);
doAnswer(invocation -> {
int x = invocation.getArgument(0);
int z = invocation.getArgument(1);
Hunk<NativeBlockState> blocks = invocation.getArgument(2);
Hunk<NativeBiome> physicalBiomes = invocation.getArgument(3);
ChunkContext context = invocation.getArgument(5);
fill(x, z, blocks, physicalBiomes, context);
return null;
}).when(mode).generateTerrain(anyInt(), anyInt(), any(), any(), anyBoolean(), any());
}
private void fill(int x, int z, Hunk<NativeBlockState> blocks,
Hunk<NativeBiome> biomes, ChunkContext context) {
assertTrue(context.isNaturalTerrain());
assertSame(context, IrisContext.require().getChunkContext());
lastSessionId = IrisContext.require().getGenerationSessionId();
computations.merge((x >> 4) + "," + (z >> 4), 1, Integer::sum);
events.add("terrain");
if (terrainOverride != null) {
terrainOverride.generate(x, z, blocks, biomes, false, context);
return;
}
for (int localX = 0; localX < 16; localX++) {
for (int localZ = 0; localZ < 16; localZ++) {
int surface = 2 + Math.floorMod(x + localX + z + localZ, 5);
context.setTerrainHeight(localX, localZ, surface);
for (int y = 0; y < 16; y++) {
blocks.setRaw(localX, y, localZ, y <= surface ? stone : air);
biomes.setRaw(localX, y, localZ, biome);
}
}
}
if (floatingIdentity != null) {
context.floatingBiomes(16).record(0, 0, 0, floatingIdentity);
context.floatingBiomes(16).record(0, 2, 0, floatingIdentity);
}
}
private static NativeBlockState block(String key, boolean air) {
NativeBlockState state = mock(NativeBlockState.class);
when(state.key()).thenReturn(key);
when(state.isAir()).thenReturn(air);
return state;
}
}
}
@@ -316,7 +316,6 @@ public class IrisComplexHydrologyNoiseCacheTest {
complex.setBaseTerrainHeightStream(height); complex.setBaseTerrainHeightStream(height);
complex.setUnblendedNaturalHeightStream(height); complex.setUnblendedNaturalHeightStream(height);
complex.setHydrologyRuntime(hydrology); complex.setHydrologyRuntime(hydrology);
setField(complex, "resolvedTerrain", mock(ResolvedTerrainProvider.class));
} }
@Override @Override
@@ -11,6 +11,7 @@ import art.arcane.iris.generation.context.IrisContext;
import art.arcane.volmlib.util.stream.ProceduralStream; import art.arcane.volmlib.util.stream.ProceduralStream;
import art.arcane.volmlib.util.stream.interpolation.Interpolated; import art.arcane.volmlib.util.stream.interpolation.Interpolated;
import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.world.history.TransitionDisplacementField;
import org.junit.Test; import org.junit.Test;
import java.lang.reflect.Method; import java.lang.reflect.Method;
@@ -86,6 +87,20 @@ public class IrisComplexNaturalBiomeReuseTest {
assertFalse(IrisComplex.hasFixedNaturalBiomeNoise(dimension, List.of(leaf))); assertFalse(IrisComplex.hasFixedNaturalBiomeNoise(dimension, List.of(leaf)));
} }
@Test
public void naturalBiomeUsesTransitionWaterHead() throws Exception {
Fixture fixture = new Fixture();
TransitionDisplacementField displacement = mock(TransitionDisplacementField.class);
doReturn(40D).when(displacement).fluidHeight(-17, 262144, 63D);
Field field = IrisComplex.class.getDeclaredField("transitionDisplacement");
field.setAccessible(true);
field.set(fixture.complex, displacement);
assertSame(fixture.sea, fixture.sample(30D));
assertSame(fixture.shore, fixture.sample(40D));
assertSame(fixture.land, fixture.sample(50D));
}
private static final class Fixture { private static final class Fixture {
private final IrisComplex complex = mock(IrisComplex.class, CALLS_REAL_METHODS); private final IrisComplex complex = mock(IrisComplex.class, CALLS_REAL_METHODS);
private final IrisImageMapRuntime images = mock(IrisImageMapRuntime.class); private final IrisImageMapRuntime images = mock(IrisImageMapRuntime.class);
@@ -4,12 +4,22 @@ import art.arcane.iris.generation.hydrology.HydrologyColumnLayer;
import art.arcane.iris.generation.hydrology.HydrologyColumnSample; import art.arcane.iris.generation.hydrology.HydrologyColumnSample;
import art.arcane.iris.generation.hydrology.HydrologyFeatureRef; import art.arcane.iris.generation.hydrology.HydrologyFeatureRef;
import art.arcane.iris.generation.hydrology.HydrologyFeatureType; import art.arcane.iris.generation.hydrology.HydrologyFeatureType;
import art.arcane.iris.world.history.TransitionDisplacementField;
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
import art.arcane.iris.world.history.TransitionGenerationPlan;
import org.junit.Test; import org.junit.Test;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.lang.reflect.Field;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Answers.CALLS_REAL_METHODS;
import static org.junit.Assert.assertSame; import static org.junit.Assert.assertSame;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisComplexTransitionTest { public class IrisComplexTransitionTest {
@Test @Test
@@ -20,13 +30,13 @@ public class IrisComplexTransitionTest {
} }
@Test @Test
public void tapersHydrologyGeometryTowardNaturalTerrain() { public void tapersRiverDepthWithoutRaisingItsFluidHead() {
HydrologyColumnSample tapered = IrisComplex.taperHydrologySample(hydrologySample(), 0.5D); HydrologyColumnSample tapered = IrisComplex.taperHydrologySample(hydrologySample(), 0.5D);
HydrologyColumnLayer layer = tapered.layers().getFirst(); HydrologyColumnLayer layer = tapered.layers().getFirst();
assertEquals(73, layer.bedY()); assertEquals(73, layer.bedY());
assertEquals(75, layer.fluidHeadY()); assertEquals(70, layer.fluidHeadY());
assertEquals(75, layer.ceilingY()); assertEquals(70, layer.ceilingY());
assertEquals(73, tapered.terrainHeight()); assertEquals(73, tapered.terrainHeight());
assertEquals("river", layer.profileKey()); assertEquals("river", layer.profileKey());
} }
@@ -55,8 +65,144 @@ public class IrisComplexTransitionTest {
assertSame(cave, tapered.layers().getFirst()); assertSame(cave, tapered.layers().getFirst());
assertEquals(73, tapered.layers().getLast().bedY()); assertEquals(73, tapered.layers().getLast().bedY());
assertEquals(75, tapered.layers().getLast().fluidHeadY()); assertEquals(70, tapered.layers().getLast().fluidHeadY());
assertEquals(75, tapered.layers().getLast().ceilingY()); assertEquals(70, tapered.layers().getLast().ceilingY());
}
@Test
public void loweredSurfaceWaterPreservesContainedOceanCaveAndPlannerDatum() {
assertOceanWaterTransition(40, 63);
}
@Test
public void raisedSurfaceWaterKeepsContainedOceanCaveAndRaisesPlannerDatum() {
assertOceanWaterTransition(70, 70);
}
@Test
public void missingDisplacementPreservesHydrologyAtFullWeight() {
HydrologyColumnSample sample = hydrologySample();
assertSame(sample, IrisComplex.transitionHydrologySample(sample, 1D, null));
}
@Test
public void tinyRiverWeightsExcavateGraduallyUntilReachingUnraisedWater() {
HydrologyColumnSample sample = hydrologySample();
int previousBed = 80;
for (int step = 1; step <= 100; step++) {
double weight = step / 100D;
HydrologyColumnSample tapered = IrisComplex.taperHydrologySample(sample, weight);
HydrologyColumnLayer layer = tapered.layers().getFirst();
assertEquals(70, layer.fluidHeadY());
assertTrue(layer.bedY() <= previousBed);
assertTrue(previousBed - layer.bedY() <= 1);
assertEquals(layer.bedY(), tapered.terrainHeight());
assertEquals(layer.bedY() <= layer.fluidHeadY(), layer.channel());
assertEquals(layer.channel(), layer.fluidOwned());
if (layer.bedY() > layer.fluidHeadY()) {
assertFalse(layer.connectedFluid());
assertFalse(layer.fallingFluid());
assertFalse(layer.receivingPool());
}
previousBed = layer.bedY();
}
assertEquals(66, previousBed);
}
@Test
public void exactSeamSuppressesSurfaceCutsAndPreservesCaves() {
HydrologyColumnLayer cave = hydrologySample(HydrologyFeatureType.UNDERGROUND_POOL).layers().getFirst();
HydrologyColumnLayer surface = hydrologySample().layers().getFirst();
HydrologyColumnSample sample = new HydrologyColumnSample(4, 6, 80, 63, false, "parent", List.of(surface, cave));
HydrologyColumnSample transitioned = IrisComplex.taperHydrologySample(sample, 0D);
assertEquals(List.of(cave), transitioned.layers());
assertEquals(80, transitioned.terrainHeight());
}
@Test
public void historicalRiverHeadRestoresFluidAboveTaperedBed() {
HydrologyColumnSample sample = hydrologySample();
TransitionDisplacementField displacement = mock(TransitionDisplacementField.class);
when(displacement.sample(4, 6)).thenReturn(
new TransitionDisplacementField.Sample(0D, 8D, 0.25D, 1D, 80D, 80D, "minecraft:water"));
when(displacement.fluidHeight(4, 6, 63D)).thenReturn(68D);
when(displacement.fluidHeight(4, 6, 70D)).thenReturn(77D);
HydrologyColumnSample transitioned = IrisComplex.transitionHydrologySample(sample, 0.35D, displacement);
HydrologyColumnLayer river = transitioned.primarySurfaceFluidLayerOrNull();
assertEquals(75, transitioned.terrainHeight());
assertEquals(75, river.bedY());
assertEquals(77, river.fluidHeadY());
assertTrue(river.channel());
assertTrue(river.fluidOwned());
assertTrue(river.connectedFluid());
assertTrue(transitioned.surfacePublicationCellAt(76).isPresent());
}
@Test
public void risingSeaDoesNotKeepElevatedRiverWaterOnSeaLevelGround() {
HydrologyColumnSample sample = hydrologySample();
TransitionDisplacementField displacement = mock(TransitionDisplacementField.class);
when(displacement.sample(4, 6)).thenReturn(
new TransitionDisplacementField.Sample(0D, 8D, 0.25D, 1D, 90D, 80D, "minecraft:water"));
when(displacement.fluidHeight(4, 6, 63D)).thenReturn(80D);
when(displacement.fluidHeight(4, 6, 70D)).thenReturn(85D);
HydrologyColumnSample transitioned = IrisComplex.transitionHydrologySample(sample, 1D, displacement);
assertEquals(80, transitioned.seaLevel());
assertTrue(transitioned.layers().isEmpty());
}
@Test
public void zeroWeightSeamKeepsCavesAndDoesNotClaimColdHydrologyIsPlanned() throws Exception {
IrisComplex complex = mock(IrisComplex.class, CALLS_REAL_METHODS);
IrisHydrologyRuntime runtime = mock(IrisHydrologyRuntime.class);
complex.setHydrologyRuntime(runtime);
Field plan = IrisComplex.class.getDeclaredField("transitionGenerationPlan");
plan.setAccessible(true);
plan.set(complex, mock(TransitionGenerationPlan.class));
TransitionDisplacementField displacement = mock(TransitionDisplacementField.class);
Field field = IrisComplex.class.getDeclaredField("transitionDisplacement");
field.setAccessible(true);
field.set(complex, displacement);
when(displacement.sample(4, 6)).thenReturn(
new TransitionDisplacementField.Sample(0D, 1D, 0D, 0D, 0D, 80D, null));
when(displacement.fluidHeight(4, 6, 63D)).thenReturn(63D);
HydrologyColumnSample sample = hydrologySample(HydrologyFeatureType.UNDERGROUND_POOL);
when(runtime.sample(4D, 6D)).thenReturn(Optional.of(sample));
assertFalse(complex.isHydrologyPlanned(4, 6));
assertSame(sample.layers().getFirst(), complex.sampleHydrologyColumn(4, 6).layers().getFirst());
}
private static void assertOceanWaterTransition(int surfaceHead, int plannerSeaLevel) {
HydrologyColumnLayer cave = oceanLayer(HydrologyFeatureType.UNDERGROUND_POOL, 55, false);
HydrologyColumnLayer apron = oceanLayer(HydrologyFeatureType.MOUTH, 63, true);
HydrologyColumnSample sample = new HydrologyColumnSample(4, 6, 30, 63, true, "parent", List.of(cave, apron));
TransitionDisplacementField displacement = mock(TransitionDisplacementField.class);
when(displacement.sample(4, 6)).thenReturn(
new TransitionDisplacementField.Sample(0D, 8D, 0.25D, 1D, surfaceHead, 30D, "minecraft:water"));
when(displacement.fluidHeight(4, 6, 63D)).thenReturn((double) surfaceHead);
HydrologyColumnSample transitioned = IrisComplex.transitionHydrologySample(sample, 1D, displacement);
assertEquals(plannerSeaLevel, transitioned.seaLevel());
assertSame(cave, transitioned.layers().stream()
.filter(layer -> layer.feature().type() == HydrologyFeatureType.UNDERGROUND_POOL).findFirst().orElseThrow());
HydrologyColumnLayer surface = transitioned.layers().stream()
.filter(HydrologyColumnLayer::oceanApron).findFirst().orElseThrow();
assertEquals(surfaceHead, surface.fluidHeadY());
assertEquals(surfaceHead, surface.ceilingY());
}
private static HydrologyColumnLayer oceanLayer(HydrologyFeatureType type, int head, boolean apron) {
HydrologyFeatureRef feature = new HydrologyFeatureRef(1L, type, 2L, 3L, 4, head, 6, 1, 0, true);
return new HydrologyColumnLayer(feature, 20, head, head, true, false, false, true,
false, false, false, false, apron, "river", "surface", "mouth", "shore", "bank", "cave");
} }
private static HydrologyColumnSample hydrologySample() { private static HydrologyColumnSample hydrologySample() {
@@ -1,517 +0,0 @@
package art.arcane.iris.generation.runtime;
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.world.history.FloatingBiomeOverlay;
import art.arcane.iris.world.history.BoundaryColumnGeometry;
import art.arcane.iris.world.history.TerrainBoundarySignatureStore;
import art.arcane.iris.world.history.TransitionGenerationPlan;
import art.arcane.iris.world.history.SavedTerrainChunk;
import art.arcane.iris.world.history.TerrainBoundarySignature;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.volmlib.nativelib.terrain.NativeBiome;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.iris.generation.context.ChunkContext;
import art.arcane.iris.generation.context.IrisContext;
import art.arcane.volmlib.util.hunk.Hunk;
import art.arcane.volmlib.util.stream.ProceduralStream;
import art.arcane.volmlib.util.collection.KList;
import org.junit.ClassRule;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.ArgumentMatchers.anyInt;
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 {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
@Test
public void cachedTransitionTerrainRetainsDeferredBlocksWhenCopied() {
Fixture fixture = new Fixture();
String key = "itemsadder:rocks/ruby_ore";
NativeBlockState custom = Fixture.block(key, false);
when(custom.isCustom()).thenReturn(true);
when(custom.placementBaseState()).thenReturn(fixture.stone);
when(custom.deferredPlacementKey()).thenReturn(key);
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.terrainOverride = (x, z, blocks, biomes, multicore, context) -> {
for (int localX = 0; localX < 16; localX++) {
for (int localZ = 0; localZ < 16; localZ++) {
context.setTerrainHeight(localX, localZ, 2);
for (int y = 0; y < 16; y++) {
blocks.setRaw(localX, y, localZ, y == 2 ? custom : y < 2 ? fixture.stone : fixture.air);
biomes.setRaw(localX, y, localZ, fixture.biome);
}
}
}
};
TerrainBoundarySignature captured = fixture.provider.column(0, 0);
assertEquals(key, captured.geometry().voxelAt(2).stateKey());
IrisPlatform platform = mock(IrisPlatform.class);
PlatformRegistries registries = mock(PlatformRegistries.class);
when(platform.registries()).thenReturn(registries);
when(registries.blockOrNull(key)).thenReturn(custom);
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);
AtomicInteger customWrites = new AtomicInteger();
Hunk<NativeBlockState> blocks = Hunk.<NativeBlockState>newArrayHunk(16, 16, 16).listen((x, y, z, state) -> {
if (state.isCustom()) {
customWrites.incrementAndGet();
}
});
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, blocks, Hunk.newArrayHunk(16, 16, 16), false, context);
}
assertEquals(256, customWrites.get());
assertSame(custom, blocks.getRaw(0, 2, 0));
assertEquals(key, blocks.getRaw(0, 2, 0).deferredPlacementKey());
assertEquals(Integer.valueOf(1), fixture.computations.get("0,0"));
}
@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();
Fixture second = new Fixture();
int[][] columns = {{0, 0}, {-1, -17}, {31, 16}, {7, 13}};
List<TerrainBoundarySignature> expected = new ArrayList<>();
for (int[] column : columns) {
expected.add(first.provider.column(column[0], column[1]));
}
for (int index = columns.length - 1; index >= 0; index--) {
int[] coordinate = columns[index];
assertColumnEquals(expected.get(index), second.provider.column(coordinate[0], coordinate[1]));
}
assertEquals(3, first.computations.size());
assertEquals(3, second.computations.size());
assertNull(IrisContext.get());
}
@Test
public void evictedChunksRegenerateTheSameGeometryAndBiomes() {
Fixture fixture = new Fixture();
TerrainBoundarySignature original = fixture.provider.column(0, 0);
assertSame(original, fixture.provider.column(0, 0));
for (int chunkX = 1; chunkX <= 64; chunkX++) {
fixture.provider.column(chunkX * 16, 0);
}
TerrainBoundarySignature regenerated = fixture.provider.column(0, 0);
assertColumnEquals(original, regenerated);
assertEquals(Integer.valueOf(2), fixture.computations.get("0,0"));
fixture.provider.clear();
assertColumnEquals(original, fixture.provider.column(0, 0));
assertEquals(Integer.valueOf(3), fixture.computations.get("0,0"));
}
@Test
public void scalarHeightsSurviveGeometryEvictionForEveryColumnAndClearWithProvider() {
Fixture fixture = new Fixture();
int expected = fixture.provider.height(-1, -17, true);
for (int chunkX = 1; chunkX <= 64; chunkX++) {
fixture.provider.column(chunkX * 16, 0);
}
for (int localX = 0; localX < 16; localX++) {
for (int localZ = 0; localZ < 16; localZ++) {
int x = -16 + localX;
int z = -32 + localZ;
int height = 2 + Math.floorMod(x + z, 5);
assertEquals(height, fixture.provider.height(x, z, true));
assertEquals(height, fixture.provider.height(x, z, false));
}
}
assertEquals(Integer.valueOf(1), fixture.computations.get("-1,-2"));
assertEquals(expected, fixture.provider.column(-1, -17).oceanFloorHeight());
assertEquals(Integer.valueOf(2), fixture.computations.get("-1,-2"));
fixture.provider.clear();
assertEquals(expected, fixture.provider.height(-1, -17, true));
assertEquals(Integer.valueOf(3), fixture.computations.get("-1,-2"));
verify(fixture.router, never()).recordNaturalTerrain(any());
}
@Test
public void scalarHeightLookupHonorsNaturalContextAndTransitionBand() throws Exception {
Fixture fixture = new Fixture();
TransitionGenerationPlan plan = mock(TransitionGenerationPlan.class);
when(plan.hasTransitionAtChunk(-1, -2)).thenReturn(true);
Field transitionField = IrisComplex.class.getDeclaredField("transitionGenerationPlan");
transitionField.setAccessible(true);
transitionField.set(fixture.complex, plan);
Field providerField = IrisComplex.class.getDeclaredField("resolvedTerrain");
providerField.setAccessible(true);
providerField.set(fixture.complex, fixture.provider);
doCallRealMethod().when(fixture.complex).resolvedTerrainHeight(anyInt(), anyInt(), anyBoolean());
doCallRealMethod().when(fixture.complex).isNaturalTerrainContext();
ChunkContext context = new ChunkContext(-16, -32, fixture.complex, false,
ChunkContext.PrefillPlan.NONE, null);
assertTrue(fixture.complex.resolvedTerrainHeight(0, 0, true).isEmpty());
try (IrisContext.Scope ignored = IrisContext.open(fixture.engine, 5L, context)) {
assertTrue(fixture.complex.resolvedTerrainHeight(-1, -17, true).isEmpty());
assertTrue(fixture.computations.isEmpty());
context.beginContent();
assertEquals(OptionalInt.of(4), fixture.complex.resolvedTerrainHeight(-1, -17, true));
}
transitionField.set(fixture.complex, null);
assertTrue(fixture.complex.resolvedTerrainHeight(-1, -17, true).isEmpty());
}
@Test
public void speculativeQueriesRestoreTheCallerContextAndNeverRecordTerrain() {
Fixture fixture = new Fixture();
ChunkContext outer = new ChunkContext(64, 64, fixture.complex, false, ChunkContext.PrefillPlan.NONE, null);
outer.beginContent();
try (IrisContext.Scope ignored = IrisContext.open(fixture.engine, 73L, outer)) {
IrisContext caller = IrisContext.require();
fixture.provider.column(-1, -1);
assertSame(caller, IrisContext.require());
assertSame(outer, IrisContext.require().getChunkContext());
assertEquals(73L, fixture.lastSessionId);
}
verify(fixture.router, never()).recordNaturalTerrain(any());
assertNull(IrisContext.get());
}
@Test
public void neighborRawTerrainKeepsOneRuntimeScopeThroughPrefillAndRestoresCaller() throws Exception {
Fixture fixture = new Fixture();
ChunkContext outer = new ChunkContext(64, 64, fixture.complex, false, ChunkContext.PrefillPlan.NONE, null);
outer.beginContent();
AtomicBoolean scoped = new AtomicBoolean();
GenerationHistoryRuntimeRouter.CoordinateScope runtimeScope = mock(GenerationHistoryRuntimeRouter.CoordinateScope.class);
when(fixture.engine.openGenerationHistoryCoordinateScope(-16, -16)).thenAnswer(invocation -> {
assertFalse(scoped.getAndSet(true));
return runtimeScope;
});
ProceduralStream<Double> height = fixture.complex.getRawHeightStream();
when(fixture.complex.getRawHeightStream()).thenAnswer(invocation -> {
assertTrue(scoped.get());
return height;
});
doAnswer(invocation -> {
assertTrue(scoped.get());
assertSame(outer, IrisContext.require().getChunkContext());
scoped.set(false);
return null;
}).when(runtimeScope).close();
doAnswer(invocation -> {
assertTrue(scoped.get());
fixture.fill(invocation.getArgument(0), invocation.getArgument(1), invocation.getArgument(2),
invocation.getArgument(3), invocation.getArgument(5));
return null;
}).when(fixture.mode).generateTerrain(anyInt(), anyInt(), any(), any(), anyBoolean(), any());
try (IrisContext.Scope ignored = IrisContext.open(fixture.engine, 73L, outer)) {
TerrainBoundarySignature result = fixture.provider.column(-1, -1);
assertSame(result, fixture.provider.column(-1, -1));
assertSame(outer, IrisContext.require().getChunkContext());
assertFalse(scoped.get());
}
verify(fixture.engine, times(1)).openGenerationHistoryCoordinateScope(-16, -16);
verify(runtimeScope, times(1)).close();
verify(fixture.router, never()).recordNaturalTerrain(any());
}
@Test
public void failedNeighborTerrainClosesItsRuntimeScopeAndRestoresCaller() throws Exception {
Fixture fixture = new Fixture();
ChunkContext outer = new ChunkContext(64, 64, fixture.complex, false, ChunkContext.PrefillPlan.NONE, null);
GenerationHistoryRuntimeRouter.CoordinateScope runtimeScope = mock(GenerationHistoryRuntimeRouter.CoordinateScope.class);
when(fixture.engine.openGenerationHistoryCoordinateScope(16, 0)).thenReturn(runtimeScope);
IllegalStateException expected = new IllegalStateException("terrain failed");
fixture.terrainOverride = (x, z, blocks, biomes, multicore, context) -> {
throw expected;
};
try (IrisContext.Scope ignored = IrisContext.open(fixture.engine, 73L, outer)) {
assertSame(expected, assertThrows(IllegalStateException.class, () -> fixture.provider.column(16, 0)));
assertSame(outer, IrisContext.require().getChunkContext());
}
verify(runtimeScope, times(1)).close();
assertNull(IrisContext.get());
}
@Test
public void actualGenerationRecordsNaturalTerrainBeforeContentMutatesBlocks() {
Fixture fixture = new Fixture();
AtomicReference<SavedTerrainChunk> recorded = new AtomicReference<>();
doAnswer(invocation -> {
assertEquals(List.of("terrain"), fixture.events);
assertTrue(IrisContext.require().getChunkContext().isNaturalTerrain());
recorded.set(invocation.getArgument(0));
fixture.events.add("receipt");
return null;
}).when(fixture.router).recordNaturalTerrain(any());
EngineStage content = (x, z, blocks, biomes, multicore, context) -> {
assertFalse(context.isNaturalTerrain());
assertEquals(List.of("terrain", "receipt"), fixture.events);
assertNotNull(recorded.get());
assertEquals("minecraft:stone", recorded.get().column(x, z).geometry().voxelAt(0).stateKey());
blocks.setRaw(0, 0, 0, fixture.air);
fixture.events.add("content");
};
when(fixture.mode.getStages()).thenReturn(new KList<>(content));
doCallRealMethod().when(fixture.mode).generate(anyInt(), anyInt(), any(), any(), anyBoolean(), anyLong());
Hunk<NativeBlockState> blocks = Hunk.newArrayHunk(16, 16, 16);
Hunk<NativeBiome> biomes = Hunk.newArrayHunk(16, 16, 16);
fixture.mode.generate(0, 0, blocks, biomes, false, 41L);
assertEquals(List.of("terrain", "receipt", "content"), fixture.events);
assertSame(fixture.air, blocks.getRaw(0, 0, 0));
assertEquals("minecraft:stone", recorded.get().column(0, 0).geometry().voxelAt(0).stateKey());
assertFalse(recorded.get().hasColumn(1, 1));
assertNull(IrisContext.get());
}
@Test
public void naturalContextGuardAppliesOnlyToItsOwnComplexUntilContentBegins() {
Fixture fixture = new Fixture();
IrisComplex other = mock(IrisComplex.class);
doCallRealMethod().when(fixture.complex).isNaturalTerrainContext();
doCallRealMethod().when(other).isNaturalTerrainContext();
ChunkContext context = new ChunkContext(0, 0, fixture.complex, false, ChunkContext.PrefillPlan.NONE, null);
assertFalse(fixture.complex.isNaturalTerrainContext());
try (IrisContext.Scope ignored = IrisContext.open(fixture.engine, 5L, context)) {
assertTrue(fixture.complex.isNaturalTerrainContext());
assertFalse(other.isNaturalTerrainContext());
context.beginContent();
assertFalse(fixture.complex.isNaturalTerrainContext());
}
assertFalse(fixture.complex.isNaturalTerrainContext());
}
@Test
public void containmentReadsHistoricalFacesFromSavedColumnsWithoutGeneratingOldChunks() throws Exception {
Fixture fixture = fluidBoundary();
TerrainBoundarySignatureStore.Snapshot snapshot = mock(TerrainBoundarySignatureStore.Snapshot.class);
when(fixture.complex.getTransitionGenerationPlan().terrainSignatures()).thenReturn(snapshot);
SavedTerrainChunk saved = SavedTerrainChunk.capture(0, 0, 0, 16, "minecraft:noise", new SavedTerrainChunk.VoxelSource() {
@Override
public BoundaryColumnGeometry.Voxel voxel(int x, int y, int z) {
return new BoundaryColumnGeometry.Voxel("minecraft:air", BoundaryColumnGeometry.Phase.AIR, "", false);
}
@Override
public String biome(int x, int y, int z) {
return "minecraft:plains";
}
});
when(snapshot.signatureAt(anyInt(), anyInt())).thenAnswer(invocation ->
Optional.of(saved.column(invocation.getArgument(0), invocation.getArgument(1))));
assertEquals(3, fixture.provider.height(16, 8, true));
assertEquals(3, fixture.provider.height(16, 8, false));
assertEquals(0, fixture.provider.height(17, 8, true));
assertEquals(3, fixture.provider.height(17, 8, false));
TerrainBoundarySignature bank = fixture.provider.column(16, 8);
assertEquals("minecraft:stone", bank.geometry().voxelAt(2).stateKey());
assertEquals("minecraft:water[level=0]", fixture.provider.column(17, 8).geometry().voxelAt(2).stateKey());
assertFalse(fixture.computations.containsKey("0,0"));
verify(fixture.router, never()).recordNaturalTerrain(any());
}
@Test
public void containmentFailsIfAHistoricalFaceIsMissing() {
Fixture fixture = fluidBoundary();
TerrainBoundarySignatureStore.Snapshot snapshot = mock(TerrainBoundarySignatureStore.Snapshot.class);
when(fixture.complex.getTransitionGenerationPlan().terrainSignatures()).thenReturn(snapshot);
when(snapshot.signatureAt(anyInt(), anyInt())).thenReturn(Optional.empty());
IllegalStateException failure = assertThrows(IllegalStateException.class, () -> fixture.provider.column(16, 8));
assertTrue(failure.getCause() instanceof IOException);
assertFalse(fixture.computations.containsKey("0,0"));
verify(fixture.router, never()).recordNaturalTerrain(any());
}
private static Fixture fluidBoundary() {
Fixture fixture = new Fixture();
TransitionGenerationPlan plan = mock(TransitionGenerationPlan.class);
when(plan.hasTransitionAtChunk(anyInt(), anyInt())).thenAnswer(invocation -> (int) invocation.getArgument(0) == 1);
when(plan.isHistoricalBlock(anyInt(), anyInt())).thenAnswer(invocation -> (int) invocation.getArgument(0) < 16);
when(plan.newEpochWeightAt(anyInt(), anyInt())).thenReturn(0.5D);
when(fixture.complex.getTransitionGenerationPlan()).thenReturn(plan);
NativeBlockState water = Fixture.block("minecraft:water[level=0]", false);
when(water.isFluid()).thenReturn(true);
fixture.terrainOverride = (x, z, blocks, biomes, multicore, context) -> {
for (int localX = 0; localX < 16; localX++) {
for (int localZ = 0; localZ < 16; localZ++) {
context.setTerrainHeight(localX, localZ, 0);
for (int y = 0; y < 16; y++) {
blocks.setRaw(localX, y, localZ, y == 0 ? fixture.stone : y < 4 ? water : fixture.air);
biomes.setRaw(localX, y, localZ, fixture.biome);
}
}
}
};
return fixture;
}
private static void assertColumnEquals(TerrainBoundarySignature expected, TerrainBoundarySignature actual) {
assertEquals(expected.column(), actual.column());
assertEquals(expected.samples(), actual.samples());
assertEquals(expected.geometry(), actual.geometry());
}
private static final class Fixture {
private final IrisEngine engine = mock(IrisEngine.class);
private final IrisComplex complex = mock(IrisComplex.class);
private final EngineMode mode = mock(EngineMode.class);
private final GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
private final NativeBlockState air = block("minecraft:air", true);
private final NativeBlockState stone = block("minecraft:stone", false);
private final NativeBiome biome = mock(NativeBiome.class);
private final Map<String, Integer> computations = new HashMap<>();
private final List<String> events = new ArrayList<>();
private final ResolvedTerrainProvider provider;
private long lastSessionId;
private EngineStage terrainOverride;
private FloatingBiomeOverlay.Identity floatingIdentity;
@SuppressWarnings("unchecked")
private Fixture() {
when(engine.getComplex()).thenReturn(complex);
when(engine.getHeight()).thenReturn(16);
when(engine.getMinHeight()).thenReturn(0);
when(engine.getMode()).thenReturn(mode);
when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
EnginePlatformHooks hooks = mock(EnginePlatformHooks.class);
when(engine.getPlatformHooks()).thenReturn(hooks);
when(mode.getEngine()).thenReturn(engine);
when(mode.getComplex()).thenReturn(complex);
when(biome.key()).thenReturn("minecraft:plains");
ProceduralStream<Double> height = mock(ProceduralStream.class);
when(height.getDouble(anyDouble(), anyDouble())).thenReturn(3D);
ProceduralStream<IrisBiome> biomes = mock(ProceduralStream.class);
ProceduralStream<IrisRegion> regions = mock(ProceduralStream.class);
ProceduralStream<NativeBlockState> materials = mock(ProceduralStream.class);
when(materials.get(anyDouble(), anyDouble())).thenReturn(stone);
when(complex.getRawHeightStream()).thenReturn(height);
when(complex.getTrueBiomeStream()).thenReturn(biomes);
when(complex.getCaveBiomeStream()).thenReturn(biomes);
when(complex.getRegionStream()).thenReturn(regions);
when(complex.getRockStream()).thenReturn(materials);
when(complex.getFluidStream()).thenReturn(materials);
provider = new ResolvedTerrainProvider(engine);
when(complex.getResolvedTerrain()).thenReturn(provider);
doAnswer(invocation -> {
int x = invocation.getArgument(0);
int z = invocation.getArgument(1);
Hunk<NativeBlockState> blocks = invocation.getArgument(2);
Hunk<NativeBiome> physicalBiomes = invocation.getArgument(3);
ChunkContext context = invocation.getArgument(5);
fill(x, z, blocks, physicalBiomes, context);
return null;
}).when(mode).generateTerrain(anyInt(), anyInt(), any(), any(), anyBoolean(), any());
}
private void fill(int x, int z, Hunk<NativeBlockState> blocks,
Hunk<NativeBiome> biomes, ChunkContext context) {
assertTrue(context.isNaturalTerrain());
assertSame(context, IrisContext.require().getChunkContext());
lastSessionId = IrisContext.require().getGenerationSessionId();
computations.merge((x >> 4) + "," + (z >> 4), 1, Integer::sum);
events.add("terrain");
if (terrainOverride != null) {
terrainOverride.generate(x, z, blocks, biomes, false, context);
return;
}
for (int localX = 0; localX < 16; localX++) {
for (int localZ = 0; localZ < 16; localZ++) {
int surface = 2 + Math.floorMod(x + localX + z + localZ, 5);
context.setTerrainHeight(localX, localZ, surface);
for (int y = 0; y < 16; y++) {
blocks.setRaw(localX, y, localZ, y <= surface ? stone : air);
biomes.setRaw(localX, y, localZ, biome);
}
}
}
if (floatingIdentity != null) {
context.floatingBiomes(16).record(0, 0, 0, floatingIdentity);
context.floatingBiomes(16).record(0, 2, 0, floatingIdentity);
}
}
private static NativeBlockState block(String key, boolean air) {
NativeBlockState state = mock(NativeBlockState.class);
when(state.key()).thenReturn(key);
when(state.isAir()).thenReturn(air);
return state;
}
}
}
@@ -41,7 +41,7 @@ public class UpperDimensionTransitionTest {
IrisComplex complex = mock(IrisComplex.class); IrisComplex complex = mock(IrisComplex.class);
Terrain3DColumn terrain = Terrain3DColumnFixtures.spans(80, 0, 30, 40, 60, 100, 110); Terrain3DColumn terrain = Terrain3DColumnFixtures.spans(80, 0, 30, 40, 60, 100, 110);
when(complex.naturalTerrainColumn(-17, 8)).thenReturn(terrain); when(complex.naturalTerrainColumn(-17, 8)).thenReturn(terrain);
when(complex.getUnblendedNaturalHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 110D)); when(complex.getNaturalHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 110D));
when(complex.getHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 64D)); when(complex.getHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 64D));
Engine engine = mock(Engine.class); Engine engine = mock(Engine.class);
when(engine.getDimension()).thenReturn(dimension); when(engine.getDimension()).thenReturn(dimension);
@@ -62,7 +62,7 @@ public class UpperDimensionTransitionTest {
when(dimension.getLoadKey()).thenReturn("main"); when(dimension.getLoadKey()).thenReturn("main");
when(dimension.getUpperDimensionGap()).thenReturn(32); when(dimension.getUpperDimensionGap()).thenReturn(32);
IrisComplex complex = mock(IrisComplex.class); IrisComplex complex = mock(IrisComplex.class);
when(complex.getUnblendedNaturalHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 139D)); when(complex.getNaturalHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 139D));
when(complex.getHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 75D + 4D * x)); when(complex.getHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 75D + 4D * x));
TransitionGenerationPlan plan = mock(TransitionGenerationPlan.class); TransitionGenerationPlan plan = mock(TransitionGenerationPlan.class);
when(complex.getTransitionGenerationPlan()).thenReturn(plan); when(complex.getTransitionGenerationPlan()).thenReturn(plan);
@@ -80,12 +80,12 @@ public class UpperDimensionTransitionTest {
} }
@Test @Test
public void upperTerrainUsesUnblendedCurrentGeneratorBeforeGeometryReconciliation() { public void upperTerrainUsesDisplacedNaturalGeneratorBeforeMirroring() {
IrisDimension dimension = mock(IrisDimension.class); IrisDimension dimension = mock(IrisDimension.class);
when(dimension.getLoadKey()).thenReturn("main"); when(dimension.getLoadKey()).thenReturn("main");
when(dimension.getUpperDimensionGap()).thenReturn(32); when(dimension.getUpperDimensionGap()).thenReturn(32);
IrisComplex complex = mock(IrisComplex.class); IrisComplex complex = mock(IrisComplex.class);
when(complex.getUnblendedNaturalHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 80D)); when(complex.getNaturalHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 80D));
when(complex.getHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 64D)); when(complex.getHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 64D));
TransitionGenerationPlan plan = mock(TransitionGenerationPlan.class); TransitionGenerationPlan plan = mock(TransitionGenerationPlan.class);
when(complex.getTransitionGenerationPlan()).thenReturn(plan); when(complex.getTransitionGenerationPlan()).thenReturn(plan);
@@ -98,8 +98,7 @@ public class UpperDimensionTransitionTest {
assertEquals(175, context.getEffectiveSurfaceY(-17, 8)); assertEquals(175, context.getEffectiveSurfaceY(-17, 8));
verify(plan, never()).terrainSampleAt(anyInt(), anyInt()); verify(plan, never()).terrainSampleAt(anyInt(), anyInt());
verify(complex, never()).getNaturalHeightStream(); verify(complex, never()).getUnblendedNaturalHeightStream();
verify(complex, never()).getNaturalTrueBiomeStream();
} }
@Test @Test
@@ -55,10 +55,12 @@ public class IrisBiomeActuatorCoordinateTest {
@Test @Test
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public void speculativeTerrainDoesNotPublishPersistentMetadata() { public void historicalChunkDoesNotPublishNewPersistentMetadata() {
Engine engine = mock(Engine.class); Engine engine = mock(Engine.class);
ChunkContext context = mock(ChunkContext.class); ChunkContext context = mock(ChunkContext.class);
when(context.isSpeculativeTerrain()).thenReturn(true); IrisComplex complex = mock(IrisComplex.class);
when(context.getComplex()).thenReturn(complex);
when(complex.allowsMantleChunkWrite(0, 0)).thenReturn(false);
IrisBiomeActuator.publishNaturalMetadata(engine, 0, 0, mock(Hunk.class), context); IrisBiomeActuator.publishNaturalMetadata(engine, 0, 0, mock(Hunk.class), context);
@@ -119,6 +121,7 @@ public class IrisBiomeActuatorCoordinateTest {
IrisComplex complex = mock(IrisComplex.class); IrisComplex complex = mock(IrisComplex.class);
when(complex.allowsNewDiscreteContentAt(100, -200)).thenReturn(false); when(complex.allowsNewDiscreteContentAt(100, -200)).thenReturn(false);
TransitionGenerationPlan transitionPlan = mock(TransitionGenerationPlan.class); TransitionGenerationPlan transitionPlan = mock(TransitionGenerationPlan.class);
when(transitionPlan.hasTransitionAtChunk(6, -13)).thenReturn(true);
TransitionGenerationPlan.TerrainSample terrainSample = mock(TransitionGenerationPlan.TerrainSample.class); TransitionGenerationPlan.TerrainSample terrainSample = mock(TransitionGenerationPlan.TerrainSample.class);
when(complex.getTransitionGenerationPlan()).thenReturn(transitionPlan); when(complex.getTransitionGenerationPlan()).thenReturn(transitionPlan);
when(transitionPlan.terrainSampleAt(100, -200)).thenReturn(terrainSample); when(transitionPlan.terrainSampleAt(100, -200)).thenReturn(terrainSample);
@@ -154,6 +157,7 @@ public class IrisBiomeActuatorCoordinateTest {
IrisComplex complex = mock(IrisComplex.class); IrisComplex complex = mock(IrisComplex.class);
when(complex.allowsNewDiscreteContentAt(100, -200)).thenReturn(false); when(complex.allowsNewDiscreteContentAt(100, -200)).thenReturn(false);
TransitionGenerationPlan transitionPlan = mock(TransitionGenerationPlan.class); TransitionGenerationPlan transitionPlan = mock(TransitionGenerationPlan.class);
when(transitionPlan.hasTransitionAtChunk(6, -13)).thenReturn(true);
TransitionGenerationPlan.TerrainSample terrainSample = mock(TransitionGenerationPlan.TerrainSample.class); TransitionGenerationPlan.TerrainSample terrainSample = mock(TransitionGenerationPlan.TerrainSample.class);
when(complex.getTransitionGenerationPlan()).thenReturn(transitionPlan); when(complex.getTransitionGenerationPlan()).thenReturn(transitionPlan);
when(transitionPlan.terrainSampleAt(100, -200)).thenReturn(terrainSample); when(transitionPlan.terrainSampleAt(100, -200)).thenReturn(terrainSample);
@@ -26,7 +26,6 @@ import java.util.List;
import static org.junit.Assert.assertSame; import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
@@ -110,14 +109,16 @@ public class IrisTransitionGeometryActuatorTest {
} }
@Test @Test
public void refusesUnresolvableRetainedMaterialInsteadOfReplacingItWithAir() { public void usesNativeMaterialWhenHistoricalPaletteIsUnavailable() {
Engine engine = mock(Engine.class); Engine engine = mock(Engine.class);
ChunkContext context = context("removed:stone"); ChunkContext context = context("removed:stone");
Hunk<NativeBlockState> blocks = filled(); Hunk<NativeBlockState> blocks = filled();
Hunk<NativeBiome> biomes = Hunk.newArrayHunk(16, 16, 16); Hunk<NativeBiome> biomes = Hunk.newArrayHunk(16, 16, 16);
assertThrows(IllegalStateException.class, () -> new IrisTransitionGeometryActuator(engine) new IrisTransitionGeometryActuator(engine).generate(16, 0, blocks, biomes, false, context);
.generate(16, 0, blocks, biomes, false, context));
assertSame(stone, blocks.getRaw(0, 3, 0));
assertSame(air, blocks.getRaw(0, 2, 0));
} }
private ChunkContext context(String material) { private ChunkContext context(String material) {
@@ -139,6 +140,7 @@ public class IrisTransitionGeometryActuatorTest {
when(complex.getTransitionGenerationPlan()).thenReturn(plan); when(complex.getTransitionGenerationPlan()).thenReturn(plan);
ChunkContext context = mock(ChunkContext.class); ChunkContext context = mock(ChunkContext.class);
when(context.getComplex()).thenReturn(complex); when(context.getComplex()).thenReturn(complex);
when(context.getRoundedHeight(anyInt(), anyInt())).thenReturn(3);
return context; return context;
} }
@@ -0,0 +1,55 @@
package art.arcane.iris.generation.terrain;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public class Terrain3DColumnTest {
@Test
public void displacementPreservesCavitiesAndUpperShelvesWhileKeepingBedrockGrounded() {
Terrain3DColumn original = new Terrain3DColumn(40D, 25, true, new int[]{0, 30, 36, 42, 50, 54});
Terrain3DColumn shifted = original.displaced(10, 128);
assertEquals(50D, shifted.baseHeight(), 0D);
assertEquals(64, shifted.topY());
assertEquals(35, shifted.minY());
assertEquals(3, shifted.spanCount());
assertTrue(shifted.isSolid(0));
assertTrue(shifted.isSolid(40));
assertFalse(shifted.isSolid(41));
assertFalse(shifted.isSolid(45));
assertTrue(shifted.isSolid(46));
assertTrue(shifted.isSolid(52));
assertFalse(shifted.isSolid(53));
assertTrue(shifted.isSolid(60));
assertEquals(54, original.topY());
assertSame(original, original.displaced(0, 128));
}
@Test
public void loweringTerrainClipsOnlySpansOutsideTheWorld() {
Terrain3DColumn original = new Terrain3DColumn(40D, 25, true, new int[]{0, 30, 36, 42, 50, 54});
Terrain3DColumn shifted = original.displaced(-38, 128);
assertEquals(2, shifted.spanCount());
assertEquals(0, shifted.ceiling(0));
assertEquals(4, shifted.floor(0));
assertEquals(12, shifted.ceiling(1));
assertEquals(16, shifted.floor(1));
assertFalse(shifted.isSolid(8));
}
@Test
public void upwardDisplacementClipsTheWorldCeilingWithoutCreatingAnEmptyColumn() {
Terrain3DColumn column = new Terrain3DColumn(40D, 25, true, new int[]{0, 30, 36, 42});
Terrain3DColumn shifted = column.displaced(100, 128);
assertEquals(127, shifted.topY());
assertEquals(1, shifted.spanCount());
assertTrue(shifted.isSolid(0));
assertTrue(shifted.isSolid(127));
assertEquals(0, column.displaced(-100, 128).topY());
}
}
@@ -37,7 +37,7 @@ public class Terrain3DSurfaceSlopeTest {
public void neighboringContinuousRiverTerrainUsesItsPlacementHeight() { public void neighboringContinuousRiverTerrainUsesItsPlacementHeight() {
IrisComplex complex = complex(); IrisComplex complex = complex();
when(complex.terrainColumn(0, 0)).thenReturn(column(60, 100, 120)); when(complex.terrainColumn(0, 0)).thenReturn(column(60, 100, 120));
when(complex.getPlacementHeightStream()).thenReturn( when(complex.getHeightStream()).thenReturn(
ProceduralStream.ofDouble((x, z) -> x == 3D ? 63D : 64D)); ProceduralStream.ofDouble((x, z) -> x == 3D ? 63D : 64D));
assertEquals(5D, complex.terrainSurfaceSlope(0, 60, 0), 0D); assertEquals(5D, complex.terrainSurfaceSlope(0, 60, 0), 0D);
@@ -1,12 +1,20 @@
package art.arcane.iris.structure.object; package art.arcane.iris.structure.object;
import art.arcane.iris.generation.block.TileData; import art.arcane.iris.generation.block.TileData;
import art.arcane.iris.generation.mantle.ObjectContinuationBundle;
import art.arcane.iris.integration.Identifier;
import art.arcane.iris.world.storage.matter.IrisMatterSupport;
import art.arcane.iris.generation.terrain.IrisDimension; import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.pack.value.IrisPosition; import art.arcane.iris.pack.value.IrisPosition;
import art.arcane.iris.pack.loading.IrisData; import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.pack.loading.ResourceLoader; import art.arcane.iris.pack.loading.ResourceLoader;
import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.world.history.GenerationBoundary;
import art.arcane.iris.world.history.TransitionGenerationPlan;
import art.arcane.iris.generation.mantle.EngineMantle; import art.arcane.iris.generation.mantle.EngineMantle;
import art.arcane.iris.spi.IrisPlatform; import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.spi.IrisPlatforms;
@@ -21,6 +29,8 @@ import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.mantle.runtime.Mantle; import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk; import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.matter.Matter; import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.IrisMatter;
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import art.arcane.volmlib.util.matter.MatterSlice; import art.arcane.volmlib.util.matter.MatterSlice;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
@@ -30,10 +40,12 @@ import org.junit.Test;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame; import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertThrows;
@@ -41,6 +53,8 @@ import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@@ -59,6 +73,7 @@ public class IrisStaticObjectLayerTest {
IrisPlatforms.unbind(); IrisPlatforms.unbind();
PlatformRegistries registries = mock(PlatformRegistries.class); PlatformRegistries registries = mock(PlatformRegistries.class);
when(registries.block(anyString())).thenAnswer(call -> state(call.getArgument(0))); when(registries.block(anyString())).thenAnswer(call -> state(call.getArgument(0)));
when(registries.decodeBlockState(anyString())).thenAnswer(call -> state(call.getArgument(0)));
IrisPlatform platform = mock(IrisPlatform.class); IrisPlatform platform = mock(IrisPlatform.class);
when(platform.registries()).thenReturn(registries); when(platform.registries()).thenReturn(registries);
IrisPlatforms.bind(platform); IrisPlatforms.bind(platform);
@@ -174,7 +189,9 @@ public class IrisStaticObjectLayerTest {
when(engineMantle.getMantle()).thenReturn(mantle); when(engineMantle.getMantle()).thenReturn(mantle);
when(mantle.getChunk(anyInt(), anyInt())).thenReturn(chunk); when(mantle.getChunk(anyInt(), anyInt())).thenReturn(chunk);
when(chunk.getOrCreate(anyInt())).thenReturn(section); when(chunk.getOrCreate(anyInt())).thenReturn(section);
when(chunk.use()).thenReturn(chunk);
when(section.getSlice(TileWrapper.class)).thenReturn(tiles); when(section.getSlice(TileWrapper.class)).thenReturn(tiles);
when(chunk.isFlagged(MantleFlag.REAL)).thenReturn(true);
Hunk<NativeBlockState> left = Hunk.newArrayHunk(16, 384, 16); Hunk<NativeBlockState> left = Hunk.newArrayHunk(16, 384, 16);
Hunk<NativeBlockState> right = Hunk.newArrayHunk(16, 384, 16); Hunk<NativeBlockState> right = Hunk.newArrayHunk(16, 384, 16);
@@ -190,6 +207,104 @@ public class IrisStaticObjectLayerTest {
verify(tiles).set(15, 0, 15, null); verify(tiles).set(15, 0, 15, null);
} }
@Test
public void transitionRejectsWholeRotatedPlacementBeforeChunkWrites() {
IrisObject object = object("rotated", 1, 1, 5);
object.getBlocks().put(new IrisBlockVector(0, 0, -2), state("minecraft:stone"));
object.getBlocks().put(new IrisBlockVector(0, 0, 2), state("minecraft:stone"));
IrisStaticObjectLayer layer = compile(entry("rotated", 15, 0, 0)
.setRotation(new IrisStaticObjectRotation().setY(90)));
assertFalse(layer.blocks(0, 0).isEmpty());
assertFalse(layer.blocks(1, 0).isEmpty());
Engine runtime = historicalChunk(0, 0);
IrisStaticObjectLayer filtered = layer.forTransition(runtime);
assertTrue(filtered.isEmpty());
assertFalse(filtered.contains(17, 64, 0));
assertSame(filtered, layer.forTransition(runtime));
Engine engine = mock(Engine.class);
filtered.apply(engine, 16, 0, Hunk.newArrayHunk(16, 384, 16));
verifyNoInteractions(engine);
assertSame(layer, layer.forTransition(null));
}
@Test
public void rejectedLaterPlacementRetainsEarlierBlocksAndTiles() {
IrisObject chest = object("chest", 1, 1, 1);
chest.getBlocks().put(new IrisBlockVector(0, 0, 0), state("minecraft:chest"));
TileData tile = new TileData("minecraft:chest", new KMap<>());
chest.getStates().put(new IrisBlockVector(0, 0, 0), tile);
IrisObject bridge = object("bridge", 5, 1, 1);
bridge.getBlocks().put(new IrisBlockVector(-2, 0, 0), state("minecraft:stone"));
bridge.getBlocks().put(new IrisBlockVector(2, 0, 0), state("minecraft:stone"));
IrisStaticObjectLayer layer = compile(entry("chest", 17, 0, 0),
entry("bridge", 15, 0, 0).setBore(true));
assertSame(state("minecraft:stone"), block(layer, 17, 64, 0).state());
IrisStaticObjectLayer filtered = layer.forTransition(historicalChunk(0, 0));
assertSame(state("minecraft:chest"), block(filtered, 17, 64, 0).state());
assertEquals(tile, block(filtered, 17, 64, 0).tile());
assertFalse(filtered.contains(16, 64, 0));
assertTrue(layer.forTransition(historicalChunk(1, 0)).isEmpty());
}
@Test
public void continuationOverlapRejectsWholePlacementWithoutRejectingOtherCellsInTheChunk() {
IrisObject line = object("line", 3, 1, 1);
for (int x = -1; x <= 1; x++) {
line.getBlocks().put(new IrisBlockVector(x, 0, 0), state("minecraft:stone"));
}
IrisStaticObjectLayer layer = compile(entry("line", 17, 0, 0), entry("line", 24, 0, 0));
IrisComplex complex = mock(IrisComplex.class);
when(complex.getTransitionGenerationPlan()).thenReturn(mock(TransitionGenerationPlan.class));
when(complex.allowsNewGenerationFootprint(anyInt(), anyInt(), anyInt(), anyInt()))
.thenAnswer(call -> (int) call.getArgument(0) > 18);
Engine runtime = runtime(complex);
IrisStaticObjectLayer filtered = layer.forTransition(runtime);
assertFalse(filtered.contains(16, 64, 0));
assertFalse(filtered.contains(18, 64, 0));
assertTrue(filtered.contains(23, 64, 0));
assertTrue(filtered.contains(25, 64, 0));
verify(complex).allowsNewGenerationFootprint(16, 0, 18, 0);
verify(complex).allowsNewGenerationFootprint(23, 0, 25, 0);
}
private static Engine historicalChunk(int x, int z) {
GenerationBoundary boundary = GenerationBoundary.freeze("static-test",
List.of(new GenerationBoundary.ChunkCoordinate(x, z)));
IrisComplex complex = mock(IrisComplex.class);
when(complex.getTransitionGenerationPlan()).thenReturn(mock(TransitionGenerationPlan.class));
when(complex.allowsNewGenerationFootprint(anyInt(), anyInt(), anyInt(), anyInt()))
.thenAnswer(call -> !boundary.intersectsHistoricalBlocks(call.getArgument(0), call.getArgument(1),
call.getArgument(2), call.getArgument(3)));
return runtime(complex);
}
private static Engine runtime(IrisComplex complex) {
Engine engine = mock(Engine.class);
when(engine.getComplex()).thenReturn(complex);
return engine;
}
@Test
public void routerAttachmentInvalidatesEarlierStaticAdmission() {
IrisObject object = object("single", 1, 1, 1);
object.getBlocks().put(new IrisBlockVector(0, 0, 0), state("minecraft:stone"));
IrisStaticObjectLayer layer = compile(entry("single", 16, 0, 0));
IrisEngine engine = mock(IrisEngine.class);
IrisComplex complex = mock(IrisComplex.class);
when(engine.getComplex()).thenReturn(complex);
when(complex.getTransitionGenerationPlan()).thenReturn(mock(TransitionGenerationPlan.class));
when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.empty());
when(complex.allowsNewGenerationFootprint(16, 0, 16, 0)).thenReturn(true);
assertSame(layer, layer.forTransition(engine));
when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(mock(GenerationHistoryRuntimeRouter.class)));
when(complex.allowsNewGenerationFootprint(16, 0, 16, 0)).thenReturn(false);
assertTrue(layer.forTransition(engine).isEmpty());
}
@Test @Test
public void placementKeepsSavedTilesWithoutSharingTheirInstances() { public void placementKeepsSavedTilesWithoutSharingTheirInstances() {
IrisObject object = object("chest", 1, 1, 1); IrisObject object = object("chest", 1, 1, 1);
@@ -214,6 +329,103 @@ public class IrisStaticObjectLayerTest {
assertTrue(failure.getMessage().contains("outside the dimension height")); assertTrue(failure.getMessage().contains("outside the dimension height"));
} }
@Test
public void savedStaticPlacementRetainsPendingBlocksCustomIdentityAndTiles() throws Exception {
NativeBlockState chest = state("minecraft:chest");
NativeBlockState custom = state("test:custom_chest");
when(custom.isCustom()).thenReturn(true);
when(custom.deferredPlacementKey()).thenReturn("test:custom_chest");
when(custom.placementBaseState()).thenReturn(chest);
IrisObject object = object("crossing", 3, 1, 1);
object.getBlocks().put(new IrisBlockVector(-1, 0, 0), state("minecraft:stone"));
object.getBlocks().put(new IrisBlockVector(0, 0, 0), custom);
object.getBlocks().put(new IrisBlockVector(1, 0, 0), state("minecraft:stone"));
TileData tile = new TileData("minecraft:chest", new KMap<>());
object.getStates().put(new IrisBlockVector(0, 0, 0), tile);
IrisStaticObjectLayer layer = compile(entry("crossing", 16, 0, 0), entry("crossing", 48, 0, 0));
ContinuationFixture fixture = new ContinuationFixture();
TileData.TileReader previous = TileData.bindPlatformReader(input -> {
String material = input.readUTF();
assertEquals("{}", input.readUTF());
return new TileData(material, new KMap<>());
});
try {
layer.apply(fixture.engine, 0, 0, Hunk.newArrayHunk(16, 384, 16));
ObjectContinuationBundle pending = fixture.bundle(1, 0);
assertNotNull(pending);
assertEquals(1, pending.fragments().size());
ObjectContinuationBundle.Fragment fragment = pending.fragments().getFirst();
assertEquals(ObjectContinuationBundle.Kind.STATIC, fragment.key().kind());
assertEquals(0, fragment.key().ordinal());
Matter restored = fragment.decode();
assertSame(chest, restored.getSlice(NativeBlockState.class).get(0, 64, 0));
assertSame(state("minecraft:stone"), restored.getSlice(NativeBlockState.class).get(1, 64, 0));
assertEquals(Identifier.fromString("test:custom_chest"), restored.getSlice(Identifier.class).get(0, 64, 0));
assertEquals(tile, restored.<TileWrapper>getSlice(TileWrapper.class).get(0, 64, 0).getData());
assertNull(fixture.bundle(2, 0));
assertNull(fixture.bundle(3, 0));
layer.apply(fixture.engine, 0, 0, Hunk.newArrayHunk(16, 384, 16));
assertSame(pending, fixture.bundle(1, 0));
} finally {
TileData.restorePlatformReader(previous);
}
}
@Test
public void realStaticDestinationsDoNotBuildOrEncodeContinuationPayloads() {
NativeBlockState stone = state("minecraft:stone");
IrisObject object = object("crossing", 3, 1, 1);
object.getBlocks().put(new IrisBlockVector(-1, 0, 0), stone);
object.getBlocks().put(new IrisBlockVector(1, 0, 0), stone);
IrisStaticObjectLayer layer = compile(entry("crossing", 16, 0, 0));
ContinuationFixture fixture = new ContinuationFixture();
when(fixture.chunk(0, 0).isFlagged(MantleFlag.REAL)).thenReturn(true);
when(fixture.chunk(1, 0).isFlagged(MantleFlag.REAL)).thenReturn(true);
clearInvocations(stone);
layer.apply(fixture.engine, 0, 0, Hunk.newArrayHunk(16, 384, 16));
assertNull(fixture.bundle(0, 0));
assertNull(fixture.bundle(1, 0));
verify(stone, never()).key();
}
private static final class ContinuationFixture {
private final Engine engine = mock(Engine.class);
private final Map<ObjectContinuationBundle.ChunkPosition, MantleChunk<Matter>> chunks = new HashMap<>();
@SuppressWarnings("unchecked")
private ContinuationFixture() {
IrisMatterSupport.ensureRegistered();
EngineMantle engineMantle = mock(EngineMantle.class);
Mantle<Matter> mantle = mock(Mantle.class);
when(engine.getMantle()).thenReturn(engineMantle);
when(engineMantle.getMantle()).thenReturn(mantle);
when(mantle.getWorldHeight()).thenReturn(384);
when(mantle.getChunk(anyInt(), anyInt())).thenAnswer(call -> chunk(call.getArgument(0), call.getArgument(1)));
}
@SuppressWarnings("unchecked")
private MantleChunk<Matter> chunk(int x, int z) {
return chunks.computeIfAbsent(new ObjectContinuationBundle.ChunkPosition(x, z), key -> {
MantleChunk<Matter> chunk = mock(MantleChunk.class);
Map<Integer, Matter> sections = new HashMap<>();
when(chunk.use()).thenReturn(chunk);
when(chunk.get(anyInt())).thenAnswer(call -> sections.get((int) call.getArgument(0)));
when(chunk.getOrCreate(anyInt())).thenAnswer(call -> sections.computeIfAbsent(call.getArgument(0),
section -> new IrisMatter(16, 16, 16)));
return chunk;
});
}
private ObjectContinuationBundle bundle(int x, int z) {
MantleChunk<Matter> chunk = chunks.get(new ObjectContinuationBundle.ChunkPosition(x, z));
Matter section = chunk == null ? null : chunk.get(0);
MatterSlice<ObjectContinuationBundle> slice = section == null ? null : section.getSlice(ObjectContinuationBundle.class);
return slice == null ? null : slice.get(0, 0, 0);
}
}
@Test @Test
public void repeatedCompilationDoesNotChangeSavedBlocksOrPlacement() { public void repeatedCompilationDoesNotChangeSavedBlocksOrPlacement() {
IrisObject object = object("line", 3, 1, 1); IrisObject object = object("line", 3, 1, 1);
@@ -565,6 +565,41 @@ public class IrisStructureLocatorContractTest {
assertTrue(IrisStructureLocator.matchesResolved(resolved, "minecraft:second")); assertTrue(IrisStructureLocator.matchesResolved(resolved, "minecraft:second"));
} }
@Test
public void vacuumStructureRejectsApronThatCrossesHistoricalTerrain() {
Engine engine = mock(Engine.class);
IrisComplex complex = mock(IrisComplex.class);
when(engine.getComplex()).thenReturn(complex);
when(complex.allowsNewGenerationFootprint(anyInt(), anyInt(), anyInt(), anyInt()))
.thenAnswer(call -> (int) call.getArgument(0) >= 16);
IrisStructurePlacement placement = new IrisStructurePlacement();
IrisStructure structure = new IrisStructure().setPlaceMode(ObjectPlaceMode.VACUUM_HIGH);
IrisStructureLocator.ResolvedPlacement resolved = new IrisStructureLocator.ResolvedPlacement(
placement, "test:vacuum", structure,
new KList<>(piece(24, 64, 24, 28, 68, 28)), new RNG(1L), 26, 64, 26, false);
assertFalse(IrisStructureLocator.allowsResolvedFootprint(engine, resolved));
verify(complex).allowsNewGenerationFootprint(4, 4, 48, 48);
structure.setPlaceMode(ObjectPlaceMode.STRUCTURE_PIECE);
assertTrue(IrisStructureLocator.allowsResolvedFootprint(engine, resolved));
verify(complex).allowsNewGenerationFootprint(24, 24, 28, 28);
}
@Test
public void multiPieceAssembliesDoNotUseSinglePieceVacuumAprons() {
Engine engine = mock(Engine.class);
IrisComplex complex = mock(IrisComplex.class);
when(engine.getComplex()).thenReturn(complex);
when(complex.allowsNewGenerationFootprint(24, 24, 34, 28)).thenReturn(true);
IrisStructureLocator.ResolvedPlacement resolved = new IrisStructureLocator.ResolvedPlacement(
new IrisStructurePlacement(), "test:vacuum", new IrisStructure().setPlaceMode(ObjectPlaceMode.VACUUM),
new KList<>(piece(24, 64, 24, 28, 68, 28), piece(30, 64, 24, 34, 68, 28)),
new RNG(1L), 26, 64, 26, false);
assertTrue(IrisStructureLocator.allowsResolvedFootprint(engine, resolved));
verify(complex).allowsNewGenerationFootprint(24, 24, 34, 28);
}
@Test @Test
public void undergroundAssemblyIsShiftedToFitOrRejectedWhole() { public void undergroundAssemblyIsShiftedToFitOrRejectedWhole() {
IrisStructurePlacement placement = new IrisStructurePlacement(); IrisStructurePlacement placement = new IrisStructurePlacement();
@@ -5,12 +5,8 @@ import java.util.List;
import java.util.Objects; import java.util.Objects;
final class DenseTransitionGeometryReference { final class DenseTransitionGeometryReference {
private static final int MAXIMUM_OPENING_HEIGHT = 64;
private static final BoundaryColumnGeometry.Voxel AIR = new BoundaryColumnGeometry.Voxel( private static final BoundaryColumnGeometry.Voxel AIR = new BoundaryColumnGeometry.Voxel(
"minecraft:air", BoundaryColumnGeometry.Phase.AIR, "", false); "minecraft:air", BoundaryColumnGeometry.Phase.AIR, "", false);
private static final BoundaryColumnGeometry.Voxel FLUID_BARRIER = new BoundaryColumnGeometry.Voxel(
"minecraft:obsidian", BoundaryColumnGeometry.Phase.SOLID, "", false);
private DenseTransitionGeometryReference() { private DenseTransitionGeometryReference() {
} }
@@ -39,8 +35,7 @@ final class DenseTransitionGeometryReference {
boolean changed = false; boolean changed = false;
for (int offset = 0; offset < currentGeometry.height(); offset++) { for (int offset = 0; offset < currentGeometry.height(); offset++) {
BoundaryColumnGeometry.Voxel value = blendVoxel(current, historical, offset, BoundaryColumnGeometry.Voxel value = blendVoxel(current, historical, offset,
influence.newTerrainWeight(), influence.openingWeight(), GenerationBlend.usesHistoricalMaterial(blockX, influence.newTerrainWeight());
currentGeometry.minimumY() + offset, blockZ, influence.newTerrainWeight()));
result.add(value); result.add(value);
changed |= !value.equals(current.voxels.get(offset)); changed |= !value.equals(current.voxels.get(offset));
} }
@@ -48,79 +43,45 @@ final class DenseTransitionGeometryReference {
} }
private static BoundaryColumnGeometry.Voxel blendVoxel( private static BoundaryColumnGeometry.Voxel blendVoxel(
ColumnProfile current, ColumnProfile current, List<WeightedProfile> historical, int offset, double currentWeight
List<WeightedProfile> historical,
int offset,
double currentWeight,
double openingWeight,
boolean historicalMaterial
) { ) {
BoundaryColumnGeometry.Voxel currentVoxel = current.voxels.get(offset); BoundaryColumnGeometry.Voxel currentVoxel = current.voxels.get(offset);
if (currentVoxel.protectedContent()) { if (currentVoxel.protectedContent() || offset == 0) {
return currentVoxel; return currentVoxel;
} }
boolean historicalOpening = false;
double solid = 0D; double solid = 0D;
double fluid = 0D;
double total = 0D; double total = 0D;
double solidMaterialWeight = -1D; double fluidWeight = 0D;
double fluidMaterialWeight = -1D;
BoundaryColumnGeometry.Voxel oldSolid = null; BoundaryColumnGeometry.Voxel oldSolid = null;
BoundaryColumnGeometry.Voxel oldFluid = null; BoundaryColumnGeometry.Voxel oldFluid = null;
for (WeightedProfile contribution : historical) { for (WeightedProfile contribution : historical) {
ColumnProfile profile = contribution.profile(); ColumnProfile profile = contribution.profile();
if (profile.voxels.get(offset).protectedContent()) { BoundaryColumnGeometry.Voxel voxel = profile.voxels.get(offset);
if (voxel.protectedContent()) {
continue; continue;
} }
historicalOpening |= profile.enclosedOpenings[offset]; total += contribution.weight();
double weight = contribution.weight(); solid += profile.solidDistances[offset] * contribution.weight();
total += weight; if (oldSolid == null) {
solid += profile.solidDistances[offset] * weight;
fluid += profile.fluidDistances[offset] * weight;
if (profile.solidMaterials[offset] != null && weight > solidMaterialWeight) {
oldSolid = profile.solidMaterials[offset]; oldSolid = profile.solidMaterials[offset];
solidMaterialWeight = weight;
} }
if (profile.fluidMaterials[offset] != null && weight > fluidMaterialWeight) { if (voxel.phase() == BoundaryColumnGeometry.Phase.FLUID
oldFluid = profile.fluidMaterials[offset]; && profile.solidAbove[offset] && contribution.weight() > fluidWeight) {
fluidMaterialWeight = weight; oldFluid = voxel;
fluidWeight = contribution.weight();
} }
} }
if (total == 0D) { if (total == 0D) {
return currentVoxel; return currentVoxel;
} }
double blendedSolid = GenerationBlend.interpolate(solid / total, if (GenerationBlend.interpolate(solid / total, current.solidDistances[offset], currentWeight) > 0D) {
current.solidDistances[offset], historicalOpening ? openingWeight : currentWeight); BoundaryColumnGeometry.Voxel material = current.solidMaterials[offset];
if (blendedSolid > 0D) { return material != null ? material : oldSolid != null ? oldSolid : currentVoxel;
return selectMaterial(oldSolid, current.solidMaterials[offset], historicalMaterial);
} }
double blendedFluid = GenerationBlend.interpolate(fluid / total, if (currentVoxel.phase() == BoundaryColumnGeometry.Phase.FLUID) {
current.fluidDistances[offset], historicalOpening ? openingWeight : currentWeight); return currentVoxel;
if (blendedFluid > 0D) {
BoundaryColumnGeometry.Voxel newFluid = current.fluidMaterials[offset];
if (oldFluid != null && newFluid != null && !fluidFamily(oldFluid).equals(fluidFamily(newFluid))) {
return FLUID_BARRIER;
}
return selectMaterial(oldFluid, newFluid, currentWeight <= 0.5D);
} }
return AIR; return oldFluid != null && currentWeight < 0.5D ? oldFluid : AIR;
}
private static String fluidFamily(BoundaryColumnGeometry.Voxel voxel) {
String key = voxel.fluidStateKey();
int properties = key.indexOf('[');
return properties < 0 ? key : key.substring(0, properties);
}
private static BoundaryColumnGeometry.Voxel selectMaterial(
BoundaryColumnGeometry.Voxel historical,
BoundaryColumnGeometry.Voxel current,
boolean historicalMaterial
) {
if (historical == null) {
return current == null ? AIR : current;
}
return current == null || historicalMaterial ? historical : current;
} }
private static BoundaryColumnGeometry.Voxel[] nearestMaterials( private static BoundaryColumnGeometry.Voxel[] nearestMaterials(
@@ -155,38 +116,22 @@ final class DenseTransitionGeometryReference {
return materials; return materials;
} }
private static boolean[] enclosedOpenings(List<BoundaryColumnGeometry.Voxel> voxels) {
boolean[] openings = new boolean[voxels.size()];
int lowerSolid = -1;
for (int index = 0; index < voxels.size(); index++) {
BoundaryColumnGeometry.Voxel voxel = voxels.get(index);
if (voxel.phase() == BoundaryColumnGeometry.Phase.SOLID && !voxel.protectedContent()) {
if (lowerSolid >= 0 && index - lowerSolid - 1 <= MAXIMUM_OPENING_HEIGHT) {
for (int opening = lowerSolid + 1; opening < index; opening++) {
openings[opening] = !voxels.get(opening).protectedContent();
}
}
lowerSolid = index;
}
}
return openings;
}
private static final class ColumnProfile { private static final class ColumnProfile {
private final List<BoundaryColumnGeometry.Voxel> voxels; private final List<BoundaryColumnGeometry.Voxel> voxels;
private final boolean[] enclosedOpenings; private final boolean[] solidAbove;
private final double[] solidDistances; private final double[] solidDistances;
private final double[] fluidDistances;
private final BoundaryColumnGeometry.Voxel[] solidMaterials; private final BoundaryColumnGeometry.Voxel[] solidMaterials;
private final BoundaryColumnGeometry.Voxel[] fluidMaterials;
private ColumnProfile(BoundaryColumnGeometry geometry) { private ColumnProfile(BoundaryColumnGeometry geometry) {
voxels = geometry.voxels(); voxels = geometry.voxels();
enclosedOpenings = enclosedOpenings(voxels);
solidDistances = geometry.solidDistances(); solidDistances = geometry.solidDistances();
fluidDistances = geometry.fluidDistances();
solidMaterials = nearestMaterials(voxels, BoundaryColumnGeometry.Phase.SOLID); solidMaterials = nearestMaterials(voxels, BoundaryColumnGeometry.Phase.SOLID);
fluidMaterials = nearestMaterials(voxels, BoundaryColumnGeometry.Phase.FLUID); solidAbove = new boolean[voxels.size()];
boolean solidSeen = false;
for (int index = voxels.size() - 1; index >= 0; index--) {
solidAbove[index] = solidSeen;
solidSeen |= voxels.get(index).phase() == BoundaryColumnGeometry.Phase.SOLID;
}
} }
} }
@@ -96,6 +96,61 @@ public class GenerationHistoryRecoveryTest extends GenerationHistorySupport {
assertEquals(1, reopened.manifest().activation(1L).orElseThrow().activationId()); assertEquals(1, reopened.manifest().activation(1L).orElseThrow().activationId());
} }
@Test
public void unchangedKernelBuildRetainsActivationOwnershipAndSavedMetadata() throws Exception {
Path world = temporaryFolder.newFolder("unchanged-build-world").toPath();
Path pack = createPack("unchanged-build-pack", "alpha");
GenerationKernelRegistry.Version version = new GenerationKernelRegistry.Version(101, 1, 1);
GenerationHistory original = GenerationHistory.create(world, pack, fingerprint(pack), 42L,
contract(), GenerationRegistryContract.empty(), version, singleKernel(version, "a"));
GenerationEpoch epoch = original.activeEpoch();
Path installedPack = original.activePackRoot();
Path region = Files.createDirectories(world.resolve("region")).resolve("r.0.0.mca");
SavedTerrainTestRegion.write(region, new int[][]{{0, 0}});
byte[] savedBlocks = Files.readAllBytes(region);
byte[] manifest = Files.readAllBytes(original.paths().manifest());
GenerationHistory rebuilt = GenerationHistory.open(world, singleKernel(version, "a"));
assertTrue(rebuilt.usesCurrentGenerator());
rebuilt.prepareCurrentGenerator(32);
assertEquals(1L, rebuilt.activeActivation().activationId());
assertEquals(1, rebuilt.manifest().activations().size());
assertEquals(1, rebuilt.manifest().epochs().size());
assertEquals(epoch, rebuilt.activeEpoch());
assertEquals(installedPack, rebuilt.activePackRoot());
assertEquals(1L, rebuilt.resolveActivation(0, 0).activationId());
assertEquals(1L, rebuilt.resolveActivation(1, 0).activationId());
assertTrue(rebuilt.pendingActivation().isEmpty());
assertArrayEquals(manifest, Files.readAllBytes(rebuilt.paths().manifest()));
assertArrayEquals(savedBlocks, Files.readAllBytes(region));
assertThrows(IllegalArgumentException.class, () -> rebuilt.transitionPlan(1L));
}
@Test
public void changedImplementationWithTheSameAbiCreatesATransitionAndRetainsOldMetadata() throws Exception {
Path world = temporaryFolder.newFolder("changed-implementation-world").toPath();
Path pack = createPack("changed-implementation-pack", "alpha");
GenerationKernelRegistry.Version version = new GenerationKernelRegistry.Version(101, 1, 1);
GenerationHistory original = GenerationHistory.create(world, pack, fingerprint(pack), 42L,
contract(), GenerationRegistryContract.empty(), version, singleKernel(version, "a"));
GenerationEpoch previous = original.activeEpoch();
Path region = Files.createDirectories(world.resolve("region")).resolve("r.0.0.mca");
SavedTerrainTestRegion.write(region, new int[][]{{0, 0}});
byte[] savedBlocks = Files.readAllBytes(region);
GenerationHistory changed = GenerationHistory.open(world, singleKernel(version, "b"));
assertFalse(changed.usesCurrentGenerator());
changed.prepareCurrentGenerator(32);
assertEquals(2L, changed.activeActivation().activationId());
assertEquals(previous, changed.resolveEpoch(0, 0));
assertEquals("b".repeat(64), changed.resolveEpoch(1, 0).kernelImplementationFingerprint());
assertEquals(previous.kernelVersion(), changed.activeEpoch().kernelVersion());
assertTrue(changed.transitionPlan(2L).hasTransitionAtChunk(1, 0));
assertArrayEquals(savedBlocks, Files.readAllBytes(region));
}
@Test @Test
public void changedBuildExpandsFromSavedTerrainWithoutTheOldFactory() throws Exception { public void changedBuildExpandsFromSavedTerrainWithoutTheOldFactory() throws Exception {
Path world = temporaryFolder.newFolder("build-upgrade-world").toPath(); Path world = temporaryFolder.newFolder("build-upgrade-world").toPath();
@@ -10,6 +10,8 @@ import org.mockito.MockedStatic;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.mockStatic;
@@ -194,6 +196,42 @@ public class GenerationHistoryTest extends GenerationHistorySupport {
assertEquals(history.boundary(2L).identity(), active.transition().boundaryIdentity()); assertEquals(history.boundary(2L).identity(), active.transition().boundaryIdentity());
} }
@Test
public void transitionPlansReuseFrozenInputsAcrossPromotionsAndReopen() throws Exception {
Path world = temporaryFolder.newFolder("shared-plan-world").toPath();
Path packA = createPack("shared-plan-a", "alpha");
Path packB = createPack("shared-plan-b", "beta");
GenerationHistory history = createHistory(world, packA);
Path region = Files.createDirectories(world.resolve("region")).resolve("r.0.0.mca");
writeRegion(region, new int[][]{{0, 0}});
stage(history, packB);
history.promotePending(signaturesForChunks(new int[][]{{0, 0}}));
TransitionGenerationPlan first = history.transitionPlan(2L);
assertSame(first, history.transitionPlan(2L));
assertSame(history.boundary(2L), first.boundary());
assertSame(history.terrainSignatures(2L), first.terrainSignatures());
assertTrue(first.hasTransitionAtChunk(1, 0));
writeRegion(region, new int[][]{{0, 0}, {1, 0}});
stage(history, packA);
history.promotePending(signaturesForChunks(new int[][]{{0, 0}, {1, 0}}));
TransitionGenerationPlan second = history.transitionPlan(3L);
assertSame(first, history.transitionPlan(2L));
assertSame(second, history.transitionPlan(3L));
assertNotSame(first, second);
assertTrue(first.hasTransitionAtChunk(1, 0));
assertFalse(second.hasTransitionAtChunk(1, 0));
assertEquals(history.activeActivation().transition().terrainSignatureIdentity(),
second.terrainSignatures().identity());
GenerationHistory reopened = GenerationHistory.open(world);
TransitionGenerationPlan restored = reopened.transitionPlan(3L);
assertNotSame(second, restored);
assertSame(restored, reopened.transitionPlan(3L));
assertEquals(second.specification(), restored.specification());
assertFalse(restored.hasTransitionAtChunk(1, 0));
}
@Test @Test
public void promotionIsIdempotentForEmptyAndRecoveredWorlds() throws Exception { public void promotionIsIdempotentForEmptyAndRecoveredWorlds() throws Exception {
Path emptyWorld = temporaryFolder.newFolder("empty-world").toPath(); Path emptyWorld = temporaryFolder.newFolder("empty-world").toPath();
@@ -137,7 +137,7 @@ public final class GenerationSemanticQueriesTest {
IrisComplex complex = mock(IrisComplex.class); IrisComplex complex = mock(IrisComplex.class);
IrisHydrologyRuntime runtime = mock(IrisHydrologyRuntime.class); IrisHydrologyRuntime runtime = mock(IrisHydrologyRuntime.class);
IrisDimension dimension = mock(IrisDimension.class); IrisDimension dimension = mock(IrisDimension.class);
TransitionGenerationPlan transition = mock(TransitionGenerationPlan.class); TransitionDisplacementField displacement = mock(TransitionDisplacementField.class);
GenerationHistory history = mock(GenerationHistory.class); GenerationHistory history = mock(GenerationHistory.class);
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class); GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
GenerationActivation activation = mock(GenerationActivation.class); GenerationActivation activation = mock(GenerationActivation.class);
@@ -159,9 +159,9 @@ public final class GenerationSemanticQueriesTest {
when(engine.getDimension()).thenReturn(dimension); when(engine.getDimension()).thenReturn(dimension);
when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router)); when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
when(complex.getHydrologyRuntime()).thenReturn(runtime); when(complex.getHydrologyRuntime()).thenReturn(runtime);
when(complex.getTransitionGenerationPlan()).thenReturn(transition); when(complex.getTransitionDisplacement()).thenReturn(displacement);
when(complex.allowsNewGenerationChunk(anyInt(), anyInt())).thenReturn(true); when(complex.allowsNewGenerationChunk(anyInt(), anyInt())).thenReturn(true);
when(transition.hydrologyWeightAt(anyInt(), anyInt())).thenAnswer(invocation -> when(displacement.seamWeight(anyInt(), anyInt())).thenAnswer(invocation ->
invocation.<Integer>getArgument(0) == tapered.x() ? 0.5D : 1D); invocation.<Integer>getArgument(0) == tapered.x() ? 0.5D : 1D);
when(dimension.getMinHeight()).thenReturn(-64); when(dimension.getMinHeight()).thenReturn(-64);
when(router.history()).thenReturn(history); when(router.history()).thenReturn(history);
@@ -92,7 +92,9 @@ public final class GenerationUnpublishedHistoryTest extends GenerationHistorySup
} }
private static void assertBiomeAppendForces(SavedBiomeStore store, Path root, int x, int count) throws Exception { private static void assertBiomeAppendForces(SavedBiomeStore store, Path root, int x, int count) throws Exception {
try (RandomAccessFile actual = new RandomAccessFile(region(root).toFile(), "rw")) { try (MockedStatic<Durability> durability = mockStatic(Durability.class, CALLS_REAL_METHODS);
RandomAccessFile actual = new RandomAccessFile(region(root).toFile(), "rw")) {
durability.when(Durability::enabled).thenReturn(true);
FileChannel channel = mock(FileChannel.class, delegatesTo(actual.getChannel())); FileChannel channel = mock(FileChannel.class, delegatesTo(actual.getChannel()));
try (MockedConstruction<RandomAccessFile> ignored = mockConstruction(RandomAccessFile.class, try (MockedConstruction<RandomAccessFile> ignored = mockConstruction(RandomAccessFile.class,
withSettings().defaultAnswer(delegatesTo(actual)), withSettings().defaultAnswer(delegatesTo(actual)),
@@ -0,0 +1,114 @@
package art.arcane.iris.world.history;
import art.arcane.iris.generation.mantle.ObjectContinuationBundle;
import art.arcane.volmlib.util.matter.IrisMatter;
import org.junit.Test;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class HistoricalObjectContinuationsTest {
@Test
public void keepsOnlySavedOriginPlacementsAndCachesRegionReads() throws Exception {
GenerationHistory history = mock(GenerationHistory.class);
GenerationManifest manifest = mock(GenerationManifest.class);
GenerationActivation first = activation(1L);
GenerationActivation second = activation(2L);
GenerationActivation third = activation(3L);
when(history.manifest()).thenReturn(manifest);
when(manifest.activations()).thenReturn(List.of(first, second, third));
when(history.resolveActivation(0, 0)).thenReturn(first);
TransitionGenerationPlan plan = plan(3L);
ObjectContinuationBundle.Fragment crossing = fragment(0, List.of(chunk(0), chunk(1)));
ObjectContinuationBundle.Fragment unrelated = fragment(1, List.of(chunk(1), chunk(2)));
AtomicInteger reads = new AtomicInteger();
HistoricalObjectContinuations continuations = new HistoricalObjectContinuations(history, (activation, x, z) -> {
reads.incrementAndGet();
return Map.of(ChunkGenerationOwnership.packChunk(1, 0),
new ObjectContinuationBundle(List.of(crossing, unrelated)));
});
assertEquals(List.of(new GenerationHistoryRuntimeRouter.ObjectContinuation(1L, crossing)), continuations.at(plan, 1, 0));
assertEquals(List.of(new GenerationHistoryRuntimeRouter.ObjectContinuation(1L, crossing)), continuations.at(plan, 1, 0));
assertEquals(2, reads.get());
assertTrue(continuations.at(plan, 0, 0).isEmpty());
assertFalse(continuations.allowsFootprint(plan, 17, 1, 18, 2));
assertTrue(continuations.allowsFootprint(plan, 24, 1, 25, 2));
assertEquals(2, reads.get());
}
@Test
public void originalPendingFragmentSurvivesAnIntermediateActivation() throws Exception {
GenerationHistory history = mock(GenerationHistory.class);
GenerationManifest manifest = mock(GenerationManifest.class);
GenerationActivation first = activation(1L);
GenerationActivation second = activation(2L);
GenerationActivation third = activation(3L);
when(history.manifest()).thenReturn(manifest);
when(manifest.activations()).thenReturn(List.of(first, second, third));
when(history.resolveActivation(0, 0)).thenReturn(first);
ObjectContinuationBundle.Fragment crossing = fragment(0, List.of(chunk(0), chunk(1), chunk(2)));
HistoricalObjectContinuations continuations = new HistoricalObjectContinuations(history, (activation, x, z) ->
activation.activationId() == 1L
? Map.of(ChunkGenerationOwnership.packChunk(2, 0), new ObjectContinuationBundle(List.of(crossing)))
: Map.of());
assertEquals(List.of(new GenerationHistoryRuntimeRouter.ObjectContinuation(1L, crossing)), continuations.at(plan(2L), 2, 0));
assertEquals(List.of(new GenerationHistoryRuntimeRouter.ObjectContinuation(1L, crossing)), continuations.at(plan(3L), 2, 0));
}
@Test
public void continuationReplayUsesSourceOrderInsteadOfConcurrentPersistenceOrder() throws Exception {
GenerationHistory history = mock(GenerationHistory.class);
GenerationManifest manifest = mock(GenerationManifest.class);
GenerationActivation first = activation(1L);
when(history.manifest()).thenReturn(manifest);
GenerationActivation second = activation(2L);
when(manifest.activations()).thenReturn(List.of(first, second));
when(history.resolveActivation(0, 0)).thenReturn(first);
ObjectContinuationBundle.Fragment earlier = fragment(0, List.of(chunk(0), chunk(1)));
ObjectContinuationBundle.Fragment later = fragment(1, List.of(chunk(0), chunk(1)));
ObjectContinuationBundle.Fragment structure = new ObjectContinuationBundle.Fragment(
new ObjectContinuationBundle.PlacementKey(ObjectContinuationBundle.Kind.STRUCTURE, -1, 0, 0),
earlier.bounds(), earlier.touchedChunks(), earlier.payload());
HistoricalObjectContinuations continuations = new HistoricalObjectContinuations(history, (activation, x, z) ->
Map.of(ChunkGenerationOwnership.packChunk(1, 0), new ObjectContinuationBundle(List.of(structure, later, earlier))));
assertEquals(List.of(new GenerationHistoryRuntimeRouter.ObjectContinuation(1L, earlier),
new GenerationHistoryRuntimeRouter.ObjectContinuation(1L, later),
new GenerationHistoryRuntimeRouter.ObjectContinuation(1L, structure)), continuations.at(plan(2L), 1, 0));
}
private static GenerationActivation activation(long id) {
GenerationActivation activation = mock(GenerationActivation.class);
when(activation.activationId()).thenReturn(id);
return activation;
}
private static TransitionGenerationPlan plan(long id) {
TransitionGenerationPlan plan = mock(TransitionGenerationPlan.class);
when(plan.activationId()).thenReturn(id);
when(plan.boundary()).thenReturn(GenerationBoundary.freeze("frontier", List.of(new GenerationBoundary.ChunkCoordinate(0, 0))));
return plan;
}
private static ObjectContinuationBundle.ChunkPosition chunk(int x) {
return new ObjectContinuationBundle.ChunkPosition(x, 0);
}
private static ObjectContinuationBundle.Fragment fragment(int ordinal, List<ObjectContinuationBundle.ChunkPosition> touched) throws IOException {
ByteArrayOutputStream payload = new ByteArrayOutputStream();
new IrisMatter(16, 64, 16).write(payload);
return new ObjectContinuationBundle.Fragment(new ObjectContinuationBundle.PlacementKey(ObjectContinuationBundle.Kind.BIOME, 0, 0, ordinal),
new ObjectContinuationBundle.Bounds(15, 1, 20, 3), touched, payload.toByteArray());
}
}
@@ -1,5 +1,6 @@
package art.arcane.iris.world.history; package art.arcane.iris.world.history;
import art.arcane.iris.world.storage.Durability;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.TemporaryFolder; import org.junit.rules.TemporaryFolder;
@@ -57,6 +58,35 @@ public class SavedBiomeStoreWriteTest {
assertArrayEquals(expectedRegion(), Files.readAllBytes(regionPath(root))); assertArrayEquals(expectedRegion(), Files.readAllBytes(regionPath(root)));
} }
@Test
public void relaxedDurabilityPreservesRecordsWithoutForcingEachAppend() throws Exception {
String previousMode = System.getProperty(Durability.MODE_PROPERTY);
Path root = temporaryFolder.newFolder().toPath();
SavedBiomeStore store = SavedBiomeStore.open(root);
store.claimAndPersist(chunk(0, 3L));
try {
System.setProperty(Durability.MODE_PROPERTY, "relaxed");
try (RandomAccessFile actual = new RandomAccessFile(regionPath(root).toFile(), "rw")) {
FileChannel channel = mock(FileChannel.class, delegatesTo(actual.getChannel()));
try (MockedConstruction<RandomAccessFile> ignored = mockConstruction(RandomAccessFile.class,
withSettings().defaultAnswer(delegatesTo(actual)),
(file, context) -> doReturn(channel).when(file).getChannel())) {
assertTrue(store.claimAndPersist(chunk(1, 3L)));
verify(channel, never()).force(true);
verify(channel).write(any(ByteBuffer[].class), anyInt(), anyInt());
}
}
assertEquals(chunk(0, 3L), SavedBiomeStore.open(root).get(0, 0).orElseThrow());
assertEquals(chunk(1, 3L), SavedBiomeStore.open(root).get(1, 0).orElseThrow());
} finally {
if (previousMode == null) {
System.clearProperty(Durability.MODE_PROPERTY);
} else {
System.setProperty(Durability.MODE_PROPERTY, previousMode);
}
}
}
@Test @Test
public void encodingAllowsOtherReadsInTheSameRegion() throws Exception { public void encodingAllowsOtherReadsInTheSameRegion() throws Exception {
SavedBiomeStore store = SavedBiomeStore.open(temporaryFolder.newFolder().toPath()); SavedBiomeStore store = SavedBiomeStore.open(temporaryFolder.newFolder().toPath());
@@ -9,6 +9,7 @@ import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.file.FileAlreadyExistsException; import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.DirectoryStream;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Arrays; import java.util.Arrays;
import java.util.ArrayList; import java.util.ArrayList;
@@ -30,6 +31,49 @@ public class TerrainBoundarySignatureStoreTest {
@Rule @Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder(); public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void loadsOnlyRequestedShardsAndValidatesTheirBytesBeforeUse() throws Exception {
TerrainBoundarySignatureStore store = new TerrainBoundarySignatureStore(
temporaryFolder.newFolder("lazy-shards").toPath());
TerrainBoundarySignature first = simpleSignature(1, 2, 70);
store.publish(1L, List.of(first));
Path damaged;
try (DirectoryStream<Path> shards = Files.newDirectoryStream(store.directory(), "terrain-cell-*.irtm")) {
damaged = shards.iterator().next();
}
store.publish(2L, List.of(first, simpleSignature(2048, 2, 90)));
byte[] original = Files.readAllBytes(damaged);
Files.write(damaged, Arrays.copyOf(original, original.length - 1));
TerrainBoundarySignatureStore.Snapshot loaded = store.load(2L);
assertEquals(0L, loaded.shardLoadCount());
assertEquals(0, loaded.cachedShardCount());
assertEquals(90, loaded.signatureAt(2048, 2).orElseThrow().surfaceHeight());
assertEquals(1L, loaded.shardLoadCount());
IllegalStateException failure = assertThrows(IllegalStateException.class,
() -> loaded.signatureAt(1, 2));
assertTrue(failure.getCause() instanceof IOException);
assertEquals(1, loaded.cachedShardCount());
Files.write(damaged, original);
assertEquals(70, loaded.signatureAt(1, 2).orElseThrow().surfaceHeight());
assertEquals(2L, loaded.shardLoadCount());
}
@Test
public void missingShardFailsWhenItsTerrainIsRequested() throws Exception {
TerrainBoundarySignatureStore store = new TerrainBoundarySignatureStore(
temporaryFolder.newFolder("missing-shard").toPath());
store.publish(1L, List.of(simpleSignature(-1, -1, 70)));
try (DirectoryStream<Path> shards = Files.newDirectoryStream(store.directory(), "terrain-cell-*.irtm")) {
Files.delete(shards.iterator().next());
}
TerrainBoundarySignatureStore.Snapshot loaded = store.load(1L);
assertTrue(loaded.signatureAt(2048, 2048).isEmpty());
assertThrows(IllegalStateException.class, () -> loaded.signatureAt(-1, -1));
assertEquals(0, loaded.cachedShardCount());
}
@Test @Test
public void preservesPresentAndAbsentUpperCeilingsAcrossShardReloads() throws Exception { public void preservesPresentAndAbsentUpperCeilingsAcrossShardReloads() throws Exception {
TerrainBoundarySignatureStore store = new TerrainBoundarySignatureStore( TerrainBoundarySignatureStore store = new TerrainBoundarySignatureStore(
@@ -0,0 +1,199 @@
package art.arcane.iris.world.history;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
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.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
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 TransitionBandIndexTest extends GenerationHistorySupport {
@Test
public void membershipMatchesExposedColumnsAcrossNegativeCoordinatesAndRegionEdges() throws Exception {
List<GenerationBoundary.ChunkCoordinate> chunks = List.of(
new GenerationBoundary.ChunkCoordinate(-33, -33),
new GenerationBoundary.ChunkCoordinate(-1, -1),
new GenerationBoundary.ChunkCoordinate(0, 0),
new GenerationBoundary.ChunkCoordinate(31, 31),
new GenerationBoundary.ChunkCoordinate(32, 31));
GenerationBoundary boundary = GenerationBoundary.freeze("boundary", chunks);
List<GenerationBoundary.BlockColumn> columns = boundary.exposedBlockColumns();
List<TerrainBoundarySignature> signatures = new ArrayList<>(columns.size());
for (GenerationBoundary.BlockColumn column : columns) {
signatures.add(signature(column.blockX(), column.blockZ()));
}
TerrainBoundarySignatureStore store = new TerrainBoundarySignatureStore(
temporaryFolder.newFolder("membership").toPath());
TerrainBoundarySignatureStore.Snapshot snapshot = store.publish(2L, signatures);
for (int width : new int[]{1, 2, 16, 17, 32, 256}) {
TransitionBandIndex index = new TransitionBandIndex(boundary, new TransitionBoundarySampler(width, snapshot));
for (GenerationBoundary.ChunkCoordinate chunk : chunks) {
for (int x = chunk.chunkX() - 4; x <= chunk.chunkX() + 4; x++) {
for (int z = chunk.chunkZ() - 4; z <= chunk.chunkZ() + 4; z++) {
assertEquals(expected(boundary, columns, x, z, width), index.contains(x, z));
}
}
}
}
}
@Test
public void repeatedMembershipReusesPositiveNegativeAndHistoricalResults() {
GenerationBoundary boundary = mock(GenerationBoundary.class);
TerrainBoundarySignatureStore.Snapshot snapshot = mock(TerrainBoundarySignatureStore.Snapshot.class);
when(boundary.isHistoricalChunk(0, 0)).thenReturn(true);
when(snapshot.intersectsTerrainBand(bounds(1, 0), 32)).thenReturn(true);
TransitionBandIndex index = new TransitionBandIndex(boundary, new TransitionBoundarySampler(32, snapshot));
for (int query = 0; query < 10_000; query++) {
assertFalse(index.contains(0, 0));
assertTrue(index.contains(1, 0));
assertFalse(index.contains(2, 0));
}
verify(boundary, times(1)).isHistoricalChunk(0, 0);
verify(boundary, times(1)).isHistoricalChunk(1, 0);
verify(boundary, times(1)).isHistoricalChunk(2, 0);
verify(snapshot, times(2)).intersectsTerrainBand(any(), eq(32));
}
@Test
public void concurrentQueriesShareOneColdEvaluation() throws Exception {
TerrainBoundarySignatureStore.Snapshot snapshot = mock(TerrainBoundarySignatureStore.Snapshot.class);
when(snapshot.intersectsTerrainBand(any(), eq(32))).thenReturn(true);
TransitionBandIndex index = new TransitionBandIndex(GenerationBoundary.freeze("empty", List.of()),
new TransitionBoundarySampler(32, snapshot));
ExecutorService workers = Executors.newFixedThreadPool(8);
CyclicBarrier start = new CyclicBarrier(8);
List<Future<Boolean>> queries = new ArrayList<>();
try {
for (int worker = 0; worker < 8; worker++) {
queries.add(workers.submit(() -> {
start.await(5, TimeUnit.SECONDS);
return index.contains(-1, -1);
}));
}
for (Future<Boolean> query : queries) {
assertTrue(query.get(5, TimeUnit.SECONDS));
}
verify(snapshot, times(1)).intersectsTerrainBand(bounds(-1, -1), 32);
} finally {
workers.shutdownNow();
assertTrue(workers.awaitTermination(5, TimeUnit.SECONDS));
}
}
@Test
public void cachedQueriesDoNotWaitForAColdQueryInTheSameRegion() throws Exception {
TerrainBoundarySignatureStore.Snapshot snapshot = mock(TerrainBoundarySignatureStore.Snapshot.class);
CountDownLatch scanning = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
when(snapshot.intersectsTerrainBand(bounds(0, 0), 32)).thenReturn(true);
when(snapshot.intersectsTerrainBand(bounds(1, 0), 32)).thenAnswer(invocation -> {
scanning.countDown();
assertTrue(release.await(5, TimeUnit.SECONDS));
return false;
});
TransitionBandIndex index = new TransitionBandIndex(GenerationBoundary.freeze("empty", List.of()),
new TransitionBoundarySampler(32, snapshot));
assertTrue(index.contains(0, 0));
ExecutorService workers = Executors.newFixedThreadPool(2);
try {
Future<Boolean> cold = workers.submit(() -> index.contains(1, 0));
assertTrue(scanning.await(5, TimeUnit.SECONDS));
Future<Boolean> warm = workers.submit(() -> index.contains(0, 0));
assertTrue(warm.get(2, TimeUnit.SECONDS));
release.countDown();
assertFalse(cold.get(5, TimeUnit.SECONDS));
} finally {
release.countDown();
workers.shutdownNow();
assertTrue(workers.awaitTermination(5, TimeUnit.SECONDS));
}
}
@Test
public void failedMembershipCanBeRetried() {
TerrainBoundarySignatureStore.Snapshot snapshot = mock(TerrainBoundarySignatureStore.Snapshot.class);
when(snapshot.intersectsTerrainBand(bounds(0, 0), 32))
.thenThrow(new IllegalStateException("Unavailable boundary shard"))
.thenReturn(true);
TransitionBandIndex index = new TransitionBandIndex(GenerationBoundary.freeze("empty", List.of()),
new TransitionBoundarySampler(32, snapshot));
assertThrows(IllegalStateException.class, () -> index.contains(0, 0));
assertTrue(index.contains(0, 0));
assertTrue(index.contains(0, 0));
verify(snapshot, times(2)).intersectsTerrainBand(bounds(0, 0), 32);
}
@Test
public void negativeRegionChurnKeepsMemoryBoundedAndResultsIndependent() {
TerrainBoundarySignatureStore.Snapshot snapshot = mock(TerrainBoundarySignatureStore.Snapshot.class);
when(snapshot.intersectsTerrainBand(any(), anyInt()))
.thenAnswer(invocation -> ((TerrainBoundarySignatureStore.BlockBounds) invocation.getArgument(0))
.minimumX() < 0L);
TransitionBandIndex index = new TransitionBandIndex(GenerationBoundary.freeze("empty", List.of()),
new TransitionBoundarySampler(32, snapshot));
for (int region = 0; region < 10_000; region++) {
assertFalse(index.contains(region * 32, region * 32));
assertTrue(index.contains(-region * 32 - 1, -region * 32 - 1));
}
assertTrue(index.cachedRegionCount() <= 1_024);
assertFalse(index.contains(0, 0));
assertTrue(index.contains(-1, -1));
}
@Test
public void historicalCoordinatesKeepTheirOriginalOverflowBehavior() {
TerrainBoundarySignatureStore.Snapshot snapshot = mock(TerrainBoundarySignatureStore.Snapshot.class);
GenerationBoundary boundary = GenerationBoundary.freeze("extreme", List.of(
new GenerationBoundary.ChunkCoordinate(Integer.MAX_VALUE, Integer.MIN_VALUE)));
TransitionBandIndex index = new TransitionBandIndex(boundary, new TransitionBoundarySampler(32, snapshot));
assertFalse(index.contains(Integer.MAX_VALUE, Integer.MIN_VALUE));
assertThrows(ArithmeticException.class, () -> index.contains(Integer.MAX_VALUE, 0));
assertThrows(ArithmeticException.class, () -> index.contains(Integer.MAX_VALUE, 0));
verifyNoInteractions(snapshot);
}
private static TerrainBoundarySignatureStore.BlockBounds bounds(int chunkX, int chunkZ) {
return new TerrainBoundarySignatureStore.BlockBounds(chunkX * 16, chunkZ * 16,
chunkX * 16 + 15, chunkZ * 16 + 15);
}
private static boolean expected(GenerationBoundary boundary, List<GenerationBoundary.BlockColumn> columns,
int chunkX, int chunkZ, int width) {
if (boundary.isHistoricalChunk(chunkX, chunkZ)) {
return false;
}
int minimumX = chunkX * 16;
int minimumZ = chunkZ * 16;
for (GenerationBoundary.BlockColumn column : columns) {
long dx = Math.max(0L, Math.max((long) minimumX - column.blockX(), column.blockX() - minimumX - 15L));
long dz = Math.max(0L, Math.max((long) minimumZ - column.blockZ(), column.blockZ() - minimumZ - 15L));
if (dx * dx + dz * dz < (long) width * width) {
return true;
}
}
return false;
}
}
@@ -0,0 +1,134 @@
package art.arcane.iris.world.history;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class TransitionDisplacementFieldTest extends GenerationHistorySupport {
private static final BoundaryColumnGeometry.Voxel STONE = new BoundaryColumnGeometry.Voxel(
"minecraft:stone", BoundaryColumnGeometry.Phase.SOLID, "", false);
private static final BoundaryColumnGeometry.Voxel AIR = new BoundaryColumnGeometry.Voxel(
"minecraft:air", BoundaryColumnGeometry.Phase.AIR, "", false);
private static final BoundaryColumnGeometry.Voxel WATER = new BoundaryColumnGeometry.Voxel(
"minecraft:water[level=0]", BoundaryColumnGeometry.Phase.FLUID, "minecraft:water[level=0]", false);
@Test
public void matchesFlatSeamAndReturnsToNativeTerrainInBothDirections() throws Exception {
for (int historical : new int[]{24, 104}) {
int nativeHeight = historical == 24 ? 104 : 24;
TransitionDisplacementField field = field(historical, -1, (x, z) -> nativeHeight);
assertEquals(historical, field.height(0, 8, nativeHeight), 0.0000001D);
double previous = historical;
for (int x = 1; x < 32; x++) {
double height = field.height(x, 8, nativeHeight);
assertTrue(historical < nativeHeight ? height >= previous - 0.0000001D : height <= previous + 0.0000001D);
previous = height;
}
assertEquals(nativeHeight, field.height(31, 8, nativeHeight), 0D);
assertEquals(nativeHeight, field.height(64, 8, nativeHeight), 0D);
assertEquals(nativeHeight, field.height(-1, 8, nativeHeight), 0D);
}
}
@Test
public void preservesNewReliefAtFullAmplitudeInsideTheBand() throws Exception {
TransitionDisplacementField field = field(72, -1, (x, z) -> 104D);
for (int x = 0; x < 32; x++) {
double relief = 4D * Math.sin(x / 2D);
assertEquals(relief, field.height(x, 8, 104D + relief) - field.height(x, 8, 104D), 0.0000001D);
}
}
@Test
public void unchangedSurfaceDoesNotMoveAndChangedWaterHasNoMissingFluidSentinel() throws Exception {
TransitionDisplacementField field = field(24, 40, (x, z) -> 24D);
double previous = 40D;
for (int x = 0; x < 32; x++) {
assertEquals(24D, field.height(x, 8, 24D), 0D);
double fluid = field.fluidHeight(x, 8, 48D);
assertTrue(fluid >= previous - 0.0000001D);
assertTrue(fluid >= 40D && fluid <= 48D);
previous = fluid;
}
assertEquals(40D, field.fluidHeight(0, 8, 48D), 0.0000001D);
assertEquals(48D, field.fluidHeight(31, 8, 48D), 0D);
assertEquals("minecraft:water[level=0]", field.fluidStateKey(0, 8));
assertNull(field.fluidStateKey(31, 8));
}
@Test
public void dryHighGroundDoesNotRaiseTheReplacementSeaLevel() throws Exception {
TransitionDisplacementField field = field(104, -1, (x, z) -> 24D);
for (int x = 0; x < 32; x++) {
assertEquals(40D, field.fluidHeight(x, 8, 40D), 0.0000001D);
assertNull(field.fluidStateKey(x, 8));
}
}
@Test
public void coldConcurrentQueriesShareTheSameFieldAndWarmQueriesDoNotResampleAnchors() throws Exception {
AtomicInteger samples = new AtomicInteger();
TransitionDisplacementField field = field(72, -1, (x, z) -> {
samples.incrementAndGet();
return 104D;
});
ExecutorService workers = Executors.newFixedThreadPool(4);
CyclicBarrier start = new CyclicBarrier(4);
List<Future<Double>> heights = new ArrayList<>();
try {
for (int index = 0; index < 4; index++) {
heights.add(workers.submit(() -> {
start.await(5, TimeUnit.SECONDS);
return field.height(0, 8, 104D);
}));
}
for (Future<Double> height : heights) {
assertEquals(72D, height.get(5, TimeUnit.SECONDS), 0.0000001D);
}
int coldSamples = samples.get();
assertTrue(coldSamples > 0);
for (int x = 0; x < 16; x++) {
field.height(x, 8, 104D);
}
assertEquals(coldSamples, samples.get());
} finally {
workers.shutdownNow();
assertTrue(workers.awaitTermination(5, TimeUnit.SECONDS));
}
}
private TransitionDisplacementField field(int ground, int fluid, TransitionDisplacementField.HeightSource source)
throws Exception {
GenerationBoundary boundary = GenerationBoundary.freeze("boundary", List.of(
new GenerationBoundary.ChunkCoordinate(-1, 0)));
SavedTerrainChunk terrain = SavedTerrainChunk.captureBoundary(-1, 0, 0, 128, "minecraft:noise",
new SavedTerrainChunk.VoxelSource() {
@Override
public BoundaryColumnGeometry.Voxel voxel(int x, int y, int z) {
return y <= ground ? STONE : y <= fluid ? WATER : AIR;
}
@Override
public String biome(int x, int y, int z) {
return "minecraft:plains";
}
});
TerrainBoundarySignatureStore store = new TerrainBoundarySignatureStore(temporaryFolder.newFolder().toPath());
TerrainBoundarySignatureStore.Snapshot snapshot = store.publish(2L, boundary, terrain::column);
TransitionGenerationPlan plan = new TransitionGenerationPlan(new TransitionGenerationPlan.Specification(
2L, "old", "new", GenerationTransition.CURRENT_ALGORITHM_VERSION, 32, boundary.identity(), snapshot.identity()),
boundary, snapshot);
return new TransitionDisplacementField(plan, new TransitionDisplacementField.Sources(source, 128));
}
}
@@ -1,166 +0,0 @@
package art.arcane.iris.world.history;
import org.junit.Test;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public final class TransitionFluidContainmentTest {
private static final BoundaryColumnGeometry.Voxel AIR = voxel("minecraft:air", BoundaryColumnGeometry.Phase.AIR);
private static final BoundaryColumnGeometry.Voxel BANK = voxel("minecraft:deepslate", BoundaryColumnGeometry.Phase.SOLID);
private static final BoundaryColumnGeometry.Voxel WATER = voxel("minecraft:water[level=0]", BoundaryColumnGeometry.Phase.FLUID);
@Test
public void fluidBanksUseTheSameRawHaloAcrossChunkEdgesAndRequestOrder() throws Exception {
TransitionGenerationPlan plan = band();
SavedTerrainChunk left = raw(0, 0);
SavedTerrainChunk right = raw(1, 0);
TransitionFluidContainment.ColumnSource source = (x, z) -> {
List<BoundaryColumnGeometry.Voxel> voxels = new ArrayList<>(16);
for (int y = 0; y < 16; y++) {
voxels.add(y == 0 ? BANK : x < 16 && y <= 4 ? WATER : AIR);
}
return BoundaryColumnGeometry.fromVoxels(0, voxels);
};
SavedTerrainChunk leftFirst = TransitionFluidContainment.contain(left, plan, source);
SavedTerrainChunk rightSecond = TransitionFluidContainment.contain(right, plan, source);
SavedTerrainChunk rightFirst = TransitionFluidContainment.contain(right, plan, source);
SavedTerrainChunk leftSecond = TransitionFluidContainment.contain(left, plan, source);
assertEquals(BANK, leftFirst.column(15, 8).geometry().voxelAt(2));
assertEquals(WATER, leftFirst.column(14, 8).geometry().voxelAt(2));
assertEquals(AIR, rightSecond.column(16, 8).geometry().voxelAt(2));
assertEquals(leftFirst.column(15, 8).geometry(), leftSecond.column(15, 8).geometry());
assertEquals(rightSecond.column(16, 8).geometry(), rightFirst.column(16, 8).geometry());
assertEquals(WATER, left.column(15, 8).geometry().voxelAt(2));
}
@Test
public void unsupportedFluidGetsABottomWhileOpenFluidSurfacesStayOpen() throws Exception {
SavedTerrainChunk floating = SavedTerrainChunk.capture(0, 0, 0, 16, "minecraft:noise", new SavedTerrainChunk.VoxelSource() {
@Override
public BoundaryColumnGeometry.Voxel voxel(int x, int y, int z) {
return y >= 3 && y <= 5 ? WATER : AIR;
}
@Override
public String biome(int x, int y, int z) {
return "minecraft:plains";
}
});
SavedTerrainChunk result = TransitionFluidContainment.contain(floating, band(),
(x, z) -> floating.column(Math.floorMod(x, 16), Math.floorMod(z, 16)).geometry());
assertEquals("minecraft:stone", result.column(8, 8).geometry().voxelAt(3).stateKey());
assertEquals(WATER, result.column(8, 8).geometry().voxelAt(4));
assertEquals(WATER, result.column(8, 8).geometry().voxelAt(5));
assertEquals(AIR, result.column(8, 8).geometry().voxelAt(6));
}
@Test
public void historicalAndOutsideBandColumnsStayUnchanged() throws Exception {
SavedTerrainChunk raw = raw(0, 0);
TransitionGenerationPlan historical = band();
when(historical.isHistoricalBlock(anyInt(), anyInt())).thenReturn(true);
TransitionGenerationPlan outside = band();
when(outside.newEpochWeightAt(anyInt(), anyInt())).thenReturn(1D);
TransitionFluidContainment.ColumnSource forbidden = (x, z) -> {
throw new AssertionError("Immutable columns must not need a halo");
};
assertSame(raw, TransitionFluidContainment.contain(raw, historical, forbidden));
assertSame(raw, TransitionFluidContainment.contain(raw, outside, forbidden));
}
@Test
public void missingHaloFailsInsteadOfTreatingUnknownTerrainAsAir() throws Exception {
assertThrows(IOException.class, () -> TransitionFluidContainment.contain(raw(0, 0), band(),
(x, z) -> { throw new IOException("Missing saved boundary column"); }));
}
@Test
public void protectedFluidAndWaterloggedStatesArePreserved() throws Exception {
BoundaryColumnGeometry.Voxel protectedWater = new BoundaryColumnGeometry.Voxel(
WATER.stateKey(), WATER.phase(), WATER.fluidStateKey(), true);
BoundaryColumnGeometry.Voxel waterlogged = new BoundaryColumnGeometry.Voxel(
"minecraft:oak_slab[type=bottom,waterlogged=true]", BoundaryColumnGeometry.Phase.SOLID,
"minecraft:water[level=0]", false);
TransitionFluidContainment.ColumnSource forbidden = (x, z) -> {
throw new AssertionError("Protected or waterlogged blocks must not be replaced");
};
for (BoundaryColumnGeometry.Voxel voxel : new BoundaryColumnGeometry.Voxel[]{protectedWater, waterlogged}) {
SavedTerrainChunk raw = uniform(voxel);
assertSame(raw, TransitionFluidContainment.contain(raw, band(), forbidden));
}
}
@Test
public void unsupportedFluidCannotUseFallingSandAsItsBank() throws Exception {
BoundaryColumnGeometry.Voxel sand = voxel("minecraft:sand", BoundaryColumnGeometry.Phase.SOLID);
SavedTerrainChunk raw = SavedTerrainChunk.capture(0, 0, 0, 16, "minecraft:noise", new SavedTerrainChunk.VoxelSource() {
@Override
public BoundaryColumnGeometry.Voxel voxel(int x, int y, int z) {
return y == 0 ? sand : y == 2 ? WATER : AIR;
}
@Override
public String biome(int x, int y, int z) {
return "minecraft:plains";
}
});
SavedTerrainChunk corrected = TransitionFluidContainment.contain(raw, band(),
(x, z) -> raw.column(Math.floorMod(x, 16), Math.floorMod(z, 16)).geometry());
assertEquals("minecraft:stone", corrected.column(8, 8).geometry().voxelAt(2).stateKey());
assertEquals(sand, corrected.column(8, 8).geometry().voxelAt(0));
}
private static SavedTerrainChunk uniform(BoundaryColumnGeometry.Voxel voxel) throws IOException {
return SavedTerrainChunk.capture(0, 0, 0, 16, "minecraft:noise", new SavedTerrainChunk.VoxelSource() {
@Override
public BoundaryColumnGeometry.Voxel voxel(int x, int y, int z) {
return voxel;
}
@Override
public String biome(int x, int y, int z) {
return "minecraft:plains";
}
});
}
private static TransitionGenerationPlan band() {
TransitionGenerationPlan plan = mock(TransitionGenerationPlan.class);
when(plan.newEpochWeightAt(anyInt(), anyInt())).thenReturn(0.5D);
return plan;
}
private static SavedTerrainChunk raw(int chunkX, int chunkZ) throws IOException {
return SavedTerrainChunk.capture(chunkX, chunkZ, 0, 16, "minecraft:noise", new SavedTerrainChunk.VoxelSource() {
@Override
public BoundaryColumnGeometry.Voxel voxel(int x, int y, int z) {
if (y == 0) {
return BANK;
}
return chunkX * 16 + x < 16 && y <= 4 ? WATER : AIR;
}
@Override
public String biome(int x, int y, int z) {
return "minecraft:plains";
}
});
}
private static BoundaryColumnGeometry.Voxel voxel(String key, BoundaryColumnGeometry.Phase phase) {
return new BoundaryColumnGeometry.Voxel(key, phase, phase == BoundaryColumnGeometry.Phase.FLUID ? key : "", false);
}
}
@@ -175,7 +175,7 @@ public class TransitionGenerationPlanTest {
} }
@Test @Test
public void exposesTheNearestFrozenPhysicalBiomeUntilDiscreteHandoff() throws Exception { public void exposesTheNearestFrozenPhysicalBiomeOnlyInsideTheSeamPatch() throws Exception {
GenerationBoundary boundary = GenerationBoundary.freeze("biomes", List.of( GenerationBoundary boundary = GenerationBoundary.freeze("biomes", List.of(
new GenerationBoundary.ChunkCoordinate(0, 0) new GenerationBoundary.ChunkCoordinate(0, 0)
)); ));
@@ -193,15 +193,13 @@ public class TransitionGenerationPlanTest {
assertEquals("iris:middle", plan.historicalPhysicalBiomeKeyAt(16, 14, 8).orElseThrow()); assertEquals("iris:middle", plan.historicalPhysicalBiomeKeyAt(16, 14, 8).orElseThrow());
assertEquals("iris:high", plan.historicalPhysicalBiomeKeyAt(16, 18, 8).orElseThrow()); assertEquals("iris:high", plan.historicalPhysicalBiomeKeyAt(16, 18, 8).orElseThrow());
assertTrue(plan.historicalPhysicalBiomeKeyAt(19, 14, 8).isEmpty());
assertTrue(plan.historicalPhysicalBiomeKeyAt(31, 14, 8).isEmpty()); assertTrue(plan.historicalPhysicalBiomeKeyAt(31, 14, 8).isEmpty());
for (int blockX = -2; blockX <= 34; blockX++) { for (int blockX = -2; blockX <= 34; blockX++) {
for (int blockZ : new int[]{-2, 8, 17}) { for (int blockZ : new int[]{-2, 8, 17}) {
TransitionGenerationPlan.TerrainSample sample = plan.terrainSampleAt(blockX, blockZ); TransitionGenerationPlan.TerrainSample sample = plan.terrainSampleAt(blockX, blockZ);
for (int blockY = -16; blockY <= 48; blockY++) { for (int blockY = -16; blockY <= 48; blockY++) {
double weight = GenerationBlend.newEpochWeight( Optional<String> expected = sample.distanceToHistoricalTerrain() < 4D
Math.max(0D, sample.distanceToHistoricalTerrain() - 1D), Math.max(1, plan.widthBlocks() - 1));
Optional<String> expected = sample.newEpochWeight() != 1D
&& GenerationBlend.usesHistoricalMaterial(blockX, blockY, blockZ, weight)
? sample.historicalPhysicalBiomeKeyAt(blockY) : Optional.empty(); ? sample.historicalPhysicalBiomeKeyAt(blockY) : Optional.empty();
assertEquals(expected, plan.historicalPhysicalBiomeKeyAt(blockX, blockY, blockZ, sample)); assertEquals(expected, plan.historicalPhysicalBiomeKeyAt(blockX, blockY, blockZ, sample));
assertEquals(expected, plan.historicalPhysicalBiomeKeyAt(blockX, blockY, blockZ)); assertEquals(expected, plan.historicalPhysicalBiomeKeyAt(blockX, blockY, blockZ));
@@ -38,7 +38,7 @@ public class TransitionGeometryBlenderTest {
} }
@Test @Test
public void narrowHistoricalCaveContinuesBeforeTaperingIntoTallSolidTerrain() throws Exception { public void narrowHistoricalCaveIsPatchedOnlyAtTheSeam() throws Exception {
ArrayList<BoundaryColumnGeometry.Voxel> oldVoxels = new ArrayList<>(768); ArrayList<BoundaryColumnGeometry.Voxel> oldVoxels = new ArrayList<>(768);
for (int offset = 0; offset < 768; offset++) { for (int offset = 0; offset < 768; offset++) {
oldVoxels.add(offset == 384 ? AIR : STONE); oldVoxels.add(offset == 384 ? AIR : STONE);
@@ -47,9 +47,8 @@ public class TransitionGeometryBlenderTest {
BoundaryColumnGeometry current = filled(STONE, 768); BoundaryColumnGeometry current = filled(STONE, 768);
TransitionGenerationPlan plan = plan(List.of(signature(15, 0, old))); TransitionGenerationPlan plan = plan(List.of(signature(15, 0, old)));
for (int x = 16; x <= 23; x++) { assertEquals(AIR, TransitionGeometryBlender.blendColumn(plan, 16, 0, current).voxelAt(380));
assertEquals(AIR, TransitionGeometryBlender.blendColumn(plan, x, 0, current).voxelAt(380)); assertSame(current, TransitionGeometryBlender.blendColumn(plan, 19, 0, current));
}
assertSame(current, TransitionGeometryBlender.blendColumn(plan, 47, 0, current)); assertSame(current, TransitionGeometryBlender.blendColumn(plan, 47, 0, current));
} }
@@ -74,7 +73,7 @@ public class TransitionGeometryBlenderTest {
} }
@Test @Test
public void conflictingFluidFamiliesProduceANewSideSolidPlug() throws Exception { public void conflictingFluidFamiliesKeepTheNativeFluidWithoutSolidPlugs() throws Exception {
BoundaryColumnGeometry.Voxel lava = new BoundaryColumnGeometry.Voxel("minecraft:lava[level=0]", BoundaryColumnGeometry.Voxel lava = new BoundaryColumnGeometry.Voxel("minecraft:lava[level=0]",
BoundaryColumnGeometry.Phase.FLUID, "minecraft:lava[level=0]", false); BoundaryColumnGeometry.Phase.FLUID, "minecraft:lava[level=0]", false);
BoundaryColumnGeometry current = filled(lava, 8); BoundaryColumnGeometry current = filled(lava, 8);
@@ -83,12 +82,23 @@ public class TransitionGeometryBlenderTest {
BoundaryColumnGeometry blended = TransitionGeometryBlender.blendColumn(plan, 16, 0, current); BoundaryColumnGeometry blended = TransitionGeometryBlender.blendColumn(plan, 16, 0, current);
for (BoundaryColumnGeometry.Voxel voxel : blended.voxels()) { for (BoundaryColumnGeometry.Voxel voxel : blended.voxels()) {
assertEquals(BoundaryColumnGeometry.Phase.SOLID, voxel.phase()); assertEquals(lava, voxel);
assertEquals("minecraft:obsidian", voxel.stateKey());
} }
assertSame(current, TransitionGeometryBlender.blendColumn(plan, 47, 0, current)); assertSame(current, TransitionGeometryBlender.blendColumn(plan, 47, 0, current));
} }
@Test
public void seamPatchKeepsTheNativeSolidPaletteWithoutDithering() throws Exception {
BoundaryColumnGeometry.Voxel deepslate = new BoundaryColumnGeometry.Voxel(
"minecraft:deepslate", BoundaryColumnGeometry.Phase.SOLID, "", false);
BoundaryColumnGeometry current = filled(deepslate, 8);
TransitionGenerationPlan plan = plan(List.of(signature(15, 0, filled(STONE, 8))));
for (int x = 16; x < 32; x++) {
assertSame(current, TransitionGeometryBlender.blendColumn(plan, x, 0, current));
}
}
@Test @Test
public void differentLevelsOfTheSameFluidDoNotCreateSolidPlugs() throws Exception { public void differentLevelsOfTheSameFluidDoNotCreateSolidPlugs() throws Exception {
BoundaryColumnGeometry.Voxel flowing = new BoundaryColumnGeometry.Voxel("minecraft:water[level=3]", BoundaryColumnGeometry.Voxel flowing = new BoundaryColumnGeometry.Voxel("minecraft:water[level=3]",
@@ -96,7 +106,7 @@ public class TransitionGeometryBlenderTest {
BoundaryColumnGeometry current = filled(flowing, 8); BoundaryColumnGeometry current = filled(flowing, 8);
TransitionGenerationPlan plan = plan(List.of(signature(15, 0, filled(WATER, 8)))); TransitionGenerationPlan plan = plan(List.of(signature(15, 0, filled(WATER, 8))));
assertEquals(WATER, TransitionGeometryBlender.blendColumn(plan, 16, 0, current).voxelAt(0)); assertEquals(flowing, TransitionGeometryBlender.blendColumn(plan, 16, 0, current).voxelAt(0));
} }
@Test @Test
@@ -105,13 +115,13 @@ public class TransitionGeometryBlenderTest {
List.of(STONE, STONE, AIR, AIR, STONE, AIR, AIR, AIR)); List.of(STONE, STONE, AIR, AIR, STONE, AIR, AIR, AIR));
BoundaryColumnGeometry current = filled(STONE, 8); BoundaryColumnGeometry current = filled(STONE, 8);
TransitionGenerationPlan plan = plan(List.of(signature(15, 0, old), signature(0, 15, old))); TransitionGenerationPlan plan = plan(List.of(signature(15, 0, old), signature(0, 15, old)));
BoundaryColumnGeometry before = TransitionGeometryBlender.blendColumn(plan, 25, 6, current); BoundaryColumnGeometry before = TransitionGeometryBlender.blendColumn(plan, 16, 1, current);
for (int x = 46; x >= 16; x--) { for (int x = 46; x >= 16; x--) {
TransitionGeometryBlender.blendColumn(plan, x, 0, current); TransitionGeometryBlender.blendColumn(plan, x, 0, current);
} }
assertEquals(before, TransitionGeometryBlender.blendColumn(plan, 25, 6, current)); assertEquals(before, TransitionGeometryBlender.blendColumn(plan, 16, 1, current));
assertSame(current, TransitionGeometryBlender.blendColumn(plan, 80, 0, current)); assertSame(current, TransitionGeometryBlender.blendColumn(plan, 80, 0, current));
} }
@@ -121,8 +131,8 @@ public class TransitionGeometryBlenderTest {
List.of(STONE, STONE, AIR, AIR, AIR, AIR, AIR, AIR)); List.of(STONE, STONE, AIR, AIR, AIR, AIR, AIR, AIR));
BoundaryColumnGeometry upper = BoundaryColumnGeometry.fromVoxels(-4, BoundaryColumnGeometry upper = BoundaryColumnGeometry.fromVoxels(-4,
List.of(AIR, AIR, AIR, AIR, STONE, STONE, AIR, AIR)); List.of(AIR, AIR, AIR, AIR, STONE, STONE, AIR, AIR));
TerrainBoundarySignature west = signature(15, 0, lower); TerrainBoundarySignature west = signature(15, 15, lower);
TerrainBoundarySignature south = signature(0, 15, upper); TerrainBoundarySignature south = signature(14, 15, upper);
TransitionGenerationPlan first = plan(List.of(west, south)); TransitionGenerationPlan first = plan(List.of(west, south));
TransitionGenerationPlan second = plan(List.of(south, west)); TransitionGenerationPlan second = plan(List.of(south, west));
BoundaryColumnGeometry current = filled(STONE, 8); BoundaryColumnGeometry current = filled(STONE, 8);
@@ -59,7 +59,7 @@ public class TransitionGeometryCursorTest {
} }
@Test @Test
public void matchesOpeningCutoffAndProtectedVoxelsInsideOpenings() { public void matchesOpeningHeightsAndProtectedVoxelsInsideOpenings() {
Random random = new Random(1234); Random random = new Random(1234);
for (int opening : new int[]{1, 63, 64, 65, 66}) { for (int opening : new int[]{1, 63, 64, 65, 66}) {
for (boolean protectedMiddle : new boolean[]{false, true}) { for (boolean protectedMiddle : new boolean[]{false, true}) {
+26 -1
View File
@@ -37,6 +37,7 @@ dependencies {
runtimeOnly(libs.fastutil) runtimeOnly(libs.fastutil)
runtimeOnly(libs.caffeine) runtimeOnly(libs.caffeine)
runtimeOnly(libs.lz4) runtimeOnly(libs.lz4)
runtimeOnly('com.fulcrumgenomics:jlibdeflate:0.2.0')
runtimeOnly(libs.zip) runtimeOnly(libs.zip)
runtimeOnly(libs.guava) runtimeOnly(libs.guava)
runtimeOnly(libs.oshi) runtimeOnly(libs.oshi)
@@ -291,7 +292,31 @@ tasks.register('riverTransectProbe', JavaExec) {
} }
} }
['hydrologyProbe', 'genProbe', 'hydrologyPackProbe', 'generationOrderProbe', 'riverTransectProbe', 'terrain3DProbe', 'hydrologyBlockProbe'].each { String taskName -> tasks.register('transitionProbe', JavaExec) {
group = 'verification'
description = 'Exercises a bounded two-pack Studio cutover, transition routing, receipts and semantics with a vanilla-biome pack.'
mainClass = 'art.arcane.iris.probe.TransitionProbe'
workingDir = rootProject.projectDir
classpath = sourceSets.main.runtimeClasspath
jvmArgs('--add-modules', 'jdk.incubator.vector')
timeout = java.time.Duration.ofMinutes(12)
doFirst {
Provider<String> replacement = providers.gradleProperty('probeReplacementPack')
if (!probePack.isPresent() || !replacement.isPresent() || !probeDimension.isPresent() || !probeOutput.isPresent()) {
throw new GradleException('transitionProbe requires probePack, probeReplacementPack, probeDimension and probeOutput')
}
java.nio.file.Path output = new File(probeOutput.get()).canonicalFile.toPath()
if (output.startsWith(rootProject.projectDir.canonicalFile.toPath())) {
throw new GradleException('probeOutput must point outside the repository')
}
args(probePack.get(), replacement.get(), probeDimension.get(), probeSeed.getOrElse('1337'),
providers.gradleProperty('probeTransitionWidth').getOrElse('32'),
providers.gradleProperty('probeTransitionChunks').getOrElse('2'), output.toString(),
providers.gradleProperty('probeTransitionReverse').getOrElse('false'))
}
}
['transitionProbe', 'hydrologyProbe', 'genProbe', 'hydrologyPackProbe', 'generationOrderProbe', 'riverTransectProbe', 'terrain3DProbe', 'hydrologyBlockProbe'].each { String taskName ->
tasks.named(taskName, JavaExec).configure { JavaExec task -> tasks.named(taskName, JavaExec).configure { JavaExec task ->
classpath += configurations.studioProbeApi classpath += configurations.studioProbeApi
doFirst { doFirst {
@@ -206,7 +206,10 @@ public final class StubPlatform implements IrisPlatform {
@Override @Override
public boolean isStorageChest() { public boolean isStorageChest() {
return false; return switch (blockKey()) {
case "minecraft:chest", "minecraft:trapped_chest", "minecraft:barrel" -> true;
default -> false;
};
} }
@Override @Override
@@ -0,0 +1,405 @@
package art.arcane.iris.probe;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.world.history.ChunkGenerationOwnership;
import art.arcane.iris.world.history.ChunkGenerationSemantics;
import art.arcane.iris.world.history.GenerationActivation;
import art.arcane.iris.world.history.GenerationEpochContractFactory;
import art.arcane.iris.world.history.GenerationHistory;
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.world.history.GenerationPackFingerprint;
import art.arcane.iris.world.history.GenerationRegistryContract;
import art.arcane.iris.world.history.NativeTerrainReceipt;
import art.arcane.iris.world.history.SavedTerrainChunk;
import art.arcane.iris.world.history.SavedBiomeChunk;
import art.arcane.iris.world.history.TerrainBoundarySignature;
import art.arcane.iris.world.history.TransitionGenerationPlan;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.nativelib.terrain.NativeTerrainReceiptStorage;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.HexFormat;
import java.nio.file.Files;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitOption;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.Path;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.CompletableFuture;
public final class TransitionProbe {
private TransitionProbe() {
}
public static void main(String[] arguments) throws Exception {
Configuration configuration = Configuration.parse(arguments);
Files.createDirectory(configuration.output());
long deadline = System.nanoTime() + Duration.ofMinutes(10).toNanos();
Bindings bindings = new Bindings();
Evidence evidence = new Evidence(
new ArrayList<>(List.of("phase,chunk_x,chunk_z,activation,band,elapsed_ns,receipt_bytes,blocks_sha256,biomes_sha256,receipt_sha256,metadata_sha256")),
new ArrayList<>(List.of("phase,x,z,ground_y,fluid_y,solid_spans,fluid_gaps")),
new ArrayList<>(List.of("phase,x,z,minimum_y,maximum_y_exclusive,block")),
new ArrayList<>(List.of("phase,chunk_x,chunk_z,block,count")));
try (RealPackProbeSupport.Workspace workspace = RealPackProbeSupport.openWorkspace(
new RealPackProbeSupport.WorkspaceOptions(configuration.packA().toFile(), configuration.dimension(),
"[transition-probe]", bindings, Path.of(System.getProperty("java.io.tmpdir"))));
RealPackProbeSupport.EngineSession session = workspace.openEngine(configuration.seed(), true, "transition")) {
IrisEngine engine = (IrisEngine) session.engine();
GenerationHistoryRuntimeRouter router = engine.getGenerationHistoryRuntimeRouter().orElseThrow();
GenerationHistory history = router.history();
long originalActivation = history.activeActivation().activationId();
for (int x = 0; x < 2; x++) {
for (int z = 0; z < 2; z++) {
generate(new GenerationRequest(engine, bindings, x, z, "original", false, deadline), evidence);
}
}
Path world = engine.getWorld().worldFolder().toPath();
Map<Long, byte[]> historicalReceipts = new HashMap<>();
for (int x = 0; x < 2; x++) {
for (int z = 0; z < 2; z++) {
historicalReceipts.put(ChunkGenerationOwnership.packChunk(x, z), SavedTerrainChunk.readReceipt(world, x, z));
}
}
Path replacementPack = workspace.pack().toPath().getParent().resolve("replacement-pack");
copyPack(configuration.packB(), replacementPack);
SavedBiomeChunk historicalBiomes = router.biomes().snapshot(0, 0).orElseThrow();
IrisData replacement = IrisData.openRuntime(replacementPack.toFile());
try {
IrisDimension dimension = replacement.getDimensionLoader().load(configuration.dimension());
if (dimension == null) {
throw new IOException("Replacement dimension did not load");
}
requireVanillaBiomes(replacement, dimension);
history.stageUpdate(replacementPack, GenerationPackFingerprint.compute(replacementPack,
GenerationPackFingerprint.CURRENT_VERSION),
GenerationEpochContractFactory.create(dimension, configuration.dimension(), "iris:transition_probe"),
GenerationRegistryContract.empty(), configuration.width());
} finally {
replacement.close();
}
try (GenerationHistoryRuntimeRouter.StudioCutover cutover = router.beginStudioCutover(30_000)) {
GenerationActivation activated = cutover.promotePending();
require(activated.activationId() != originalActivation, "Pack update did not create another activation");
}
require(historicalBiomes.equals(router.biomes().snapshot(0, 0).orElseThrow()),
"Historical biome data changed during promotion");
require(history.resolveActivation(0, 0).activationId() == originalActivation,
"Historical chunk ownership changed");
require(history.semantics(0, 0).orElseThrow().activationId() == originalActivation,
"Historical semantic ownership changed");
for (int sequence = 0; sequence < configuration.chunks(); sequence++) {
int index = configuration.reverse() ? configuration.chunks() - 1 - sequence : sequence;
generate(new GenerationRequest(engine, bindings, 2 + index, 0, "transition", true, deadline), evidence);
generate(new GenerationRequest(engine, bindings, 2 + configuration.width() / 16 + index, 0,
"distant", false, deadline), evidence);
}
for (int x = 0; x < 2; x++) {
for (int z = 0; z < 2; z++) {
require(Arrays.equals(historicalReceipts.get(ChunkGenerationOwnership.packChunk(x, z)),
SavedTerrainChunk.readReceipt(world, x, z)), "Historical disk receipt changed after generation");
}
}
require(historicalBiomes.equals(router.biomes().snapshot(0, 0).orElseThrow()),
"Historical biome data changed after generation");
for (Throwable failure : RealPackProbeSupport.settleAndDrain()) {
throw new IllegalStateException("Generation reported a failure", failure);
}
require(bindings.captures > 0, "Promotion did not capture historical terrain");
Files.write(configuration.output().resolve("chunks.csv"), evidence.chunks());
Files.write(configuration.output().resolve("transect.csv"), evidence.transect());
Files.write(configuration.output().resolve("sections.csv"), evidence.sections());
Files.write(configuration.output().resolve("materials.csv"), evidence.materials());
System.out.println("[transition-probe] PASS generated=" + bindings.terrain.size()
+ " boundaryChunkCaptures=" + bindings.captures
+ " timing=diagnostic-only platform=stub cutover=studio output=" + configuration.output());
}
}
private static void generate(GenerationRequest request, Evidence evidence) throws Exception {
require(System.nanoTime() < request.deadline(), "Probe exceeded ten-minute workload deadline");
long key = ChunkGenerationOwnership.packChunk(request.x(), request.z());
require(!request.bindings().terrain.containsKey(key), "Probe scheduled a duplicate chunk");
GenerationHistoryRuntimeRouter router = request.engine().getGenerationHistoryRuntimeRouter().orElseThrow();
try (GenerationHistoryRuntimeRouter.RuntimeRoute route = router.openRoute(request.x(), request.z());
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope scope = route.openRuntimeScope()) {
TransitionGenerationPlan plan = route.transitionPlan();
boolean band = plan != null && plan.hasTransitionAtChunk(request.x(), request.z());
require(band == request.band(), "Unexpected transition membership at " + request.x() + "," + request.z());
long started = System.nanoTime();
RealPackProbeSupport.GeneratedChunk generated = RealPackProbeSupport.generateChunk(
request.engine(), request.x(), request.z());
require(route.claimGeneratedSemantics((x, y, z) -> {
NativeBlockState state = generated.blocks().getRaw(x & 15, y, z & 15);
return state == null || state.isAir();
}), "Generated semantic claim was rejected");
SavedTerrainChunk terrain = route.naturalTerrain().orElseThrow();
byte[] receipt = NativeTerrainReceipt.encode(terrain, route.activation().activationId(), route.epoch().epochId());
NativeTerrainReceipt.Decoded decoded = NativeTerrainReceipt.decode(receipt, "minecraft:full");
require(decoded.activationId() == route.activation().activationId()
&& decoded.epochId().equals(route.epoch().epochId()), "Receipt changed generation ownership");
for (int localX = 0; localX < 16; localX++) {
for (int localZ = 0; localZ < 16; localZ++) {
if (!terrain.hasColumn(localX, localZ)) {
continue;
}
TerrainBoundarySignature original = terrain.column(request.x() * 16 + localX, request.z() * 16 + localZ);
TerrainBoundarySignature restored = decoded.terrain().column(request.x() * 16 + localX, request.z() * 16 + localZ);
require(original.column().equals(restored.column()) && original.geometry().equals(restored.geometry()),
"Receipt changed captured terrain");
}
}
request.bindings().terrain.put(key, terrain);
writeReceipt(request.engine().getWorld().worldFolder().toPath(), request.x(), request.z(), receipt);
require(router.history().semantics(request.x(), request.z()).orElseThrow().activationId()
== route.activation().activationId(), "Generated semantics lost activation ownership");
require(router.biomes().snapshot(request.x(), request.z()).orElseThrow().activationId()
== route.activation().activationId(), "Generated saved biomes lost activation ownership");
long elapsed = System.nanoTime() - started;
GenerationOrderProbe.ChunkHash hash = GenerationOrderProbe.hashChunk(
new GenerationOrderProbe.ChunkCoordinate(request.x(), request.z()),
generated.blocks(), generated.biomes(), generated.height());
String metadataHash = metadataHash(router.biomes().snapshot(request.x(), request.z()).orElseThrow(),
router.history().semantics(request.x(), request.z()).orElseThrow());
for (Map.Entry<String, Integer> material : new TreeMap<>(hash.blockCounts()).entrySet()) {
evidence.materials().add(request.phase() + "," + request.x() + "," + request.z() + ","
+ material.getKey() + "," + material.getValue());
}
evidence.chunks().add(request.phase() + "," + request.x() + "," + request.z() + ","
+ route.activation().activationId() + "," + band + "," + elapsed + "," + receipt.length
+ "," + hash.blocks() + "," + hash.biomes() + ","
+ HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(receipt)) + "," + metadataHash);
if (request.z() == 0) {
for (int x = 0; x < 16; x++) {
int ground = -1;
int fluid = -1;
int solidSpans = 0;
boolean solidBelow = false;
String stateBelow = null;
int spanStart = 0;
for (int y = 0; y < generated.height(); y++) {
NativeBlockState state = generated.blocks().getRaw(x, y, 8);
String stateKey = state == null ? "minecraft:air" : state.key();
if (!stateKey.equals(stateBelow)) {
if (stateBelow != null) {
evidence.sections().add(request.phase() + "," + (request.x() * 16 + x) + ",8,"
+ (spanStart + request.engine().getMinHeight()) + ","
+ (y + request.engine().getMinHeight()) + "," + stateBelow);
}
stateBelow = stateKey;
spanStart = y;
}
boolean solid = state != null && !state.isAir() && !isFluid(state);
if (solid && !solidBelow) {
solidSpans++;
}
solidBelow = solid;
if (state != null && !state.isAir()) {
if (isFluid(state)) {
fluid = y + request.engine().getMinHeight();
} else {
ground = y + request.engine().getMinHeight();
}
}
}
evidence.sections().add(request.phase() + "," + (request.x() * 16 + x) + ",8,"
+ (spanStart + request.engine().getMinHeight()) + ","
+ (generated.height() + request.engine().getMinHeight()) + "," + stateBelow);
int fluidGaps = 0;
for (int y = ground + 1; y < fluid; y++) {
NativeBlockState state = generated.blocks().getRaw(x, y - request.engine().getMinHeight(), 8);
if (state == null || state.isAir()) {
fluidGaps++;
}
}
evidence.transect().add(request.phase() + "," + (request.x() * 16 + x) + ",8," + ground + "," + fluid
+ "," + solidSpans + "," + fluidGaps);
}
}
}
}
private static boolean isFluid(NativeBlockState state) {
return state.isWater() || state.key().startsWith("minecraft:lava");
}
private static String metadataHash(SavedBiomeChunk biomes, ChunkGenerationSemantics semantics) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
digest.update(biomes.column(x, z).toString().getBytes(StandardCharsets.UTF_8));
}
}
for (Collection<?> values : List.of(semantics.surfaceBiomeKeys(), semantics.caveBiomeKeys(),
semantics.regionKeys(), semantics.riverProfileKeys(), semantics.objectKeys(), semantics.riverFeatures(),
semantics.pointsOfInterest(), semantics.structures())) {
digest.update(values.stream().map(Object::toString).sorted().toList().toString().getBytes(StandardCharsets.UTF_8));
}
return HexFormat.of().formatHex(digest.digest());
}
static void writeReceipt(Path world, int chunkX, int chunkZ, byte[] receipt) throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (DataOutputStream nbt = new DataOutputStream(bytes)) {
nbt.writeByte(10);
nbt.writeUTF("");
nbt.writeByte(3);
nbt.writeUTF("xPos");
nbt.writeInt(chunkX);
nbt.writeByte(3);
nbt.writeUTF("zPos");
nbt.writeInt(chunkZ);
nbt.writeByte(8);
nbt.writeUTF("Status");
nbt.writeUTF("minecraft:full");
nbt.writeByte(7);
nbt.writeUTF(NativeTerrainReceiptStorage.NBT_KEY);
nbt.writeInt(receipt.length);
nbt.write(receipt);
nbt.writeByte(0);
}
Path region = Files.createDirectories(world.resolve("region")).resolve(
"r." + (chunkX >> 5) + "." + (chunkZ >> 5) + ".mca");
byte[] payload = bytes.toByteArray();
int sectors = (payload.length + 5 + 4095) / 4096;
require(sectors <= 255, "Receipt exceeds MCA sector limit");
try (RandomAccessFile output = new RandomAccessFile(region.toFile(), "rw")) {
if (output.length() == 0) {
output.setLength(8192);
}
int offset = Math.toIntExact((output.length() + 4095) / 4096);
output.setLength((long) (offset + sectors) * 4096);
output.seek((long) offset * 4096);
output.writeInt(payload.length + 1);
output.writeByte(3);
output.write(payload);
output.seek((long) ((chunkX & 31) + (chunkZ & 31) * 32) * 4);
output.writeInt(offset << 8 | sectors);
}
}
private static void copyPack(Path source, Path destination) throws IOException {
Files.walkFileTree(source, EnumSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE,
new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path directory, BasicFileAttributes attributes) throws IOException {
Files.createDirectory(destination.resolve(source.relativize(directory)));
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) throws IOException {
Files.copy(file, destination.resolve(source.relativize(file)));
return FileVisitResult.CONTINUE;
}
});
}
private static void requireVanillaBiomes(IrisData data, IrisDimension dimension) {
require(!dimension.getAllBiomes(() -> data).stream().anyMatch(biome -> biome.isCustom()),
"Transition probe requires vanilla physical biomes");
}
private static void require(boolean condition, String message) {
if (!condition) {
throw new IllegalStateException(message);
}
}
record Configuration(Path packA, Path packB, String dimension, long seed, int width, int chunks, Path output, boolean reverse) {
static Configuration parse(String[] arguments) {
if (arguments.length != 7 && arguments.length != 8) {
throw new IllegalArgumentException("Expected packA packB dimension seed width chunks output [reverse]");
}
int width = Integer.parseInt(arguments[4]);
int chunks = Integer.parseInt(arguments[5]);
if (width < 16 || width > 128 || width % 16 != 0 || chunks < 1 || chunks > 8 || chunks * 16 > width) {
throw new IllegalArgumentException("Width must be 16..128 in multiples of 16; chunks must be 1..8 within width");
}
Path packA = Path.of(arguments[0]).toAbsolutePath().normalize();
Path packB = Path.of(arguments[1]).toAbsolutePath().normalize();
if (!Files.isDirectory(packA) || !Files.isDirectory(packB)) {
throw new IllegalArgumentException("Both source packs must be existing directories");
}
Path output = Path.of(arguments[6]).toAbsolutePath().normalize();
if (Files.exists(output) || !Files.isDirectory(output.getParent())) {
throw new IllegalArgumentException("Output must be a new directory beneath an existing parent");
}
if (arguments[2].isBlank()) {
throw new IllegalArgumentException("Dimension cannot be blank");
}
return new Configuration(packA, packB, arguments[2], Long.parseLong(arguments[3]),
width, chunks, output, arguments.length == 8
&& RealPackProbeSupport.parseBoolean(arguments[7], "reverse"));
}
}
private record Evidence(List<String> chunks, List<String> transect, List<String> sections, List<String> materials) {
}
private record GenerationRequest(IrisEngine engine, Bindings bindings, int x, int z, String phase,
boolean band, long deadline) {
}
private static final class Bindings implements RealPackProbeSupport.RuntimeBindings {
private final Map<Long, SavedTerrainChunk> terrain = new HashMap<>();
private int captures;
@Override
public IrisPlatform create(File platformRoot) {
return new StubPlatform(platformRoot);
}
@Override
public void prepare(IrisData data, IrisDimension dimension) {
requireVanillaBiomes(data, dimension);
}
@Override
public GenerationHistory createHistory(RealPackProbeSupport.HistoryRequest request) throws IOException {
Files.createDirectories(request.worldRoot());
Path pack = request.data().getDataFolder().toPath();
return GenerationHistory.create(request.worldRoot(), pack,
GenerationPackFingerprint.compute(pack, GenerationPackFingerprint.CURRENT_VERSION), request.seed(),
GenerationEpochContractFactory.create(request.dimension(), request.dimension().getLoadKey(),
"iris:transition_probe"), GenerationRegistryContract.empty());
}
@Override
public CompletableFuture<SavedTerrainChunk> captureSavedTerrainChunk(Engine engine, int chunkX, int chunkZ) {
captures++;
SavedTerrainChunk chunk = terrain.get(ChunkGenerationOwnership.packChunk(chunkX, chunkZ));
return chunk == null ? CompletableFuture.failedFuture(new IOException("Missing generated historical chunk"))
: CompletableFuture.completedFuture(chunk);
}
@Override
public CompletableFuture<Void> flushSavedTerrainCapture(Engine engine) {
return CompletableFuture.completedFuture(null);
}
@Override
public void close() {
terrain.clear();
}
}
}
@@ -0,0 +1,414 @@
package art.arcane.iris.probe;
import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.EngineTarget;
import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.structure.placement.StructurePlacementMarker;
import art.arcane.iris.testsupport.IrisRuntimeState;
import art.arcane.iris.world.IrisWorld;
import art.arcane.iris.world.history.GenerationEpochContractFactory;
import art.arcane.iris.world.history.GenerationHistory;
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.world.history.GenerationPackFingerprint;
import art.arcane.iris.world.history.GenerationRegistryContract;
import art.arcane.iris.world.history.IrisBoundarySignatureSampler;
import art.arcane.iris.world.history.NativeTerrainReceipt;
import art.arcane.iris.world.history.SavedTerrainChunk;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.matter.Matter;
import org.junit.AfterClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Stream;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
public final class ObjectContinuationProbeTest {
private static final long SEED = 1338;
private static final Fixture TREE = new Fixture("boundary-tree", "trees/boundary", Set.of("minecraft:oak_log", "minecraft:oak_leaves"), true);
private static final Fixture STRUCTURE = new Fixture("boundary-structure", "buildings/boundary", Set.of("minecraft:bricks", "minecraft:oak_planks", "minecraft:chest"), false);
@Rule
public TemporaryFolder temporary = new TemporaryFolder();
@AfterClass
public static void resetRuntime() {
IrisRuntimeState.reset();
}
@Test(timeout = 120_000)
public void removedTreeCompletesOnlyItsPublishedPlacementsAfterRestart() throws Exception {
verifyCompletion(false, TREE);
}
@Test(timeout = 120_000)
public void originalTreeSurvivesTwoUpdatesBeforeItsTailGenerates() throws Exception {
verifyCompletion(true, TREE);
}
@Test(timeout = 120_000)
public void removedStructureCompletesItsCarveAndFoundationAfterRestart() throws Exception {
verifyCompletion(false, STRUCTURE);
}
private void verifyCompletion(boolean secondUpdate, Fixture fixture) throws Exception {
Map<Position, ObjectVoxel> controlOld;
Map<Position, ObjectVoxel> controlNew;
GeneratedSnapshot nativeSource;
GeneratedSnapshot nativeDestination;
try (RealPackProbeSupport.Workspace workspace = RealPackProbeSupport.openWorkspace(
fixture(fixture.pack()).toFile(), "transition", "[object-control]");
RealPackProbeSupport.EngineSession session = workspace.openEngine(SEED, true, "control")) {
nativeSource = generatedObjects(session.engine(), 0, fixture);
controlOld = nativeSource.objects();
nativeDestination = generatedObjects(session.engine(), 1, fixture);
controlNew = nativeDestination.objects();
}
Set<String> historicalPlacements = new HashSet<>();
for (ObjectVoxel voxel : controlOld.values()) {
historicalPlacements.add(voxel.marker());
}
Map<Position, ObjectVoxel> expectedTail = new HashMap<>();
Map<Position, ObjectVoxel> unrelated = new HashMap<>();
for (Map.Entry<Position, ObjectVoxel> entry : controlNew.entrySet()) {
(historicalPlacements.contains(entry.getValue().marker()) ? expectedTail : unrelated)
.put(entry.getKey(), entry.getValue());
}
assertFalse("Fixture must have old object blocks in the saved chunk", controlOld.isEmpty());
assertFalse("Fixture must have an old placement crossing into the next chunk", expectedTail.isEmpty());
if (fixture.tree()) {
assertFalse("Fixture must include old placements wholly inside the new area", unrelated.isEmpty());
Map<String, Position> centers = new HashMap<>();
Map<Position, ObjectVoxel> control = new HashMap<>(controlOld);
control.putAll(controlNew);
int trunkY = control.keySet().stream().mapToInt(Position::y).min().orElseThrow();
for (Map.Entry<Position, ObjectVoxel> entry : control.entrySet()) {
if (entry.getKey().y() == trunkY) {
centers.put(entry.getValue().marker(), entry.getKey());
}
}
boolean crossingFromNewSource = false;
boolean unrelatedFromNewSource = false;
for (Map.Entry<String, Position> entry : centers.entrySet()) {
if (entry.getValue().x() >= 16 && entry.getValue().x() < 32) {
crossingFromNewSource |= historicalPlacements.contains(entry.getKey());
unrelatedFromNewSource |= !historicalPlacements.contains(entry.getKey());
}
}
assertTrue("Fixture must distinguish two placements from the same new source chunk: " + centers,
crossingFromNewSource && unrelatedFromNewSource);
} else {
assertTrue("Structure fixture must place foundations", nativeDestination.foundations() > 0);
assertTrue("Structure fixture must carve solid ground", nativeDestination.carvedGround() > 0);
}
Bindings bindings = new Bindings();
try (RealPackProbeSupport.Workspace workspace = RealPackProbeSupport.openWorkspace(
new RealPackProbeSupport.WorkspaceOptions(fixture(fixture.pack()).toFile(), "transition",
"[object-update]", bindings, temporary.getRoot().toPath()))) {
IrisWorld world;
byte[] oldReceipt;
long originalActivation;
try (RealPackProbeSupport.EngineSession session = workspace.openEngine(SEED, true, "original")) {
IrisEngine engine = (IrisEngine) session.engine();
GenerationHistoryRuntimeRouter router = engine.getGenerationHistoryRuntimeRouter().orElseThrow();
originalActivation = router.history().activeActivation().activationId();
assertEquals(controlOld, generateClaimed(engine, bindings, 0, fixture).objects());
world = engine.getWorld();
oldReceipt = SavedTerrainChunk.readReceipt(world.worldFolder().toPath(), 0, 0);
Path replacement = workspace.pack().toPath().getParent().resolve("replacement");
copyPack(fixture("lowland"), replacement);
IrisData replacementData = IrisData.openRuntime(replacement.toFile());
try {
IrisDimension dimension = replacementData.getDimensionLoader().load("transition");
assertNotNull(dimension);
router.history().stageUpdate(replacement,
GenerationPackFingerprint.compute(replacement, GenerationPackFingerprint.CURRENT_VERSION),
GenerationEpochContractFactory.create(dimension, "transition", "iris:object_probe"),
GenerationRegistryContract.empty(), 32);
} finally {
replacementData.close();
}
try (GenerationHistoryRuntimeRouter.StudioCutover cutover = router.beginStudioCutover(30_000)) {
assertTrue(cutover.promotePending().activationId() > originalActivation);
}
}
GenerationHistory reopened = GenerationHistory.open(world.worldFolder().toPath(), SEED);
if (secondUpdate) {
Path third = workspace.pack().toPath().getParent().resolve("third-pack");
copyPack(fixture("mountain"), third);
IrisData thirdData = IrisData.openRuntime(third.toFile());
try {
IrisDimension dimension = thirdData.getDimensionLoader().load("transition");
assertNotNull(dimension);
reopened.stageUpdate(third,
GenerationPackFingerprint.compute(third, GenerationPackFingerprint.CURRENT_VERSION),
GenerationEpochContractFactory.create(dimension, "transition", "iris:object_probe"),
GenerationRegistryContract.empty(), 32);
} finally {
thirdData.close();
}
reopened.prepareCurrentGenerator(32);
reopened = GenerationHistory.open(world.worldFolder().toPath(), SEED);
assertTrue(reopened.activeActivation().activationId() > originalActivation + 1);
}
IrisData data = IrisData.openRuntime(reopened.activePackRoot().toFile());
try {
data.bindGenerationRegistryContract(reopened.activeEpoch().registryContract());
IrisDimension dimension = data.getDimensionLoader().load("transition");
assertNotNull(dimension);
IrisEngine engine = new IrisEngine(new EngineTarget(world, dimension, data),
IrisEngine.InitializationMode.RUNTIME,
reopened.paths().activationMantleRoot(reopened.activeActivation().activationId()),
reopened.activeEpoch().kernelVersion(), reopened.transitionPlan(reopened.activeActivation().activationId()));
try {
GenerationHistoryRuntimeRouter router = GenerationHistoryRuntimeRouter.attach(
engine, reopened, IrisBoundarySignatureSampler.INSTANCE);
router.preloadActiveRuntimes();
assertEquals("Old object blocks and metadata must reload before neighboring generation",
controlOld, historicalObjects(engine, fixture));
assertHistoricalEdits(engine, nativeSource.edits());
GeneratedSnapshot result = generateClaimed(engine, bindings, 1, fixture);
Map<Position, ObjectVoxel> completed = result.objects();
if (!fixture.tree()) {
for (Map.Entry<Position, String> edit : nativeDestination.edits().entrySet()) {
assertEquals("Structure carve/foundation at " + edit.getKey(), edit.getValue(),
result.blocks().get(edit.getKey()));
}
assertTrue("Completed storage blocks must retain structure-aware placement markers",
completed.values().stream().anyMatch(voxel -> voxel.marker() != null));
}
assertEquals("Complete only placements with a published old portion", expectedTail, completed);
for (Map.Entry<Position, ObjectVoxel> entry : unrelated.entrySet()) {
assertFalse("Unrelated old placement was imported at " + entry.getKey(),
completed.containsKey(entry.getKey()));
}
assertEquals("Old geometry and metadata must survive neighboring generation",
controlOld, historicalObjects(engine, fixture));
assertHistoricalEdits(engine, nativeSource.edits());
assertArrayEquals(oldReceipt, SavedTerrainChunk.readReceipt(world.worldFolder().toPath(), 0, 0));
assertEquals(originalActivation, reopened.resolveActivation(0, 0).activationId());
assertTrue(reopened.semantics(1, 0).orElseThrow().objectKeys().contains(fixture.objectKey()));
assertTrue("Generation must not report swallowed errors", RealPackProbeSupport.settleAndDrain().isEmpty());
} finally {
engine.close();
}
} finally {
data.close();
}
}
}
private static GeneratedSnapshot generateClaimed(IrisEngine engine, Bindings bindings, int chunkX, Fixture fixture)
throws Exception {
GenerationHistoryRuntimeRouter router = engine.getGenerationHistoryRuntimeRouter().orElseThrow();
try (GenerationHistoryRuntimeRouter.RuntimeRoute route = router.openRoute(chunkX, 0);
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope scope = route.openRuntimeScope()) {
GeneratedSnapshot result = generatedObjects(engine, chunkX, fixture);
assertTrue(route.claimGeneratedSemantics());
SavedTerrainChunk terrain = route.naturalTerrain().orElseThrow();
bindings.terrain.put(chunkX, terrain);
TransitionProbe.writeReceipt(engine.getWorld().worldFolder().toPath(), chunkX, 0,
NativeTerrainReceipt.encode(terrain, route.activation().activationId(), route.epoch().epochId()));
return result;
}
}
private static GeneratedSnapshot generatedObjects(Engine engine, int chunkX, Fixture fixture) {
RealPackProbeSupport.GeneratedChunk chunk = RealPackProbeSupport.generateChunk(engine, chunkX, 0);
Map<Position, ObjectVoxel> result = objectMetadata(engine, chunkX, fixture);
for (Map.Entry<Position, ObjectVoxel> entry : result.entrySet()) {
Position position = entry.getKey();
NativeBlockState state = chunk.blocks().getRaw(position.x() & 15, position.y(), position.z());
assertNotNull("Object metadata must correspond to a generated block", state);
assertEquals(entry.getValue().block(), state.key());
}
Map<Position, String> blocks = new HashMap<>();
Map<Position, String> edits = new HashMap<>();
int objectBlocks = 0;
int foundations = 0;
int carvedGround = 0;
for (int x = 0; x < 16; x++) {
for (int y = 0; y < chunk.height(); y++) {
for (int z = 0; z < 16; z++) {
NativeBlockState state = chunk.blocks().getRaw(x, y, z);
if (state != null && fixture.materials().contains(state.key().split("\\[", 2)[0])) {
objectBlocks++;
}
if (!fixture.tree()) {
Position position = new Position(chunkX * 16 + x, y, z);
String block = state == null || state.isAir() ? "minecraft:air" : state.key();
blocks.put(position, block);
if (state != null && state.key().contains("cobblestone")) {
foundations++;
edits.put(position, block);
}
if (y > 1 && y <= 72 && (state == null || state.isAir())) {
carvedGround++;
edits.put(position, block);
}
}
}
}
}
assertEquals("Every generated object block must keep placement metadata", objectBlocks, result.size());
return new GeneratedSnapshot(result, blocks, edits, foundations, carvedGround);
}
private static Map<Position, ObjectVoxel> historicalObjects(IrisEngine engine, Fixture fixture) throws Exception {
GenerationHistoryRuntimeRouter router = engine.getGenerationHistoryRuntimeRouter().orElseThrow();
try (GenerationHistoryRuntimeRouter.SavedChunkMantle saved = router.openSavedChunkMantle(0, 0)) {
return objectMetadata(saved.mantle().getChunk(0, 0), 0, engine.getHeight(), fixture);
}
}
private static void assertHistoricalEdits(IrisEngine engine, Map<Position, String> edits) throws Exception {
if (edits.isEmpty()) {
return;
}
GenerationHistoryRuntimeRouter router = engine.getGenerationHistoryRuntimeRouter().orElseThrow();
try (GenerationHistoryRuntimeRouter.SavedChunkMantle saved = router.openSavedChunkMantle(0, 0)) {
MantleChunk<Matter> chunk = saved.mantle().getChunk(0, 0).use();
try {
for (Map.Entry<Position, String> edit : edits.entrySet()) {
Position position = edit.getKey();
NativeBlockState block = chunk.get(position.x(), position.y(), position.z(), NativeBlockState.class);
assertNotNull("Historical carve/foundation must reload at " + position, block);
assertEquals("Historical carve/foundation changed at " + position, edit.getValue(),
block.isAir() ? "minecraft:air" : block.key());
}
} finally {
chunk.release();
}
}
}
private static Map<Position, ObjectVoxel> objectMetadata(Engine engine, int chunkX, Fixture fixture) {
return objectMetadata(engine.getMantle().getMantle().getChunk(chunkX, 0), chunkX, engine.getHeight(), fixture);
}
private static Map<Position, ObjectVoxel> objectMetadata(MantleChunk<Matter> chunk, int chunkX, int height, Fixture fixture) {
Map<Position, ObjectVoxel> result = new HashMap<>();
chunk.use();
try {
for (int x = 0; x < 16; x++) {
for (int y = 0; y < height; y++) {
for (int z = 0; z < 16; z++) {
String marker = chunk.get(x, y, z, String.class);
StructurePlacementMarker.Decoded decoded = StructurePlacementMarker.decode(marker);
NativeBlockState block = chunk.get(x, y, z, NativeBlockState.class);
boolean objectBlock = block != null && fixture.materials().contains(block.key().split("\\[", 2)[0]);
if (decoded != null && decoded.objectKey().equals(fixture.objectKey()) || !fixture.tree() && objectBlock) {
TreeBlockMaterial material = chunk.get(x, y, z, TreeBlockMaterial.class);
assertNotNull("Placement marker must retain its object block", block);
if (fixture.tree()) {
assertNotNull("Tree material metadata must survive", material);
assertTrue(material.matches(block.key()));
} else if (block.isStorageChest()) {
assertNotNull("Structure storage blocks must have placement metadata", decoded);
assertEquals("boundary", decoded.structureKey());
}
result.put(new Position(chunkX * 16 + x, y, z), new ObjectVoxel(block.key(), marker, material));
}
}
}
}
} finally {
chunk.release();
}
return result;
}
private static Path fixture(String name) throws Exception {
URL resource = Objects.requireNonNull(ObjectContinuationProbeTest.class.getResource("/transition-packs/" + name));
return Path.of(resource.toURI());
}
private static void copyPack(Path source, Path destination) throws IOException {
try (Stream<Path> paths = Files.walk(source)) {
for (Path path : paths.toList()) {
Path target = destination.resolve(source.relativize(path));
if (Files.isDirectory(path)) {
Files.createDirectories(target);
} else {
Files.copy(path, target);
}
}
}
}
private record Fixture(String pack, String objectKey, Set<String> materials, boolean tree) {
}
private record GeneratedSnapshot(Map<Position, ObjectVoxel> objects, Map<Position, String> blocks,
Map<Position, String> edits, int foundations, int carvedGround) {
}
private record Position(int x, int y, int z) {
}
private record ObjectVoxel(String block, String marker, TreeBlockMaterial material) {
}
private static final class Bindings implements RealPackProbeSupport.RuntimeBindings {
private final Map<Integer, SavedTerrainChunk> terrain = new HashMap<>();
@Override
public IrisPlatform create(File platformRoot) {
return new StubPlatform(platformRoot);
}
@Override
public void prepare(IrisData data, IrisDimension dimension) {
}
@Override
public GenerationHistory createHistory(RealPackProbeSupport.HistoryRequest request) throws IOException {
Files.createDirectories(request.worldRoot());
Path pack = request.data().getDataFolder().toPath();
return GenerationHistory.create(request.worldRoot(), pack,
GenerationPackFingerprint.compute(pack, GenerationPackFingerprint.CURRENT_VERSION), request.seed(),
GenerationEpochContractFactory.create(request.dimension(), "transition", "iris:object_probe"),
GenerationRegistryContract.empty());
}
@Override
public CompletableFuture<SavedTerrainChunk> captureSavedTerrainChunk(Engine engine, int chunkX, int chunkZ) {
SavedTerrainChunk chunk = chunkZ == 0 ? terrain.get(chunkX) : null;
return chunk == null ? CompletableFuture.failedFuture(new IOException("Missing historical chunk"))
: CompletableFuture.completedFuture(chunk);
}
@Override
public CompletableFuture<Void> flushSavedTerrainCapture(Engine engine) {
return CompletableFuture.completedFuture(null);
}
@Override
public void close() {
terrain.clear();
}
}
}
@@ -0,0 +1,347 @@
package art.arcane.iris.probe;
import art.arcane.iris.testsupport.IrisRuntimeState;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import org.junit.AfterClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.Arrays;
import java.util.TreeMap;
import java.util.stream.Stream;
import java.util.Objects;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertTrue;
public final class TransitionFixtureProbeTest {
@Rule
public TemporaryFolder temporary = new TemporaryFolder();
@AfterClass
public static void resetRuntime() {
IrisRuntimeState.reset();
}
@Test(timeout = 120_000)
public void lowlandToMountainPreservesHistoricalTrackingAndActivatesTheNewPack() throws Exception {
run(new Scenario("lowland", "mountain", 72, -1, 104, -1));
}
@Test(timeout = 120_000)
public void oceanToMountainRetainsOldWaterAndProducesDryDistantTerrain() throws Exception {
run(new Scenario("ocean", "mountain", 24, 40, 104, -1));
}
@Test(timeout = 120_000)
public void mountainToOceanProducesTheReplacementWaterTable() throws Exception {
run(new Scenario("mountain", "ocean", 104, -1, 24, 40));
}
@Test(timeout = 120_000)
public void waterLevelChangeKeepsTheAuthoredOceanFloor() throws Exception {
run(new Scenario("ocean", "ocean-highwater", 24, 40, 24, 48));
}
private void run(Scenario scenario) throws Exception {
Path output = temporary.newFolder().toPath().resolve("output");
Path originalPack = fixture(scenario.original());
Path replacementPack = fixture(scenario.replacement());
Map<Path, byte[]> originalSource = snapshot(originalPack);
Map<Path, byte[]> replacementSource = snapshot(replacementPack);
TransitionProbe.main(new String[]{originalPack.toString(), replacementPack.toString(),
"transition", "1337", "32", "2", output.toString()});
assertSnapshotEquals(originalSource, snapshot(originalPack));
assertSnapshotEquals(replacementSource, snapshot(replacementPack));
List<String> chunks = Files.readAllLines(output.resolve("chunks.csv"));
assertEquals(9, chunks.size());
long originalActivation = Long.parseLong(chunks.get(1).split(",")[3]);
int original = 0;
int transition = 0;
int distant = 0;
for (String row : chunks.subList(1, chunks.size())) {
String[] cells = row.split(",");
long activation = Long.parseLong(cells[3]);
boolean band = Boolean.parseBoolean(cells[4]);
assertTrue(Long.parseLong(cells[6]) > 0);
switch (cells[0]) {
case "original" -> {
original++;
assertEquals(originalActivation, activation);
assertTrue(!band);
}
case "transition" -> {
transition++;
assertTrue(activation > originalActivation);
assertTrue(band);
}
case "distant" -> {
distant++;
assertTrue(activation > originalActivation);
assertTrue(!band);
}
default -> throw new AssertionError("Unexpected phase " + cells[0]);
}
}
assertEquals(4, original);
assertEquals(2, transition);
assertEquals(2, distant);
List<String> transect = Files.readAllLines(output.resolve("transect.csv"));
assertEquals(97, transect.size());
assertFlatSeam(scenario, transect);
for (String row : transect.subList(1, transect.size())) {
String[] cells = row.split(",");
if (cells[0].equals("original")) {
assertEquals(row, scenario.originalGround(), Integer.parseInt(cells[3]));
assertEquals(row, scenario.originalFluid(), Integer.parseInt(cells[4]));
} else if (cells[0].equals("distant")) {
assertEquals(row, scenario.replacementGround(), Integer.parseInt(cells[3]));
assertEquals(row, scenario.replacementFluid(), Integer.parseInt(cells[4]));
}
}
}
@Test(timeout = 120_000)
public void slopedTransitionsKeepChunkOutputAndTrackingIndependentOfGenerationOrder() throws Exception {
compareOrders("slope-lowland", "slope-mountain", false);
}
@Test(timeout = 120_000)
public void volumetricTransitionsKeepChunkOutputIndependentOfGenerationOrder() throws Exception {
compareOrders("volumetric-lowland", "volumetric-mountain", true);
}
@Test(timeout = 120_000)
public void nearSeamDisplacementRetainsNewTerrainRippleCurvature() throws Exception {
Path original = fixture("lowland");
Path replacement = fixture("ripple-mountain");
Path output = temporary.newFolder().toPath().resolve("ripple");
TransitionProbe.main(new String[]{original.toString(), replacement.toString(), "transition", "1337",
"64", "4", output.toString()});
Map<Integer, int[]> blended = columns(Files.readAllLines(output.resolve("transect.csv")));
Map<Integer, Integer> nativeHeights = new HashMap<>();
try (RealPackProbeSupport.Workspace workspace = RealPackProbeSupport.openWorkspace(
replacement.toFile(), "transition", "[transition-ripple-control]");
RealPackProbeSupport.EngineSession session = workspace.openEngine(1337, true, "native-control")) {
Map<String, List<String>> displacedChunks = chunkContents(output);
for (int chunkX : new int[]{2, 3, 6, 7, 8, 9}) {
RealPackProbeSupport.GeneratedChunk chunk = RealPackProbeSupport.generateChunk(session.engine(), chunkX, 0);
if (chunkX >= 6) {
GenerationOrderProbe.ChunkHash nativeHash = GenerationOrderProbe.hashChunk(
new GenerationOrderProbe.ChunkCoordinate(chunkX, 0), chunk.blocks(), chunk.biomes(), chunk.height());
assertEquals("Transition must not alter distant voxels", displacedChunks.get(chunkX + ",0").get(0), nativeHash.blocks());
assertEquals("Transition must not alter distant physical biomes", displacedChunks.get(chunkX + ",0").get(1), nativeHash.biomes());
continue;
}
for (int x = 0; x < 16; x++) {
int ground = -1;
for (int y = 0; y < chunk.height(); y++) {
NativeBlockState block = chunk.blocks().getRaw(x, y, 8);
if (block != null && !block.isAir() && !block.isWater()) {
ground = y;
}
}
nativeHeights.put(chunkX * 16 + x, ground);
}
}
}
int strongRipples = 0;
for (int x = 40; x <= 60; x++) {
int nativeCurvature = nativeHeights.get(x - 3) - 2 * nativeHeights.get(x) + nativeHeights.get(x + 3);
if (Math.abs(nativeCurvature) >= 6) {
strongRipples++;
int blendedCurvature = blended.get(x - 3)[0] - 2 * blended.get(x)[0] + blended.get(x + 3)[0];
assertTrue("New terrain relief was attenuated near the seam at x=" + x
+ " native=" + nativeCurvature + " blended=" + blendedCurvature,
Math.abs(nativeCurvature - blendedCurvature) <= 3);
}
}
assertTrue("Control fixture must contain enough measurable ripples", strongRipples >= 5);
}
@Test(timeout = 120_000)
public void transitionSolidsUseTheNewPaletteWithoutHistoricalMaterialDither() throws Exception {
Path original = fixture("granite-lowland");
Path replacement = fixture("calcite-mountain");
Path output = temporary.newFolder().toPath().resolve("materials");
TransitionProbe.main(new String[]{original.toString(), replacement.toString(), "transition", "1337",
"32", "2", output.toString()});
int oldSolid = 0;
int newSolid = 0;
List<String> rows = Files.readAllLines(output.resolve("materials.csv"));
for (String row : rows.subList(1, rows.size())) {
String[] cells = row.split(",");
if (cells[3].equals("minecraft:air") || cells[3].equals("minecraft:cave_air")) {
continue;
}
if (cells[0].equals("original")) {
assertEquals(row, "minecraft:granite", cells[3]);
oldSolid += Integer.parseInt(cells[4]);
} else {
assertEquals(row, "minecraft:calcite", cells[3]);
newSolid += Integer.parseInt(cells[4]);
}
}
assertTrue(oldSolid > 0);
assertTrue(newSolid > 0);
}
@Test(timeout = 120_000)
public void oldCaveOpeningContinuesAtTheSeamAndStopsWithinFourBlocks() throws Exception {
Path original = fixture("cave-lowland");
Path replacement = fixture("lowland");
Path output = temporary.newFolder().toPath().resolve("cave");
TransitionProbe.main(new String[]{original.toString(), replacement.toString(), "transition", "1337",
"32", "2", output.toString()});
Map<Integer, String[]> sections = sections(Files.readAllLines(output.resolve("sections.csv")));
int opening = 0;
for (int y = 32; y <= 47; y++) {
if (sections.get(31)[y].equals("minecraft:air") || sections.get(31)[y].equals("minecraft:cave_air")) {
opening++;
assertTrue("Historical cave mouth must continue at the first seam column y=" + y,
sections.get(32)[y].equals("minecraft:air") || sections.get(32)[y].equals("minecraft:cave_air"));
}
assertEquals("Cave patch exceeded four blocks at y=" + y, "minecraft:stone", sections.get(35)[y]);
assertEquals("Cave must not be extruded across the transition at y=" + y, "minecraft:stone", sections.get(48)[y]);
}
assertTrue("Fixture must expose an actual cave mouth at the historical edge", opening >= 12);
assertEquals("Cave mouth requires a floor", "minecraft:stone", sections.get(31)[31]);
assertEquals("Cave mouth requires a roof", "minecraft:stone", sections.get(31)[48]);
}
private static Map<Integer, String[]> sections(List<String> rows) {
Map<Integer, String[]> result = new HashMap<>();
for (String row : rows.subList(1, rows.size())) {
String[] cells = row.split(",", 6);
int x = Integer.parseInt(cells[1]);
String[] column = result.computeIfAbsent(x, ignored -> new String[128]);
String blockKey = cells[5].contains(":") ? cells[5] : "minecraft:" + cells[5];
Arrays.fill(column, Integer.parseInt(cells[3]), Integer.parseInt(cells[4]), blockKey);
}
return result;
}
private void compareOrders(String original, String replacement, boolean volumetric) throws Exception {
Path originalPack = fixture(original);
Path replacementPack = fixture(replacement);
Path forward = temporary.newFolder().toPath().resolve("forward");
Path reverse = temporary.newFolder().toPath().resolve("reverse");
for (Path output : List.of(forward, reverse)) {
TransitionProbe.main(new String[]{originalPack.toString(), replacementPack.toString(), "transition", "1337",
"64", "4", output.toString(), Boolean.toString(output.equals(reverse))});
}
assertEquals(chunkContents(forward), chunkContents(reverse));
Map<Integer, int[]> transect = columns(Files.readAllLines(forward.resolve("transect.csv")));
assertEquals("Transect must cover both joins without missing columns", 160, transect.size());
if (volumetric) {
assertTrue("Volumetric fixture must contain more than one solid span",
transect.values().stream().anyMatch(column -> column[2] > 1));
} else {
assertTrue("Old-side fixture must have a non-flat slope", transect.get(31)[0] != transect.get(0)[0]);
assertTrue("New-side fixture must have a non-flat slope", transect.get(159)[0] != transect.get(96)[0]);
assertTrue("Height must meet historical ground at the seam", Math.abs(transect.get(32)[0] - transect.get(31)[0]) <= 2);
assertTrue("Height must meet native ground at the end of the band", Math.abs(transect.get(96)[0] - transect.get(95)[0]) <= 2);
for (int x = 32; x < 96; x++) {
assertTrue("Slope step at x=" + x, Math.abs(transect.get(x)[0] - transect.get(x - 1)[0]) <= 3);
}
}
}
private static Map<String, List<String>> chunkContents(Path output) throws Exception {
Map<String, List<String>> result = new HashMap<>();
List<String> rows = Files.readAllLines(output.resolve("chunks.csv"));
for (String row : rows.subList(1, rows.size())) {
String[] cells = row.split(",");
result.put(cells[1] + "," + cells[2], Arrays.asList(cells).subList(7, 11));
}
return result;
}
private static void assertFlatSeam(Scenario scenario, List<String> rows) {
Map<Integer, int[]> columns = columns(rows);
assertEquals("Transect must cover both joins without missing columns", 96, columns.size());
int difference = scenario.replacementGround() - scenario.originalGround();
int maximumStep = (int) Math.ceil(Math.abs(difference) * 1.875 / 32) + 1;
assertTrue("First new column must meet historical ground: old=" + columns.get(31)[0]
+ " new=" + columns.get(32)[0], Math.abs(columns.get(32)[0] - columns.get(31)[0]) <= 1);
assertTrue("Band must reach native new ground: band=" + columns.get(63)[0]
+ " native=" + columns.get(64)[0], Math.abs(columns.get(64)[0] - columns.get(63)[0]) <= 1);
for (int x = 32; x < 64; x++) {
int[] column = columns.get(x);
int step = column[0] - columns.get(x - 1)[0];
assertTrue("Excessive terrain step at x=" + x + " step=" + step + " maximum=" + maximumStep, Math.abs(step) <= maximumStep);
assertTrue("Terrain overshoot at x=" + x, column[0] >= Math.min(scenario.originalGround(), scenario.replacementGround())
&& column[0] <= Math.max(scenario.originalGround(), scenario.replacementGround()));
assertEquals("Floating water or a dry gap below water at x=" + x, 0, column[3]);
if (difference == 0) {
assertEquals("Fluid-level transition must not create a stone dam at x=" + x, scenario.originalGround(), column[0]);
assertTrue("Fluid-level transition must remain wet at x=" + x, column[1] > column[0]);
assertTrue("Water level must stay within the old/new range at x=" + x,
column[1] >= Math.min(scenario.originalFluid(), scenario.replacementFluid())
&& column[1] <= Math.max(scenario.originalFluid(), scenario.replacementFluid()));
assertTrue("Water level must rise monotonically at x=" + x, column[1] >= columns.get(x - 1)[1]);
assertTrue("Water step at x=" + x, column[1] - columns.get(x - 1)[1] <= 1);
} else {
assertTrue("Transition must not create an isolated wall or pit at x=" + x + " step=" + step,
difference > 0 ? step >= 0 : step <= 0);
}
}
}
private static Map<Integer, int[]> columns(List<String> rows) {
Map<Integer, int[]> result = new TreeMap<>();
for (String row : rows.subList(1, rows.size())) {
String[] cells = row.split(",");
int[] values = {Integer.parseInt(cells[3]), Integer.parseInt(cells[4]),
Integer.parseInt(cells[5]), Integer.parseInt(cells[6])};
assertEquals("Duplicate transect column", null, result.put(Integer.parseInt(cells[1]), values));
}
return result;
}
private static Map<Path, byte[]> snapshot(Path root) throws Exception {
Map<Path, byte[]> entries = new TreeMap<>();
try (Stream<Path> paths = Files.walk(root)) {
for (Path path : paths.toList()) {
entries.put(root.relativize(path), Files.isDirectory(path) ? null : Files.readAllBytes(path));
}
}
return entries;
}
private static void assertSnapshotEquals(Map<Path, byte[]> expected, Map<Path, byte[]> actual) {
assertEquals(expected.keySet(), actual.keySet());
for (Map.Entry<Path, byte[]> entry : expected.entrySet()) {
assertArrayEquals(entry.getKey().toString(), entry.getValue(), actual.get(entry.getKey()));
}
}
private Path fixture(String name) throws Exception {
URL resource = Objects.requireNonNull(TransitionFixtureProbeTest.class.getClassLoader()
.getResource("transition-packs/" + name), "Transition fixture " + name);
Path source = Path.of(resource.toURI());
Path pack = temporary.newFolder(name).toPath();
try (Stream<Path> paths = Files.walk(source)) {
for (Path file : paths.filter(path -> Files.isRegularFile(path) && path.toString().endsWith(".json")
&& !source.relativize(path).toString().startsWith(".")).toList()) {
Path target = pack.resolve(source.relativize(file));
Files.createDirectories(target.getParent());
Files.copy(file, target);
}
}
Files.write(pack.resolve(".source-content"), new byte[]{0, 17, -128, 127});
return pack;
}
private record Scenario(String original, String replacement, int originalGround, int originalFluid,
int replacementGround, int replacementFluid) {
}
}
@@ -0,0 +1,76 @@
package art.arcane.iris.probe;
import art.arcane.iris.world.history.BoundaryColumnGeometry;
import art.arcane.iris.world.history.NativeTerrainReceipt;
import art.arcane.iris.world.history.SavedTerrainChunk;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.nio.file.Path;
import java.nio.file.Files;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
public final class TransitionProbeTest {
@Rule
public TemporaryFolder temporary = new TemporaryFolder();
@Test
public void rejectsWorkloadsOutsideTheBandAndExistingOutputs() throws Exception {
Path root = temporary.newFolder().toPath();
String[] arguments = {root.toString(), root.toString(), "overworld", "1337", "32", "2", root.resolve("result").toString()};
assertEquals(2, TransitionProbe.Configuration.parse(arguments).chunks());
arguments[5] = "3";
assertThrows(IllegalArgumentException.class, () -> TransitionProbe.Configuration.parse(arguments));
arguments[5] = "2";
arguments[4] = "8192";
assertThrows(IllegalArgumentException.class, () -> TransitionProbe.Configuration.parse(arguments));
arguments[4] = "32";
arguments[6] = root.toString();
assertThrows(IllegalArgumentException.class, () -> TransitionProbe.Configuration.parse(arguments));
}
@Test
public void missingSourcesAreRejectedWithoutCreatingSourceOrOutputDirectories() throws Exception {
Path root = temporary.newFolder().toPath();
for (int missingIndex : new int[]{0, 1}) {
Path missing = root.resolve("missing-" + missingIndex);
Path output = root.resolve("output-" + missingIndex);
String[] arguments = {root.toString(), root.toString(), "transition", "1337", "32", "2", output.toString()};
arguments[missingIndex] = missing.toString();
assertThrows(IllegalArgumentException.class, () -> TransitionProbe.main(arguments));
assertFalse(Files.exists(missing));
assertFalse(Files.exists(output));
}
}
@Test
public void persistsActualReceiptsAcrossNegativeRegionBoundariesAndMultipleChunks() throws Exception {
Path world = temporary.newFolder().toPath();
SavedTerrainChunk.VoxelSource source = new SavedTerrainChunk.VoxelSource() {
@Override
public BoundaryColumnGeometry.Voxel voxel(int localX, int worldY, int localZ) {
return worldY <= 5
? new BoundaryColumnGeometry.Voxel("minecraft:stone", BoundaryColumnGeometry.Phase.SOLID, "", false)
: new BoundaryColumnGeometry.Voxel("minecraft:air", BoundaryColumnGeometry.Phase.AIR, "", false);
}
@Override
public String biome(int localX, int worldY, int localZ) {
return "minecraft:plains";
}
};
for (int x : new int[]{-33, -32, -31, 0, 1, 32}) {
SavedTerrainChunk chunk = SavedTerrainChunk.captureBoundary(x, -1, 0, 16, "minecraft:full", source);
TransitionProbe.writeReceipt(world, x, -1, NativeTerrainReceipt.encode(chunk, 7, "epoch"));
}
for (int x : new int[]{-33, -32, -31, 0, 1, 32}) {
assertEquals("minecraft:full", SavedTerrainChunk.readStatus(world, x, -1));
assertEquals(5, SavedTerrainChunk.read(world, x, -1, 0, 16).column(x * 16, -16).oceanFloorHeight());
assertEquals(7, NativeTerrainReceipt.decode(SavedTerrainChunk.readReceipt(world, x, -1), "minecraft:full").activationId());
}
}
}
@@ -0,0 +1,21 @@
{
"name": "Terrain",
"derivative": "minecraft:plains",
"vanillaDerivative": "minecraft:plains",
"layers": [
{
"palette": [
{
"block": "minecraft:stone"
}
]
}
],
"generators": [
{
"generator": "flat",
"min": 32,
"max": 32
}
]
}
@@ -0,0 +1,49 @@
{
"name": "Boundary structure transition fixture",
"version": 1,
"mode": {
"type": "OVERWORLD"
},
"regions": [
"terrain"
],
"environment": "NORMAL",
"dimensionHeight": {
"min": 0,
"max": 128
},
"logicalHeight": 128,
"fluidHeight": 40,
"caveProfile": {
"enabled": false
},
"structures": [
{
"structures": [
"boundary"
],
"distribution": "RANDOM_SPREAD",
"spacing": 4,
"separation": 3,
"anchor": "HEIGHT_BAND",
"minHeight": 48,
"maxHeight": 64,
"terrain": {
"mode": "BORE",
"horizontalPadding": 2,
"floorPadding": 10,
"ceilingPadding": 2
},
"stilt": {
"maxDepth": 20,
"palette": {
"palette": [
{
"block": "minecraft:cobblestone"
}
]
}
}
}
]
}
@@ -0,0 +1,15 @@
{
"seed": 310,
"interpolator": {
"function": "NONE",
"horizontalScale": 1
},
"composite": [
{
"seed": 310,
"style": {
"style": "FLAT"
}
}
]
}
@@ -0,0 +1,4 @@
{
"object": "buildings/boundary",
"rotatable": false
}
@@ -0,0 +1,8 @@
{
"pieces": [
{
"piece": "boundary",
"weight": 1
}
]
}
Loaded 100 of 159 files, more files were not shown because too many files have changed in this diff. Show more