mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-11 19:14:11 +00:00
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:
+36
-31
@@ -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.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.GenerationSessionLease;
|
||||
import art.arcane.iris.world.history.BoundaryColumnGeometry;
|
||||
import art.arcane.iris.world.history.TerrainBoundarySignature;
|
||||
import art.arcane.iris.testsupport.PlatformLeakGuard;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeTerrainColumnPolicy;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
@@ -24,7 +24,7 @@ import java.util.Optional;
|
||||
import java.util.OptionalInt;
|
||||
|
||||
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.Mockito.CALLS_REAL_METHODS;
|
||||
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.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.never;
|
||||
|
||||
public final class IrisChunkGeneratorHeightCacheTest {
|
||||
@Rule
|
||||
@@ -44,68 +45,72 @@ public final class IrisChunkGeneratorHeightCacheTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void repeatedNativeHeightSurvivesResolvedTerrainEvictionAndRuntimeChange() throws Exception {
|
||||
public void repeatedNativeHeightUsesLiveHeightQueriesAcrossRuntimeChanges() throws Exception {
|
||||
Fixture fixture = fixture();
|
||||
LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32);
|
||||
assertEquals(-11, fixture.generator().getBaseHeight(-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||
when(fixture.complex().resolvedTerrainColumn(-1, -33))
|
||||
.thenThrow(new IllegalStateException("Full terrain was evicted"));
|
||||
|
||||
when(fixture.engine().getHeight(-1, -33, false)).thenReturn(4);
|
||||
for (int index = 0; index < 100; index++) {
|
||||
assertEquals(-11, fixture.generator().getBaseHeight(
|
||||
-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);
|
||||
doReturn(Optional.of(signature())).when(fixture.complex()).resolvedTerrainColumn(-1, -33);
|
||||
assertEquals(-11, fixture.generator().getBaseHeight(-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||
verify(fixture.complex(), times(2)).resolvedTerrainColumn(-1, -33);
|
||||
when(fixture.engine().getHeight(-1, -33, false)).thenReturn(7);
|
||||
assertEquals(-8, fixture.generator().getBaseHeight(-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||
verify(fixture.engine(), never()).getComplex();
|
||||
verify(fixture.engine(), never()).getMode();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ordinaryHeightRemainsLiveUntilResolvedTransitionTerrainExists() throws Exception {
|
||||
public void nativeHeightPredicatesSelectFluidOrSolidHeightWithoutGeneratingTerrain() throws Exception {
|
||||
Fixture fixture = fixture();
|
||||
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, true)).thenReturn(8);
|
||||
|
||||
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(-11, 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));
|
||||
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
|
||||
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);
|
||||
for (int repeat = 0; repeat < 2; repeat++) {
|
||||
assertEquals(-11, fixture.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, fixture.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, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE,
|
||||
assertEquals(-11, generator.getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||
assertEquals(-13, generator.getBaseHeight(0, 0, Heightmap.Types.OCEAN_FLOOR, bounds, null));
|
||||
assertEquals(-13, generator.getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING, bounds, null));
|
||||
assertEquals(-14, generator.getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, bounds, null));
|
||||
assertEquals(-14, generator.getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE,
|
||||
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));
|
||||
}
|
||||
verify(fixture.complex(), times(6)).resolvedTerrainColumn(0, 0);
|
||||
verify(session, times(6)).resolvedColumn();
|
||||
}
|
||||
|
||||
private static Fixture fixture() throws Exception {
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
when(engine.getCacheID()).thenReturn(1);
|
||||
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);
|
||||
set(generator, "terrainColumns", new NativeTerrainColumns(new BukkitTerrainColumnPolicy(engine, null)));
|
||||
return new Fixture(generator, engine, complex);
|
||||
set(generator, "terrainColumns", new NativeTerrainColumns(policy));
|
||||
return generator;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private record Fixture(NativeChunkGenerator<?, ?, ?, ?, ?> generator, Engine engine, IrisComplex complex) {
|
||||
private record Fixture(NativeChunkGenerator<?, ?, ?, ?, ?> generator, Engine engine) {
|
||||
}
|
||||
}
|
||||
|
||||
+36
-31
@@ -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.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.GenerationSessionLease;
|
||||
import art.arcane.iris.world.history.BoundaryColumnGeometry;
|
||||
import art.arcane.iris.world.history.TerrainBoundarySignature;
|
||||
import art.arcane.iris.testsupport.PlatformLeakGuard;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeTerrainColumnPolicy;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
@@ -24,7 +24,7 @@ import java.util.Optional;
|
||||
import java.util.OptionalInt;
|
||||
|
||||
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.Mockito.CALLS_REAL_METHODS;
|
||||
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.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.never;
|
||||
|
||||
public final class IrisChunkGeneratorHeightCacheTest {
|
||||
@Rule
|
||||
@@ -45,68 +46,72 @@ public final class IrisChunkGeneratorHeightCacheTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void repeatedNativeHeightSurvivesResolvedTerrainEvictionAndRuntimeChange() throws Exception {
|
||||
public void repeatedNativeHeightUsesLiveHeightQueriesAcrossRuntimeChanges() throws Exception {
|
||||
Fixture fixture = fixture();
|
||||
LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32);
|
||||
assertEquals(-11, fixture.generator().getBaseHeight(-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||
when(fixture.complex().resolvedTerrainColumn(-1, -33))
|
||||
.thenThrow(new IllegalStateException("Full terrain was evicted"));
|
||||
|
||||
when(fixture.engine().getHeight(-1, -33, false)).thenReturn(4);
|
||||
for (int index = 0; index < 100; index++) {
|
||||
assertEquals(-11, fixture.generator().getBaseHeight(
|
||||
-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);
|
||||
doReturn(Optional.of(signature())).when(fixture.complex()).resolvedTerrainColumn(-1, -33);
|
||||
assertEquals(-11, fixture.generator().getBaseHeight(-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||
verify(fixture.complex(), times(2)).resolvedTerrainColumn(-1, -33);
|
||||
when(fixture.engine().getHeight(-1, -33, false)).thenReturn(7);
|
||||
assertEquals(-8, fixture.generator().getBaseHeight(-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||
verify(fixture.engine(), never()).getComplex();
|
||||
verify(fixture.engine(), never()).getMode();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ordinaryHeightRemainsLiveUntilResolvedTransitionTerrainExists() throws Exception {
|
||||
public void nativeHeightPredicatesSelectFluidOrSolidHeightWithoutGeneratingTerrain() throws Exception {
|
||||
Fixture fixture = fixture();
|
||||
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, true)).thenReturn(8);
|
||||
|
||||
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(-11, 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));
|
||||
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
|
||||
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);
|
||||
for (int repeat = 0; repeat < 2; repeat++) {
|
||||
assertEquals(-11, fixture.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, fixture.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, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE,
|
||||
assertEquals(-11, generator.getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||
assertEquals(-13, generator.getBaseHeight(0, 0, Heightmap.Types.OCEAN_FLOOR, bounds, null));
|
||||
assertEquals(-13, generator.getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING, bounds, null));
|
||||
assertEquals(-14, generator.getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, bounds, null));
|
||||
assertEquals(-14, generator.getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE,
|
||||
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));
|
||||
}
|
||||
verify(fixture.complex(), times(6)).resolvedTerrainColumn(0, 0);
|
||||
verify(session, times(6)).resolvedColumn();
|
||||
}
|
||||
|
||||
private static Fixture fixture() throws Exception {
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
when(engine.getCacheID()).thenReturn(1);
|
||||
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);
|
||||
set(generator, "terrainColumns", new NativeTerrainColumns(new BukkitTerrainColumnPolicy(engine, null)));
|
||||
return new Fixture(generator, engine, complex);
|
||||
set(generator, "terrainColumns", new NativeTerrainColumns(policy));
|
||||
return generator;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private record Fixture(NativeChunkGenerator<?, ?, ?, ?, ?> generator, Engine engine, IrisComplex complex) {
|
||||
private record Fixture(NativeChunkGenerator<?, ?, ?, ?, ?> generator, Engine engine) {
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -66,7 +66,6 @@ import art.arcane.iris.world.history.TerrainNativeBlockKeys;
|
||||
|
||||
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.pack.loading.IrisData;
|
||||
import art.arcane.iris.pack.PackValidationRegistry;
|
||||
@@ -1086,7 +1085,7 @@ public final class IrisModdedChunkGenerator implements NativeGeneratorOwner, Nat
|
||||
|
||||
@Override
|
||||
public NativeBlockColumn resolvedColumn(Engine current, int x, int z) {
|
||||
return current.getComplex().resolvedTerrainColumn(x, z).map(TerrainBoundarySignature::geometry).orElse(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+2
-1
@@ -102,7 +102,8 @@ final class ModdedNativeStructurePolicy implements NativeModdedStructureStage.Po
|
||||
|
||||
@Override
|
||||
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());
|
||||
}
|
||||
|
||||
|
||||
+40
@@ -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/ValueWithDiagnostics.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/CommandPregen.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())));
|
||||
}
|
||||
|
||||
@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
|
||||
public void rejectsChangedAddedAndDeletedGenerationSources() throws Exception {
|
||||
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
|
||||
public void generationChangesProduceANewBuildRevision() throws Exception {
|
||||
Fixture fixture = fixture();
|
||||
@@ -88,6 +107,10 @@ public class GenerationRevisionScopeTest {
|
||||
}
|
||||
assertFalse(manifest.sources().containsKey(ENGINE_ROOT + "EngineDiagnostics.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
|
||||
|
||||
@@ -21,7 +21,6 @@ import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class ChunkContext {
|
||||
private boolean naturalTerrain = true;
|
||||
private boolean speculativeTerrain;
|
||||
private Hunk<NativeBiome> terrainBiomes;
|
||||
private FloatingBiomeOverlay floatingBiomes;
|
||||
private final int x;
|
||||
@@ -164,14 +163,6 @@ public class ChunkContext {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isSpeculativeTerrain() {
|
||||
return speculativeTerrain;
|
||||
}
|
||||
|
||||
public void beginSpeculativeTerrain() {
|
||||
speculativeTerrain = true;
|
||||
}
|
||||
|
||||
public boolean isNaturalTerrain() {
|
||||
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.Engine;
|
||||
import art.arcane.iris.world.history.TerrainBoundarySignature;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyColumnLayer;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyColumnSample;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
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.collection.KList;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
final class IrisSugarCane {
|
||||
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) {
|
||||
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);
|
||||
HydrologyColumnLayer fluid = sample == null ? null : sample.primarySurfaceFluidLayerOrNull();
|
||||
if (fluid != null) {
|
||||
|
||||
@@ -21,8 +21,6 @@ package art.arcane.iris.generation.mantle;
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
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.DimensionStackLayout;
|
||||
import art.arcane.iris.generation.runtime.UpperDimensionContext;
|
||||
@@ -55,8 +53,6 @@ import org.jetbrains.annotations.UnmodifiableView;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalInt;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
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) {
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -124,21 +116,6 @@ public interface EngineMantle extends MatterGenerator {
|
||||
}
|
||||
|
||||
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);
|
||||
if (hydrology != null) {
|
||||
return hydrology.carves();
|
||||
@@ -149,18 +126,6 @@ public interface EngineMantle extends MatterGenerator {
|
||||
|
||||
default NativeBlockState get(int x, int y, int z) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -177,10 +142,6 @@ public interface EngineMantle extends MatterGenerator {
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
|
||||
+41
-4
@@ -29,16 +29,24 @@ import art.arcane.volmlib.util.collection.KList;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
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;
|
||||
|
||||
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 Map<PositionKey, NativeBlockState> bufferedBlocks;
|
||||
private int blockWrites;
|
||||
|
||||
FloatingObjectPlacementTransaction(IslandObjectPlacer delegate) {
|
||||
this.delegate = delegate;
|
||||
FloatingObjectPlacementTransaction(IObjectPlacer delegate, ObjectContinuationBundle.PlacementKey placementKey) {
|
||||
this.delegate = Objects.requireNonNull(delegate);
|
||||
this.placementKey = Objects.requireNonNull(placementKey);
|
||||
this.mutations = new KList<>();
|
||||
this.bufferedBlocks = new HashMap<>();
|
||||
}
|
||||
@@ -56,7 +64,8 @@ final class FloatingObjectPlacementTransaction implements IObjectPlacer {
|
||||
discard();
|
||||
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();
|
||||
return CommitResult.REJECTED_SUPPORT;
|
||||
}
|
||||
@@ -64,10 +73,38 @@ final class FloatingObjectPlacementTransaction implements IObjectPlacer {
|
||||
for (BufferedMutation mutation : mutations) {
|
||||
mutation.apply(delegate);
|
||||
}
|
||||
persistContinuation(engine);
|
||||
discard();
|
||||
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() {
|
||||
mutations.clear();
|
||||
bufferedBlocks.clear();
|
||||
|
||||
@@ -81,12 +81,14 @@ public class IrisStructureComponent extends IrisMantleComponent {
|
||||
if (!complex.allowsNewGenerationChunk(x, z)) {
|
||||
return;
|
||||
}
|
||||
int ordinal = 0;
|
||||
for (IrisStructurePlacement placement : StructurePlacementScope.placementsAt(
|
||||
getEngineMantle().getEngine(), x, z)) {
|
||||
int placementOrdinal = ordinal++;
|
||||
if (!placement.hasIrisStructures()) {
|
||||
continue;
|
||||
}
|
||||
placeFromPlacement(writer, placement, x, z, complex);
|
||||
placeFromPlacement(writer, placement, x, z, placementOrdinal);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +98,7 @@ public class IrisStructureComponent extends IrisMantleComponent {
|
||||
IrisStructurePlacement placement,
|
||||
int cx,
|
||||
int cz,
|
||||
IrisComplex complex
|
||||
int ordinal
|
||||
) {
|
||||
IrisStructureLocator.ResolvedPlacement resolved = IrisStructureLocator.resolvePlacement(
|
||||
getEngineMantle().getEngine(), placement, cx, cz);
|
||||
@@ -117,16 +119,7 @@ public class IrisStructureComponent extends IrisMantleComponent {
|
||||
return;
|
||||
}
|
||||
IrisStructureTerrain terrain = placement.resolvedTerrain();
|
||||
int[] bounds = computePieceBounds(pieces);
|
||||
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))) {
|
||||
if (!IrisStructureLocator.allowsResolvedFootprint(getEngineMantle().getEngine(), resolved)) {
|
||||
return;
|
||||
}
|
||||
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());
|
||||
}
|
||||
|
||||
if (!placement.isAnchoredUnderground()) {
|
||||
clearIntersectingObjectTrees(writer, resolved);
|
||||
}
|
||||
try (MantleWriter.ObjectPlacementCapture capture = writer.captureObjectPlacement(
|
||||
new ObjectContinuationBundle.PlacementKey(ObjectContinuationBundle.Kind.STRUCTURE, cx, cz, ordinal))) {
|
||||
if (!placement.isAnchoredUnderground()) {
|
||||
clearIntersectingObjectTrees(writer, resolved);
|
||||
}
|
||||
|
||||
StructureTerrainMode terrainMode = terrain.resolvedMode();
|
||||
if (terrainMode == StructureTerrainMode.FORCE_CARVE) {
|
||||
forceCarveStructure(writer, pieces, terrain);
|
||||
} else if (terrainMode == StructureTerrainMode.BORE) {
|
||||
boreStructure(writer, pieces, terrain);
|
||||
} else if (terrainMode != StructureTerrainMode.PRESERVE
|
||||
&& terrainMode != StructureTerrainMode.SOURCE) {
|
||||
throw new IllegalStateException("Iris assembly terrain mode " + terrainMode
|
||||
+ " is not implemented for structure '" + key + "'");
|
||||
}
|
||||
StructureTerrainMode terrainMode = terrain.resolvedMode();
|
||||
if (terrainMode == StructureTerrainMode.FORCE_CARVE) {
|
||||
forceCarveStructure(writer, pieces, terrain);
|
||||
} else if (terrainMode == StructureTerrainMode.BORE) {
|
||||
boreStructure(writer, pieces, terrain);
|
||||
} else if (terrainMode != StructureTerrainMode.PRESERVE
|
||||
&& terrainMode != StructureTerrainMode.SOURCE) {
|
||||
throw new IllegalStateException("Iris assembly terrain mode " + terrainMode
|
||||
+ " is not implemented for structure '" + key + "'");
|
||||
}
|
||||
|
||||
ObjectPlaceMode mode = structure.getPlaceMode();
|
||||
int failedPieces = 0;
|
||||
IrisStructureStiltSettings stilt = placement.getStilt();
|
||||
Long2IntOpenHashMap foundationColumns = stilt == null
|
||||
? null : new Long2IntOpenHashMap();
|
||||
boolean supportNonOccluding = stilt != null && stilt.isSupportNonOccluding();
|
||||
if (placement.isAnchoredUnderground()) {
|
||||
ObjectPlaceMode undergroundMode = (mode == ObjectPlaceMode.ORGANIC_STILT || mode == ObjectPlaceMode.CEILING_HANG)
|
||||
? mode : ObjectPlaceMode.STRUCTURE_PIECE;
|
||||
for (PlacedStructurePiece p : pieces) {
|
||||
if (placeObject(writer, structure, p, undergroundMode, p.getY(), rng,
|
||||
ObjectPlaceMode mode = structure.getPlaceMode();
|
||||
int failedPieces = 0;
|
||||
IrisStructureStiltSettings stilt = placement.getStilt();
|
||||
Long2IntOpenHashMap foundationColumns = stilt == null
|
||||
? null : new Long2IntOpenHashMap();
|
||||
boolean supportNonOccluding = stilt != null && stilt.isSupportNonOccluding();
|
||||
if (placement.isAnchoredUnderground()) {
|
||||
ObjectPlaceMode undergroundMode = (mode == ObjectPlaceMode.ORGANIC_STILT || mode == ObjectPlaceMode.CEILING_HANG)
|
||||
? mode : ObjectPlaceMode.STRUCTURE_PIECE;
|
||||
for (PlacedStructurePiece p : pieces) {
|
||||
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) {
|
||||
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 {
|
||||
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) {
|
||||
failedPieces++;
|
||||
requireAppliedPieces(resolved, cx, cz, failedPieces);
|
||||
if (failedPieces == 0 && stilt != null) {
|
||||
placeFoundation(writer, foundationColumns, stilt, rng, !placement.isAnchoredUnderground());
|
||||
}
|
||||
} else {
|
||||
for (PlacedStructurePiece p : pieces) {
|
||||
if (placeObject(writer, structure, p, ObjectPlaceMode.STRUCTURE_PIECE, p.getY(), rng,
|
||||
foundationColumns, supportNonOccluding) == -1) {
|
||||
failedPieces++;
|
||||
}
|
||||
if (failedPieces == 0) {
|
||||
capture.commit();
|
||||
}
|
||||
}
|
||||
requireAppliedPieces(resolved, cx, cz, failedPieces);
|
||||
if (failedPieces == 0 && stilt != null) {
|
||||
placeFoundation(writer, foundationColumns, stilt, rng, !placement.isAnchoredUnderground());
|
||||
}
|
||||
}
|
||||
|
||||
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};
|
||||
}
|
||||
|
||||
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,
|
||||
ObjectPlaceMode mode, int y, RNG rng, Long2IntOpenHashMap foundationColumns,
|
||||
boolean supportNonOccluding) {
|
||||
|
||||
+39
-12
@@ -18,6 +18,7 @@
|
||||
|
||||
package art.arcane.iris.generation.mantle;
|
||||
|
||||
import java.io.UncheckedIOException;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.generation.cache.Cache;
|
||||
@@ -65,6 +66,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
|
||||
return;
|
||||
}
|
||||
IrisData data = getData();
|
||||
PlacementSequence placements = new PlacementSequence();
|
||||
int minX = x << 4;
|
||||
int minZ = z << 4;
|
||||
RNG chunkRng = new RNG(Cache.key(x, z) + seed() + 0x0FA710BEL);
|
||||
@@ -122,7 +124,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
|
||||
KList<IrisObjectPlacement> floating = entry.getFloatingObjects();
|
||||
if (floating != null && !floating.isEmpty()) {
|
||||
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);
|
||||
if (hasSurface) {
|
||||
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) {
|
||||
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()) {
|
||||
KList<Integer> interior = interiorColumns(sampleResolver, columns, minX, minZ, entry, IslandObjectPlacer.AnchorFace.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
|
||||
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()) {
|
||||
return;
|
||||
}
|
||||
@@ -210,21 +212,32 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
|
||||
}
|
||||
int id = rng.i(0, Integer.MAX_VALUE);
|
||||
|
||||
FloatingObjectPlacementTransaction transaction = new FloatingObjectPlacementTransaction(
|
||||
writer, placements.next(minX >> 4, minZ >> 4));
|
||||
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);
|
||||
if (marker != null && shouldWritePlacementMarker(writer, bd, b.getX(), b.getY(), b.getZ())) {
|
||||
writer.setData(b.getX(), b.getY(), b.getZ(), marker);
|
||||
if (marker != null && shouldWritePlacementMarker(transaction, bd, b.getX(), b.getY(), b.getZ())) {
|
||||
transaction.setData(b.getX(), b.getY(), b.getZ(), marker);
|
||||
}
|
||||
}, null, data);
|
||||
if (resultY < 0) {
|
||||
transaction.discard();
|
||||
} else {
|
||||
transaction.commit();
|
||||
}
|
||||
} catch (UncheckedIOException failure) {
|
||||
transaction.discard();
|
||||
throw failure;
|
||||
} catch (Throwable e) {
|
||||
transaction.discard();
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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()) {
|
||||
return;
|
||||
}
|
||||
@@ -293,7 +306,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
|
||||
int yv = pickTopY + 1 - fp.getLowestSolidKeyY();
|
||||
|
||||
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);
|
||||
|
||||
try {
|
||||
@@ -308,6 +321,9 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
|
||||
} else {
|
||||
transaction.commit();
|
||||
}
|
||||
} catch (UncheckedIOException failure) {
|
||||
transaction.discard();
|
||||
throw failure;
|
||||
} catch (Throwable e) {
|
||||
transaction.discard();
|
||||
IrisLogging.reportError(e);
|
||||
@@ -316,7 +332,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
|
||||
}
|
||||
|
||||
@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()) {
|
||||
return;
|
||||
}
|
||||
@@ -402,7 +418,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
|
||||
int yv = invertedBaseY(pickBottomY, fp, invertedRotation);
|
||||
|
||||
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);
|
||||
|
||||
try {
|
||||
@@ -417,6 +433,9 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
|
||||
} else {
|
||||
transaction.commit();
|
||||
}
|
||||
} catch (UncheckedIOException failure) {
|
||||
transaction.discard();
|
||||
throw failure;
|
||||
} catch (Throwable e) {
|
||||
transaction.discard();
|
||||
IrisLogging.reportError(e);
|
||||
@@ -636,4 +655,12 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
|
||||
}
|
||||
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.cache.Cache;
|
||||
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.Engine;
|
||||
import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial;
|
||||
@@ -145,11 +142,12 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
x,
|
||||
z,
|
||||
getRadius(),
|
||||
(sourceX, sourceZ) -> transaction.apply(sourcePlans.get(
|
||||
sourceX,
|
||||
sourceZ,
|
||||
() -> buildSourcePlan(writer, sourceX, sourceZ, context)
|
||||
))
|
||||
(sourceX, sourceZ) -> {
|
||||
ObjectSourcePlan plan = sourcePlans.get(sourceX, sourceZ,
|
||||
() -> buildSourcePlan(writer, sourceX, sourceZ, context));
|
||||
transaction.apply(plan);
|
||||
plan.persistContinuations(writer, sourceX, sourceZ, x, z);
|
||||
}
|
||||
);
|
||||
transaction.commit();
|
||||
}
|
||||
@@ -668,6 +666,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
try {
|
||||
int placeResult = -1;
|
||||
CaveObjectPlacementTransaction.CommitResult commitResult = CaveObjectPlacementTransaction.CommitResult.EMPTY;
|
||||
int continuation = writer.beginObjectPlacement();
|
||||
if (carving) {
|
||||
int caveFloorY = caveAnchor.y();
|
||||
if (golden) {
|
||||
@@ -707,6 +706,9 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
}
|
||||
}, null, getData());
|
||||
}
|
||||
if (placeResult >= 0 && (!carving || commitResult == CaveObjectPlacementTransaction.CommitResult.COMMITTED)) {
|
||||
writer.endObjectPlacement(continuation);
|
||||
}
|
||||
if (golden) {
|
||||
IrisLogging.debug("Goldendebug procedural result: chunk=" + x + "," + z
|
||||
+ " placement=" + p.getName()
|
||||
@@ -778,19 +780,10 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
|
||||
private boolean acceptsCaveAnchorAt(int x, int y, int z, boolean underwater,
|
||||
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(),
|
||||
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) {
|
||||
return acceptsCaveAnchorFluid(underwater, cavern, null, y, lavaHeight);
|
||||
@@ -914,6 +907,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
}
|
||||
try {
|
||||
String marker = placementMarker(v, id, "surface");
|
||||
int continuation = writer.beginObjectPlacement();
|
||||
int result = v.place(xx, -1, zz, placePlacer, effectivePlacement, rng, (b, data) -> {
|
||||
if (marker != null) {
|
||||
writer.setData(b.getX(), b.getY(), b.getZ(), marker);
|
||||
@@ -927,6 +921,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
}, null, getData());
|
||||
|
||||
if (result >= 0) {
|
||||
writer.endObjectPlacement(continuation);
|
||||
placed++;
|
||||
} else {
|
||||
rejected++;
|
||||
@@ -1096,6 +1091,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
IrisObjectPlacement effectivePlacement = resolveCavePlacement(objectPlacement, object, caveProfile);
|
||||
|
||||
try {
|
||||
int continuation = writer.beginObjectPlacement();
|
||||
ContainedPlacementResult contained = placeContainedCaveObject(
|
||||
writer,
|
||||
object,
|
||||
@@ -1112,6 +1108,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
int result = contained.resultY();
|
||||
boolean wroteBlocks = contained.commitResult() == CaveObjectPlacementTransaction.CommitResult.COMMITTED;
|
||||
if (wroteBlocks) {
|
||||
writer.endObjectPlacement(continuation);
|
||||
placed++;
|
||||
} else {
|
||||
rejected++;
|
||||
@@ -1286,7 +1283,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
}
|
||||
boolean treePlacement = isTreePlacement(v, objectPlacement);
|
||||
String marker = placementMarker(v, id, "upper");
|
||||
|
||||
int continuation = writer.beginObjectPlacement();
|
||||
int result = v.place(xx, anchorY, zz, writer, placement, rng, (b, data) -> {
|
||||
if (marker != null) {
|
||||
writer.setData(b.getX(), b.getY(), b.getZ(), marker);
|
||||
@@ -1299,6 +1296,9 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
}
|
||||
}, null, getData());
|
||||
|
||||
if (result >= 0) {
|
||||
writer.endObjectPlacement(continuation);
|
||||
}
|
||||
if (traceRegen) {
|
||||
IrisLogging.debug("Upper object placement: chunk=" + chunkX + "," + chunkZ
|
||||
+ " 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.terrain.Terrain3DColumn;
|
||||
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.pack.value.IrisPosition;
|
||||
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.math.RNG;
|
||||
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.MatterSlice;
|
||||
import art.arcane.volmlib.util.noise.CNG;
|
||||
@@ -61,9 +59,13 @@ import art.arcane.iris.generation.block.B;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
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.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicReferenceArray;
|
||||
|
||||
@@ -86,6 +88,8 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
|
||||
@EqualsAndHashCode.Exclude
|
||||
@ToString.Exclude
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
|
||||
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) {
|
||||
if (t == null || !allowsWrite(x, z)) {
|
||||
return;
|
||||
@@ -323,6 +343,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
|
||||
Class<?> sliceType = t instanceof NativeBlockState ? NativeBlockState.class : matter.getClass(t);
|
||||
capturePreObjectOriginal(matter, x, y, z, sliceType);
|
||||
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.set(x & 15, y & 15, z & 15, value);
|
||||
recordObjectValue(x, y, z, MatterCavern.class, value);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -384,6 +406,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
|
||||
if (blockSlice != null) {
|
||||
capturePreObjectOriginal(matter, x, y, z, NativeBlockState.class);
|
||||
blockSlice.set(x & 15, y & 15, z & 15, null);
|
||||
recordObjectValue(x, y, z, NativeBlockState.class, AIR.get());
|
||||
}
|
||||
clearDeferredPlacement(matter, x, y, z);
|
||||
MatterSlice<MatterCavern> cavernSlice = matter.getSlice(MatterCavern.class);
|
||||
@@ -395,6 +418,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
|
||||
cavernSlice = matter.slice(MatterCavern.class);
|
||||
}
|
||||
cavernSlice.set(x & 15, y & 15, z & 15, value);
|
||||
recordObjectValue(x, y, z, MatterCavern.class, value);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -413,14 +437,13 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
|
||||
if (hasProtectedHydrology(matter, x, y, z)) {
|
||||
return;
|
||||
}
|
||||
MatterSlice<NativeBlockState> blockSlice = matter.getSlice(NativeBlockState.class);
|
||||
if (blockSlice != null) {
|
||||
capturePreObjectOriginal(matter, x, y, z, NativeBlockState.class);
|
||||
blockSlice.set(x & 15, y & 15, z & 15, null);
|
||||
}
|
||||
capturePreObjectOriginal(matter, x, y, z, NativeBlockState.class);
|
||||
matter.<NativeBlockState>slice(NativeBlockState.class).set(x & 15, y & 15, z & 15, AIR.get());
|
||||
clearDeferredPlacement(matter, x, y, z);
|
||||
capturePreObjectOriginal(matter, x, y, z, MatterCavern.class);
|
||||
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) {
|
||||
capturePreObjectOriginal(matter, x, y, z, NativeBlockState.class);
|
||||
blockSlice.set(x & 15, y & 15, z & 15, null);
|
||||
recordObjectValue(x, y, z, NativeBlockState.class, AIR.get());
|
||||
}
|
||||
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) {
|
||||
Optional<TerrainBoundarySignature> resolved = resolvedColumn(x, z);
|
||||
if (resolved.isPresent()) {
|
||||
return resolvedBlock(resolved.get(), y);
|
||||
}
|
||||
NativeBlockState block = getPrerequisiteDataIfPresent(x, y, z, NativeBlockState.class);
|
||||
return block == null ? AIR.get() : block;
|
||||
}
|
||||
|
||||
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);
|
||||
if (hydrology != null) {
|
||||
return hydrology.carves();
|
||||
@@ -554,10 +558,6 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
|
||||
}
|
||||
|
||||
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());
|
||||
byte[] carvedColumn = new byte[cappedHeight];
|
||||
if (cappedHeight <= 0) {
|
||||
@@ -646,6 +646,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
|
||||
return false;
|
||||
}
|
||||
restoreRaw(matter, x, y, z, type, cell.original(type));
|
||||
recordObjectValue(x, y, z, type, cell.original(type));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -672,12 +673,15 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
|
||||
}
|
||||
if (cell.blockCaptured()) {
|
||||
restoreRaw(matter, x, y, z, NativeBlockState.class, cell.block());
|
||||
recordObjectValue(x, y, z, NativeBlockState.class, cell.block());
|
||||
}
|
||||
if (cell.stringCaptured()) {
|
||||
restoreRaw(matter, x, y, z, String.class, cell.string());
|
||||
recordObjectValue(x, y, z, String.class, cell.string());
|
||||
}
|
||||
if (cell.cavernCaptured()) {
|
||||
restoreRaw(matter, x, y, z, MatterCavern.class, cell.cavern());
|
||||
recordObjectValue(x, y, z, MatterCavern.class, cell.cavern());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -705,6 +709,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
|
||||
}
|
||||
capturePreObjectOriginal(matter, x, y, z, type);
|
||||
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);
|
||||
blockSlice.set(x & 15, y & 15, z & 15, baseState);
|
||||
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
|
||||
// a 16^3 section + slice on a miss the way getData's getOrCreate path does.
|
||||
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)
|
||||
return AIR.get();
|
||||
return block;
|
||||
@@ -924,20 +924,6 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
|
||||
|
||||
@Override
|
||||
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);
|
||||
if (hydrology != null) {
|
||||
return hydrology.carves();
|
||||
@@ -946,45 +932,8 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
|
||||
|| 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) {
|
||||
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());
|
||||
byte[] carvedColumn = new byte[cappedHeight];
|
||||
if (cappedHeight <= 0) {
|
||||
@@ -1045,11 +994,6 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
|
||||
|
||||
@Override
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+77
@@ -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;
|
||||
}
|
||||
}
|
||||
+23
-1
@@ -30,6 +30,7 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
||||
private final int destinationChunkZ;
|
||||
private final int worldHeight;
|
||||
private final List<Mutation> mutations;
|
||||
private final List<PlacementRange> placements = new ArrayList<>();
|
||||
private final Long2ObjectOpenHashMap<Int2ObjectOpenHashMap<OverlayCell>> overlay;
|
||||
|
||||
ObjectDestinationTransaction(MantleWriter writer, int destinationChunkX, int destinationChunkZ) {
|
||||
@@ -53,7 +54,25 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
||||
if (checkpoint < 0 || checkpoint > mutations.size()) {
|
||||
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) {
|
||||
@@ -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) {
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,13 @@ public interface ObjectPassPlacer extends IObjectPlacer {
|
||||
|
||||
byte[] getCarvedColumn(int x, int z, int height);
|
||||
|
||||
default int beginObjectPlacement() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
default void endObjectPlacement(int checkpoint) {
|
||||
}
|
||||
|
||||
@Override
|
||||
default <T> @Nullable T getData(int x, int y, int z, Class<T> 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.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.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.List;
|
||||
|
||||
final class ObjectSourcePlan {
|
||||
private static final int DESTINATION_INDEX_WEIGHT = 8;
|
||||
private final Long2ObjectOpenHashMap<List<ObjectDestinationTransaction.Mutation>> destinations;
|
||||
private final int mutationWeight;
|
||||
private final List<Placement> placements;
|
||||
|
||||
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);
|
||||
long weight = 1L;
|
||||
for (ObjectDestinationTransaction.Mutation mutation : mutations) {
|
||||
@@ -29,6 +53,12 @@ final class ObjectSourcePlan {
|
||||
for (Long2ObjectMap.Entry<ArrayList<ObjectDestinationTransaction.Mutation>> entry : grouped.long2ObjectEntrySet()) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -37,10 +67,95 @@ final class ObjectSourcePlan {
|
||||
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() {
|
||||
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) {
|
||||
return ((long) chunkX << 32) | (chunkZ & 0xffffffffL);
|
||||
}
|
||||
|
||||
@@ -125,10 +125,10 @@ public final class DimensionTerrainContext implements DataProvider {
|
||||
engine.getDimension(),
|
||||
engine.getData(),
|
||||
engine.getHeight(),
|
||||
natural ? complex.getUnblendedNaturalHeightStream() : complex.getHeightStream(),
|
||||
natural ? complex.getNaturalHeightStream() : complex.getHeightStream(),
|
||||
natural ? complex.getNaturalSlopeStream() : complex.getSlopeStream(),
|
||||
fluidHeightStream,
|
||||
natural ? complex.getUnblendedNaturalTrueBiomeStream() : complex.getTrueBiomeStream(),
|
||||
natural ? complex.getNaturalTrueBiomeStream() : complex.getTrueBiomeStream(),
|
||||
complex.getRegionStream(),
|
||||
complex.getRockStream(),
|
||||
fluidBlockSampler,
|
||||
|
||||
@@ -47,7 +47,6 @@ import art.arcane.iris.structure.placement.IrisStructure;
|
||||
import art.arcane.iris.world.IrisWorld;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
|
||||
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.NativeBlockState;
|
||||
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.iris.generation.concurrent.BurstExecutor;
|
||||
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 org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalInt;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
@@ -496,10 +490,6 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
|
||||
@BlockCoordinates
|
||||
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();
|
||||
if (dimensionStackContext != null) {
|
||||
return ignoreFluid
|
||||
|
||||
@@ -18,17 +18,31 @@
|
||||
|
||||
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.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.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 {
|
||||
KList<EngineStage> getTerrainStages();
|
||||
@@ -199,12 +213,115 @@ public interface EngineMode extends Staged {
|
||||
|
||||
EngineStage[] stages = getStages().toArray(new EngineStage[0]);
|
||||
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);
|
||||
ctx.beginContent();
|
||||
List<Matter> continuations = restoreObjectContinuations(x, z);
|
||||
for (EngineStage i : stages) {
|
||||
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.world.history.GenerationBlend;
|
||||
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.HydrologyColumnSample;
|
||||
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.IrisImageMapRuntime;
|
||||
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.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.biome.IrisBiomeGeneratorLink;
|
||||
@@ -99,7 +101,6 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalInt;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
@@ -175,7 +176,6 @@ public class IrisComplex implements DataProvider {
|
||||
private final transient boolean reuseNaturalBaseBiome;
|
||||
private ProceduralStream<UUID> baseBiomeIDStream;
|
||||
private ProceduralStream<IrisBiome> naturalTrueBiomeStream;
|
||||
private ProceduralStream<IrisBiome> unblendedNaturalTrueBiomeStream;
|
||||
private ProceduralStream<IrisBiome> trueBiomeStream;
|
||||
private ProceduralStream<NativeBiome> trueBiomeDerivativeStream;
|
||||
private ProceduralStream<Double> naturalHeightStream;
|
||||
@@ -188,8 +188,7 @@ public class IrisComplex implements DataProvider {
|
||||
private transient Terrain3DRuntime terrain3D;
|
||||
private transient HydrologyBankTerrainRuntime hydrologyBanks3D;
|
||||
private ProceduralStream<Double> unblendedNaturalHeightStream;
|
||||
private final ResolvedTerrainProvider resolvedTerrain;
|
||||
private ProceduralStream<Double> placementHeightStream;
|
||||
private final TransitionDisplacementField transitionDisplacement;
|
||||
private ProceduralStream<Double> heightStream;
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Setter(AccessLevel.NONE)
|
||||
@@ -248,7 +247,6 @@ public class IrisComplex implements DataProvider {
|
||||
biomeBoundsSamplingStep = engine.getDimension().getBiomeBoundsSamplingStep();
|
||||
this.detached = detached;
|
||||
this.transitionGenerationPlan = transitionGenerationPlan;
|
||||
this.resolvedTerrain = new ResolvedTerrainProvider(engine);
|
||||
int cacheSize = noiseCacheSize(engine, IrisSettings.get().getPerformance().getNoiseCacheSize(), detached);
|
||||
IrisBiome emptyBiome = new IrisBiome().setInferredType(InferredType.CAVE);
|
||||
UUID focusUUID = UUID.nameUUIDFromBytes("focus".getBytes());
|
||||
@@ -408,22 +406,21 @@ public class IrisComplex implements DataProvider {
|
||||
unblendedNaturalHeightStream = terrain3DEnabled
|
||||
? GenerationStreams.cache2DDouble(ProceduralStream.ofDouble(this::sampleTerrain3DHeight), "unblendedNaturalHeightStream", engine, cacheSize)
|
||||
: 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
|
||||
.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)
|
||||
? fixBiomeType(h, baseBiomeStream.get(x, z), regionStream.get(x, z), x, z,
|
||||
transitionFluidHeight(x, z, fluidHeight))
|
||||
: mapped;
|
||||
}), "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();
|
||||
if (hydrologyActive(configuredHydrology)) {
|
||||
hydrologyRuntime = new IrisHydrologyRuntime(new IrisHydrologyRuntimeContext(
|
||||
@@ -456,10 +453,9 @@ public class IrisComplex implements DataProvider {
|
||||
ProceduralStream.ofDouble(this::resolveHydrologyTerrainHeight), cacheSize);
|
||||
heightStream = ProceduralStream.ofDouble((x, z) -> terrainEngine.getPlatformHooks().isMainThread()
|
||||
? nonblockingTerrainHeight(x, z) : cachedHeightStream.getDouble(x, z));
|
||||
placementHeightStream = ProceduralStream.ofDouble(this::samplePlacementHeight);
|
||||
roundedHeighteightStream = GenerationStreams.contextInjecting(placementHeightStream, engine, (c, x, z) -> c.getHeight().getDouble(x, z))
|
||||
roundedHeighteightStream = GenerationStreams.contextInjecting(heightStream, engine, (c, x, z) -> c.getHeight().getDouble(x, z))
|
||||
.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);
|
||||
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.
|
||||
@@ -534,7 +530,7 @@ public class IrisComplex implements DataProvider {
|
||||
List<NoiseCacheCapacity> capacities = new ArrayList<>(5);
|
||||
int additionalChunks = HydrologyNoiseCacheBudget.MAXIMUM_CHUNKS;
|
||||
for (ProceduralStream<?> stream : new ProceduralStream<?>[]{
|
||||
baseBiomeStream, baseTerrainHeightStream, unblendedNaturalHeightStream, regionStream, bridgeStream}) {
|
||||
baseBiomeStream, baseTerrainHeightStream, unblendedNaturalHeightStream, naturalHeightStream, regionStream, bridgeStream}) {
|
||||
if (!(stream instanceof CachedStream2D<?> || stream instanceof CachedDoubleStream2D)
|
||||
|| selected.put(stream, Boolean.TRUE) != null) {
|
||||
continue;
|
||||
@@ -641,9 +637,7 @@ public class IrisComplex implements DataProvider {
|
||||
}
|
||||
|
||||
private double sampleNaturalTerrainHeight(Engine engine, double x, double z) {
|
||||
return terrain3D != null && terrain3D.active()
|
||||
? sampleTerrain3DHeight(x, z)
|
||||
: sampleUnblendedNaturalTerrainHeight(x, z);
|
||||
return naturalHeightStream.getDouble(x, z);
|
||||
}
|
||||
|
||||
private double sampleTerrain3DHeight(double x, double z) {
|
||||
@@ -672,6 +666,10 @@ public class IrisComplex implements DataProvider {
|
||||
return null;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -740,7 +738,7 @@ public class IrisComplex implements DataProvider {
|
||||
|
||||
private double nearbyTerrainSurfaceHeight(int x, int surfaceY, int 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) {
|
||||
@@ -783,17 +781,18 @@ public class IrisComplex implements DataProvider {
|
||||
if (biome == null || region == null) {
|
||||
return biome;
|
||||
}
|
||||
double shoreTop = shoreBandTop(height, region, x, z, fluidHeight, shoreSlopeStream);
|
||||
if (height >= fluidHeight - 1 && height <= shoreTop && !biome.isShore()) {
|
||||
double localFluidHeight = transitionFluidHeight(x, z, fluidHeight);
|
||||
double shoreTop = shoreBandTop(height, region, x, z, localFluidHeight, shoreSlopeStream);
|
||||
if (height >= localFluidHeight - 1 && height <= shoreTop && !biome.isShore()) {
|
||||
return sampleInferredBiome(region, InferredType.SHORE, x, z);
|
||||
}
|
||||
if (height > shoreTop && !biome.isLand()) {
|
||||
return sampleInferredBiome(region, InferredType.LAND, x, z);
|
||||
}
|
||||
if (height < fluidHeight && !biome.isAquatic()) {
|
||||
if (height < localFluidHeight && !biome.isAquatic()) {
|
||||
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 biome;
|
||||
@@ -955,6 +954,16 @@ public class IrisComplex implements DataProvider {
|
||||
}
|
||||
|
||||
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);
|
||||
if (layer != null) {
|
||||
return resolveHydrologyFluid(layer.profileKey(), x, z);
|
||||
@@ -1023,7 +1032,15 @@ public class IrisComplex implements DataProvider {
|
||||
|
||||
private double resolveHydrologyFluidSurface(double x, double 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) {
|
||||
@@ -1034,27 +1051,55 @@ public class IrisComplex implements DataProvider {
|
||||
return hydrologyRuntime.sample(x, z).orElse(null);
|
||||
}
|
||||
double hydrologyWeight = transitionHydrologyWeight(x, z);
|
||||
if (hydrologyWeight == 0D) {
|
||||
return null;
|
||||
}
|
||||
HydrologyColumnSample sample = hydrologyRuntime.sample(x, z).orElse(null);
|
||||
if (sample == null || hydrologyWeight == 1D) {
|
||||
if (sample == null) {
|
||||
return sample;
|
||||
}
|
||||
return taperHydrologySample(sample, hydrologyWeight);
|
||||
return transitionHydrologySample(sample, hydrologyWeight, transitionDisplacement);
|
||||
}
|
||||
|
||||
private HydrologyColumnSnapshot sampleHydrologySnapshot(int x, int z) {
|
||||
if (hydrologyRuntime == null) {
|
||||
return HydrologyColumnSnapshot.ready(null);
|
||||
}
|
||||
double weight = transitionHydrologyWeight(x, z);
|
||||
if (weight == 0D) {
|
||||
return HydrologyColumnSnapshot.ready(null);
|
||||
}
|
||||
HydrologyColumnSnapshot snapshot = hydrologyRuntime.sampleSnapshot(x, z);
|
||||
return !snapshot.available() || snapshot.column() == null || weight == 1D ? snapshot
|
||||
: HydrologyColumnSnapshot.ready(taperHydrologySample(snapshot.column(), weight));
|
||||
return !snapshot.available() || snapshot.column() == null ? snapshot
|
||||
: 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(
|
||||
@@ -1067,6 +1112,9 @@ public class IrisComplex implements DataProvider {
|
||||
}
|
||||
ArrayList<HydrologyColumnLayer> taperedLayers = new ArrayList<>(sample.layers().size());
|
||||
for (HydrologyColumnLayer layer : sample.layers()) {
|
||||
if (hydrologyWeight == 0D && !layer.feature().type().isUnderground() && !layer.feature().type().isDeepFluid()) {
|
||||
continue;
|
||||
}
|
||||
taperedLayers.add(taperHydrologyLayer(layer, sample.naturalHeight(), hydrologyWeight));
|
||||
}
|
||||
return new HydrologyColumnSample(
|
||||
@@ -1087,19 +1135,21 @@ public class IrisComplex implements DataProvider {
|
||||
if (layer.feature().type().isUnderground() || layer.feature().type().isDeepFluid()) {
|
||||
return layer;
|
||||
}
|
||||
int bed = GenerationBlend.interpolateHeight(naturalHeight, layer.bedY(), hydrologyWeight);
|
||||
boolean dryChannel = layer.channel() && bed > layer.fluidHeadY();
|
||||
return new HydrologyColumnLayer(
|
||||
layer.feature(),
|
||||
GenerationBlend.interpolateHeight(naturalHeight, layer.bedY(), hydrologyWeight),
|
||||
GenerationBlend.interpolateHeight(naturalHeight, layer.fluidHeadY(), hydrologyWeight),
|
||||
GenerationBlend.interpolateHeight(naturalHeight, layer.ceilingY(), hydrologyWeight),
|
||||
layer.channel(),
|
||||
bed,
|
||||
layer.fluidHeadY(),
|
||||
layer.ceilingY(),
|
||||
layer.channel() && !dryChannel,
|
||||
layer.shore(),
|
||||
layer.grading(),
|
||||
layer.connectedFluid(),
|
||||
layer.fallingFluid(),
|
||||
layer.receivingPool(),
|
||||
layer.connectedFluid() && !dryChannel,
|
||||
layer.fallingFluid() && !dryChannel,
|
||||
layer.receivingPool() && !dryChannel,
|
||||
layer.terrainOwned(),
|
||||
layer.fluidOwned(),
|
||||
layer.fluidOwned() && !dryChannel,
|
||||
layer.oceanApron(),
|
||||
layer.profileKey(),
|
||||
layer.surfaceBiomeKey(),
|
||||
@@ -1114,14 +1164,14 @@ public class IrisComplex implements DataProvider {
|
||||
}
|
||||
|
||||
private double transitionHydrologyWeight(double x, double z) {
|
||||
return transitionGenerationPlan == null
|
||||
return transitionDisplacement == null
|
||||
? 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). */
|
||||
public boolean isHydrologyPlanned(int x, int z) {
|
||||
if (hydrologyRuntime == null || transitionHydrologyWeight(x, z) == 0D) {
|
||||
if (hydrologyRuntime == null) {
|
||||
return true;
|
||||
}
|
||||
if (!hydrologyRuntime.isPlanned(x, z)) {
|
||||
@@ -1144,23 +1194,7 @@ public class IrisComplex implements DataProvider {
|
||||
}
|
||||
|
||||
public ProceduralStream<Double> getHeightStream() {
|
||||
return placementHeightStream;
|
||||
}
|
||||
|
||||
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));
|
||||
return heightStream;
|
||||
}
|
||||
|
||||
boolean isNaturalTerrainContext() {
|
||||
@@ -1170,11 +1204,6 @@ public class IrisComplex implements DataProvider {
|
||||
&& 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) {
|
||||
return transitionGenerationPlan != null
|
||||
&& transitionGenerationPlan.boundary().isHistoricalChunk(chunkX, chunkZ);
|
||||
@@ -1214,8 +1243,16 @@ public class IrisComplex implements DataProvider {
|
||||
int maximumX,
|
||||
int maximumZ
|
||||
) {
|
||||
return transitionGenerationPlan == null
|
||||
|| transitionGenerationPlan.allowsNewFootprint(minimumX, minimumZ, maximumX, maximumZ);
|
||||
if (transitionGenerationPlan == null) {
|
||||
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) {
|
||||
@@ -1248,7 +1285,8 @@ public class IrisComplex implements DataProvider {
|
||||
return mapped;
|
||||
}
|
||||
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) {
|
||||
@@ -1336,7 +1374,7 @@ public class IrisComplex implements DataProvider {
|
||||
regionStream.get(x, z),
|
||||
x,
|
||||
z,
|
||||
fluidHeight
|
||||
transitionFluidHeight(x, z, fluidHeight)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2094,6 +2132,8 @@ public class IrisComplex implements DataProvider {
|
||||
hydrologyRuntime.close();
|
||||
}
|
||||
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.math.M;
|
||||
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.PrecisionStopwatch;
|
||||
import lombok.AccessLevel;
|
||||
@@ -1478,7 +1481,18 @@ public class IrisEngine implements Engine {
|
||||
|
||||
boolean skipRealFlag = platformHooks.shouldBypassMantleStages(this);
|
||||
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());
|
||||
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 height = h.getHeight();
|
||||
Engine engine = getEngine();
|
||||
Mantle<Matter> mantle = context.isSpeculativeTerrain() ? null : engine.getMantle().getMantle();
|
||||
Mantle<Matter> mantle = engine.getMantle().getMantle();
|
||||
ChunkedDataCache<IrisBiome> biomeCache = context.getBiome();
|
||||
IrisComplex complex = context.getComplex();
|
||||
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,
|
||||
ChunkContext context) {
|
||||
if (context.isSpeculativeTerrain() || !context.getComplex().allowsMantleChunkWrite(x >> 4, z >> 4)) {
|
||||
if (!context.getComplex().allowsMantleChunkWrite(x >> 4, z >> 4)) {
|
||||
return;
|
||||
}
|
||||
Mantle<Matter> mantle = engine.getMantle().getMantle();
|
||||
@@ -146,7 +146,7 @@ public class IrisBiomeActuator extends EngineAssignedActuator<NativeBiome> {
|
||||
IrisComplex complex
|
||||
) {
|
||||
TransitionGenerationPlan transitionPlan = complex.getTransitionGenerationPlan();
|
||||
if (transitionPlan == null) {
|
||||
if (transitionPlan == null || !transitionPlan.hasTransitionAtChunk(worldX >> 4, worldZ >> 4)) {
|
||||
return;
|
||||
}
|
||||
TransitionGenerationPlan.TerrainSample terrainSample = transitionPlan.terrainSampleAt(worldX, worldZ);
|
||||
|
||||
@@ -82,8 +82,7 @@ public final class IrisDimensionStackActuator extends EngineAssignedActuator<Nat
|
||||
if (getEngine().getDimensionStackContext() == null) {
|
||||
return;
|
||||
}
|
||||
MantleChunk<Matter> mantleChunk = context.isSpeculativeTerrain() ? null
|
||||
: getEngine().getMantle().getMantle().getChunk(x >> 4, z >> 4).use();
|
||||
MantleChunk<Matter> mantleChunk = getEngine().getMantle().getMantle().getChunk(x >> 4, z >> 4).use();
|
||||
try {
|
||||
MetadataCleaner metadata = new MetadataCleaner(mantleChunk, blocks.getHeight());
|
||||
for (int localX = 0; localX < blocks.getWidth(); localX++) {
|
||||
|
||||
+1
-3
@@ -431,9 +431,7 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Nativ
|
||||
}
|
||||
context.setNaturalBiome(wx & 15, y, wz & 15, selected);
|
||||
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) {
|
||||
IrisLogging.reportError(e);
|
||||
|
||||
@@ -149,7 +149,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
||||
boolean riverOwned = padRiverBed && hydrologyTerrain != null && hydrologyTerrain.terrainOwned();
|
||||
IrisRiverMaterialConfig roleMaterial = hydrologyRoleMaterial(
|
||||
hydrologyTerrain, bedMaterial, shoreMaterial, bankMaterial);
|
||||
NativeBlockState fluid = hydrologyFluid == null
|
||||
NativeBlockState fluid = hydrologyFluid == null || complex.getTransitionDisplacement() != null
|
||||
? complex.resolveSurfaceFluid(realX, realZ)
|
||||
: complex.resolveHydrologyFluid(hydrologyFluid.profileKey(), realX, realZ);
|
||||
NativeBlockState rock = rockCache.get(xf, zf);
|
||||
|
||||
+1
-4
@@ -5,7 +5,6 @@ import art.arcane.iris.generation.runtime.EngineAssignedComponent;
|
||||
import art.arcane.iris.generation.runtime.EngineStage;
|
||||
import art.arcane.iris.world.history.BoundaryColumnGeometry;
|
||||
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.TransitionGenerationPlan;
|
||||
import art.arcane.iris.world.history.TransitionGeometryBlender;
|
||||
@@ -70,9 +69,7 @@ public final class IrisTransitionGeometryActuator extends EngineAssignedComponen
|
||||
if (blended == current) {
|
||||
continue;
|
||||
}
|
||||
double expectedFloor = GenerationBlend.interpolate(
|
||||
plan.terrainSampleAt(x + localX, z + localZ).historicalOceanFloorHeight(),
|
||||
context.getRoundedHeight(localX, localZ), influence.newTerrainWeight());
|
||||
double expectedFloor = context.getRoundedHeight(localX, localZ);
|
||||
int floor = blended.surfaceOffsetNear(expectedFloor);
|
||||
List<BoundaryColumnGeometry.Voxel> voxels = blended.voxels();
|
||||
for (int offset = 0; offset < voxels.size(); offset++) {
|
||||
|
||||
@@ -86,6 +86,30 @@ public final class Terrain3DColumn {
|
||||
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
|
||||
public boolean equals(Object value) {
|
||||
return this == value || value instanceof Terrain3DColumn other
|
||||
|
||||
+2
-1
@@ -98,7 +98,8 @@ public final class BukkitStructureStagePolicy implements StructureStagePolicy<
|
||||
|
||||
@Override
|
||||
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());
|
||||
}
|
||||
|
||||
|
||||
+1
-3
@@ -8,7 +8,6 @@ import art.arcane.iris.generation.runtime.IrisEngine;
|
||||
import art.arcane.iris.platform.generation.BukkitChunkGenerator;
|
||||
import art.arcane.iris.studio.generation.JigsawStudioGenerator;
|
||||
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
|
||||
import art.arcane.iris.world.history.TerrainBoundarySignature;
|
||||
import art.arcane.iris.world.history.TerrainNativeBlockKeys;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockColumn;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeTerrainColumnPolicy;
|
||||
@@ -99,8 +98,7 @@ public final class BukkitTerrainColumnPolicy implements NativeTerrainColumnPolic
|
||||
|
||||
@Override
|
||||
public Optional<? extends NativeBlockColumn> resolvedColumn() {
|
||||
return engine.getComplex().resolvedTerrainColumn(query.blockX(), query.blockZ())
|
||||
.map(TerrainBoundarySignature::geometry);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -94,7 +94,8 @@ public class ImportedFeaturePolicy implements NativeImportedFeaturePolicy<IrisDi
|
||||
|
||||
@Override
|
||||
public Placement placement() {
|
||||
IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData());
|
||||
IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData())
|
||||
.forTransition(engine);
|
||||
int minimumY = engine.getMinHeight();
|
||||
DimensionStackContext stack = engine.getDimensionStackContext();
|
||||
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.generation.runtime.Engine;
|
||||
import art.arcane.iris.world.history.TerrainBoundarySignature;
|
||||
|
||||
import java.util.Optional;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@@ -61,11 +59,6 @@ public interface IObjectPlacer {
|
||||
return 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(
|
||||
engine.getComplex().getRiverWaterSurfaceStream().get(x, z)
|
||||
);
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
package art.arcane.iris.structure.object;
|
||||
|
||||
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.pack.value.IrisPosition;
|
||||
|
||||
import art.arcane.iris.integration.Identifier;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
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.volmlib.nativelib.terrain.NativeBlockState;
|
||||
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.math.RNG;
|
||||
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.MatterSlice;
|
||||
import art.arcane.volmlib.util.matter.MatterStructurePOI;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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(
|
||||
TileWrapper.class, Identifier.class, String.class, TreeBlockMaterial.class,
|
||||
MatterMarker.class, MatterStructurePOI.class);
|
||||
|
||||
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.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) {
|
||||
@@ -45,7 +66,7 @@ public final class IrisStaticObjectLayer {
|
||||
if (dimension.getStaticObjects().isEmpty()) {
|
||||
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++) {
|
||||
IrisStaticObject entry = dimension.getStaticObjects().get(index);
|
||||
try {
|
||||
@@ -65,15 +86,48 @@ public final class IrisStaticObjectLayer {
|
||||
} else if (entry.isSmartBore()) {
|
||||
object = object.copy();
|
||||
}
|
||||
Compilation compilation = new Compilation(dimension.getMaxHeight() - dimension.getMinHeight());
|
||||
IrisPosition position = entry.getPosition();
|
||||
object.place(position.getX(), position.getY() - dimension.getMinHeight(),
|
||||
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) {
|
||||
throw new IllegalArgumentException("Invalid staticObjects[" + index + "] in dimension '"
|
||||
+ 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) {
|
||||
@@ -99,6 +153,7 @@ public final class IrisStaticObjectLayer {
|
||||
if (blocks.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
persistContinuations(engine, x >> 4, z >> 4, output.getHeight());
|
||||
MantleChunk<Matter> chunk = engine.getMantle().getMantle().getChunk(x >> 4, z >> 4);
|
||||
chunk.use();
|
||||
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) {
|
||||
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 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 final int height;
|
||||
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) {
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
private IrisStaticObjectLayer freeze() {
|
||||
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 new IrisStaticObjectLayer(Map.copyOf(compiled));
|
||||
private Placement freeze(ObjectContinuationBundle.PlacementKey key) {
|
||||
return new Placement(key, freezeChunks(chunks), chunks.isEmpty() ? null
|
||||
: new Footprint(minimumX, minimumZ, maximumX, maximumZ));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -166,6 +282,10 @@ public final class IrisStaticObjectLayer {
|
||||
if (state.isCustom() && (state.deferredPlacementKey() == null || state.placementBaseState() == null)) {
|
||||
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<>());
|
||||
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.structure.nativegen.NativeStructureSuppression;
|
||||
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.math.RNG;
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
@@ -589,7 +590,7 @@ public final class IrisStructureLocator {
|
||||
"assembled pieces exceed the configured structure or world bounds")) {
|
||||
return null;
|
||||
}
|
||||
if (!allowsResolvedFootprint(engine, placement, pieces)) {
|
||||
if (!allowsResolvedFootprint(engine, placement, structure, pieces)) {
|
||||
return null;
|
||||
}
|
||||
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) {
|
||||
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,
|
||||
@@ -909,6 +910,7 @@ public final class IrisStructureLocator {
|
||||
private static boolean allowsResolvedFootprint(
|
||||
Engine engine,
|
||||
IrisStructurePlacement placement,
|
||||
IrisStructure structure,
|
||||
KList<PlacedStructurePiece> pieces
|
||||
) {
|
||||
int[] bounds = computeBounds(pieces);
|
||||
@@ -921,6 +923,10 @@ public final class IrisStructureLocator {
|
||||
|| mode == StructureTerrainMode.BORE
|
||||
? Math.max(0, terrain.getHorizontalPadding())
|
||||
: 0;
|
||||
if (pieces.size() == 1 && !placement.isAnchoredUnderground()
|
||||
&& IrisObjectVacuum.isVacuumMode(structure.getPlaceMode())) {
|
||||
padding = Math.max(padding, IrisObjectVacuum.resolveRadius(structure.getPlaceMode(), null));
|
||||
}
|
||||
return engine.getComplex().allowsNewGenerationFootprint(
|
||||
saturatedOffset(bounds[0], -padding),
|
||||
saturatedOffset(bounds[2], -padding),
|
||||
|
||||
@@ -58,23 +58,6 @@ public final class GenerationBlend {
|
||||
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) {
|
||||
if (!Double.isFinite(weight) || weight < 0D || weight > 1D) {
|
||||
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 {
|
||||
private static final int MAXIMUM_CACHED_BOUNDARIES = 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_PENDING_SEMANTIC_CLAIMS = 128;
|
||||
|
||||
@@ -44,6 +45,7 @@ public final class GenerationHistory {
|
||||
private final GenerationAdmission admission;
|
||||
private final Map<Long, GenerationBoundary> boundaryCache;
|
||||
private final Map<Long, TerrainBoundarySignatureStore.Snapshot> terrainSignatureCache;
|
||||
private final Map<Long, TransitionGenerationPlan> transitionPlanCache;
|
||||
private final ArrayBlockingQueue<PendingSemanticClaim> pendingSemanticClaims =
|
||||
new ArrayBlockingQueue<>(MAXIMUM_PENDING_SEMANTIC_CLAIMS);
|
||||
|
||||
@@ -68,6 +70,7 @@ public final class GenerationHistory {
|
||||
this.admission = new GenerationAdmission(paths.dimensionRoot());
|
||||
this.boundaryCache = boundedCache(MAXIMUM_CACHED_BOUNDARIES);
|
||||
this.terrainSignatureCache = boundedCache(MAXIMUM_CACHED_TERRAIN_SIGNATURES);
|
||||
this.transitionPlanCache = boundedCache(MAXIMUM_CACHED_TRANSITION_PLANS);
|
||||
validateReferencedState(publication, true);
|
||||
}
|
||||
|
||||
@@ -872,6 +875,10 @@ public final class GenerationHistory {
|
||||
}
|
||||
|
||||
public synchronized TransitionGenerationPlan transitionPlan(long activationId) throws IOException {
|
||||
TransitionGenerationPlan cached = transitionPlanCache.get(activationId);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
GenerationActivation activation = requireActivation(activationId);
|
||||
if (activation.isInitial()) {
|
||||
throw new IllegalArgumentException("The initial generation activation has no transition plan.");
|
||||
@@ -888,7 +895,7 @@ public final class GenerationHistory {
|
||||
GenerationBoundary boundary = boundary(activationId);
|
||||
TerrainBoundarySignatureStore.Snapshot terrainSnapshot = terrainSignatures(activationId);
|
||||
requireTransitionSnapshotIdentities(activation, boundary, terrainSnapshot);
|
||||
return new TransitionGenerationPlan(
|
||||
TransitionGenerationPlan plan = new TransitionGenerationPlan(
|
||||
new TransitionGenerationPlan.Specification(
|
||||
activation.activationId(),
|
||||
requireEpoch(parent.epochId()).epochId(),
|
||||
@@ -901,6 +908,8 @@ public final class GenerationHistory {
|
||||
boundary,
|
||||
terrainSnapshot
|
||||
);
|
||||
transitionPlanCache.put(activationId, plan);
|
||||
return plan;
|
||||
}
|
||||
|
||||
public synchronized int explicitChunkCount() {
|
||||
|
||||
+87
-3
@@ -10,14 +10,21 @@ import art.arcane.iris.generation.runtime.IrisEngine;
|
||||
import art.arcane.iris.generation.runtime.IrisEngineMantle;
|
||||
import art.arcane.iris.generation.runtime.EngineTarget;
|
||||
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.volmlib.util.mantle.MantleRegionFiles;
|
||||
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.MatterSlice;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -32,6 +39,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
|
||||
private final IrisEngine engine;
|
||||
private final GenerationHistory history;
|
||||
private final SavedBiomeRuntime biomes;
|
||||
private final HistoricalObjectContinuations objectContinuations;
|
||||
private final GenerationBoundarySignatureSampler signatureSampler;
|
||||
private final ActivationRuntimeFactory runtimeFactory;
|
||||
private final LinkedHashMap<Long, RuntimeCacheEntry> bindings;
|
||||
@@ -65,6 +73,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
|
||||
this.operationDepth = ThreadLocal.withInitial(() -> 0);
|
||||
this.scopedRoute = new ThreadLocal<>();
|
||||
this.biomes = new SavedBiomeRuntime(engine, history);
|
||||
this.objectContinuations = new HistoricalObjectContinuations(history, this::readObjectContinuationRegion);
|
||||
|
||||
GenerationActivation active = history.activeActivation();
|
||||
GenerationEpoch epoch = requireEpoch(active);
|
||||
@@ -96,6 +105,41 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
|
||||
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(
|
||||
IrisEngine engine,
|
||||
GenerationHistory history,
|
||||
@@ -461,6 +505,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
|
||||
} catch (Throwable biomeFailure) {
|
||||
failure = appendFailure(failure, biomeFailure);
|
||||
}
|
||||
objectContinuations.clear();
|
||||
try {
|
||||
retireBindings(retired);
|
||||
} catch (Throwable retirementFailure) {
|
||||
@@ -493,6 +538,45 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
|
||||
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 {
|
||||
SavedMantleEntry saved = null;
|
||||
RuntimeCacheEntry runtime;
|
||||
@@ -526,7 +610,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
|
||||
if (runtime != null) {
|
||||
RuntimeLease lease = awaitRuntime(runtime, activation.activationId());
|
||||
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) {
|
||||
lease.close();
|
||||
throw failure;
|
||||
@@ -552,7 +636,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
|
||||
entry.leases--;
|
||||
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 {
|
||||
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,
|
||||
|
||||
@@ -112,7 +112,7 @@ final class GenerationKernelV1 implements GenerationKernelRegistry.RuntimeFactor
|
||||
public void registerStaticObjects(IrisEngine engine, EngineMode mode) {
|
||||
IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData());
|
||||
mode.registerStage((x, z, blocks, biomes, multicore, context) ->
|
||||
staticObjects.apply(engine, x, z, blocks));
|
||||
staticObjects.forTransition(engine).apply(engine, x, z, blocks));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -208,8 +208,8 @@ public final class GenerationSemanticCapture {
|
||||
}
|
||||
|
||||
private static boolean hasFullHydrologyWeight(IrisComplex complex, int blockX, int blockZ) {
|
||||
TransitionGenerationPlan transition = complex.getTransitionGenerationPlan();
|
||||
return transition == null || transition.hydrologyWeightAt(blockX, blockZ) == 1D;
|
||||
TransitionDisplacementField displacement = complex.getTransitionDisplacement();
|
||||
return displacement == null || displacement.seamWeight(blockX, blockZ) == 1D;
|
||||
}
|
||||
|
||||
private static void captureCave(
|
||||
|
||||
@@ -205,8 +205,8 @@ public final class GenerationSemanticQueries {
|
||||
}
|
||||
|
||||
private static boolean allowsActiveHydrologyPrediction(Engine engine, int blockX, int blockZ) {
|
||||
TransitionGenerationPlan transition = engine.getComplex().getTransitionGenerationPlan();
|
||||
if (transition != null && transition.hydrologyWeightAt(blockX, blockZ) != 1D) {
|
||||
TransitionDisplacementField displacement = engine.getComplex().getTransitionDisplacement();
|
||||
if (displacement != null && displacement.seamWeight(blockX, blockZ) != 1D) {
|
||||
return false;
|
||||
}
|
||||
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;
|
||||
|
||||
import art.arcane.iris.world.storage.Durability;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
@@ -225,14 +227,14 @@ public final class SavedBiomeStore {
|
||||
}
|
||||
}
|
||||
if (!unpublished) {
|
||||
output.getChannel().force(true);
|
||||
Durability.force(output.getChannel());
|
||||
}
|
||||
appendCompleted = true;
|
||||
} catch (IOException failure) {
|
||||
try {
|
||||
output.setLength(offset);
|
||||
if (!unpublished) {
|
||||
output.getChannel().force(true);
|
||||
Durability.force(output.getChannel());
|
||||
}
|
||||
} catch (IOException rollback) {
|
||||
failure.addSuppressed(rollback);
|
||||
@@ -467,7 +469,7 @@ public final class SavedBiomeStore {
|
||||
while (header.hasRemaining()) {
|
||||
output.write(header);
|
||||
}
|
||||
output.force(true);
|
||||
Durability.force(output);
|
||||
}
|
||||
Files.move(temporary, region.path, StandardCopyOption.ATOMIC_MOVE);
|
||||
forceDirectory(directory);
|
||||
@@ -477,11 +479,11 @@ public final class SavedBiomeStore {
|
||||
}
|
||||
|
||||
private static void forceDirectory(Path path) throws IOException {
|
||||
if (File.separatorChar == '\\') {
|
||||
if (File.separatorChar == '\\' || !Durability.enabled()) {
|
||||
return;
|
||||
}
|
||||
try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
|
||||
channel.force(true);
|
||||
Durability.force(channel);
|
||||
} catch (UnsupportedOperationException 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);
|
||||
}
|
||||
}
|
||||
output.force(true);
|
||||
Durability.force(output);
|
||||
}
|
||||
Files.move(temporary, region.path, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
|
||||
synchronized (regions) {
|
||||
@@ -591,7 +593,7 @@ public final class SavedBiomeStore {
|
||||
|
||||
private static void truncateTail(RandomAccessFile file, long length) throws IOException {
|
||||
file.setLength(length);
|
||||
file.getChannel().force(true);
|
||||
Durability.force(file.getChannel());
|
||||
}
|
||||
|
||||
private static int checksum(byte[] bytes) {
|
||||
|
||||
+28
-45
@@ -48,6 +48,7 @@ import java.util.PriorityQueue;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.concurrent.atomic.LongAdder;
|
||||
import java.util.zip.CRC32;
|
||||
|
||||
@@ -243,11 +244,6 @@ public final class TerrainBoundarySignatureStore {
|
||||
|
||||
long[] cellKeys = new long[cellCount];
|
||||
Long2ObjectOpenHashMap<CellReference> cells = new Long2ObjectOpenHashMap<>(cellCount);
|
||||
LinkedHashMap<String, List<TerrainBoundarySignature>> validated = new LinkedHashMap<>(
|
||||
MAXIMUM_CACHED_SHARDS,
|
||||
0.75F,
|
||||
true
|
||||
);
|
||||
int countedEntries = 0;
|
||||
long previousCellKey = 0L;
|
||||
for (int index = 0; index < cellCount; index++) {
|
||||
@@ -266,19 +262,6 @@ public final class TerrainBoundarySignatureStore {
|
||||
throw invalid(snapshot, "truncated shard hash");
|
||||
}
|
||||
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);
|
||||
cellKeys[index] = cellKey;
|
||||
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) {
|
||||
while (cache.size() > MAXIMUM_CACHED_SHARDS) {
|
||||
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) {
|
||||
try {
|
||||
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 {
|
||||
int size = CELL_SIZE * SUPER_CELL_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) {
|
||||
CandidateIndex candidates = chunkCache(blockX, blockZ).candidates;
|
||||
Nearest nearest = candidates.nearest(blockX, blockZ, square(terrainWidth));
|
||||
if (nearest.count == 0 || nearest.distancesSquared[0] >= square(terrainWidth)) {
|
||||
int seamWidth = Math.min(4, terrainWidth);
|
||||
Nearest nearest = candidates.nearest(blockX, blockZ, square(seamWidth));
|
||||
if (nearest.count == 0 || nearest.distancesSquared[0] >= square(seamWidth)) {
|
||||
return BoundaryGeometryInfluence.none();
|
||||
}
|
||||
double distance = Math.sqrt(nearest.distancesSquared[0]);
|
||||
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;
|
||||
double total = 0D;
|
||||
for (int index = 0; index < count; index++) {
|
||||
@@ -77,10 +78,7 @@ final class TransitionBoundarySampler {
|
||||
nearest.signatures[index].geometry(),
|
||||
(1D / Math.max(1D, nearest.distancesSquared[index])) / total));
|
||||
}
|
||||
int openingDepth = Math.min(8, Math.max(1, terrainWidth / 4));
|
||||
double openingWeight = GenerationBlend.newEpochWeight(Math.max(0D, distance - openingDepth),
|
||||
Math.max(1, terrainWidth - openingDepth));
|
||||
return new BoundaryGeometryInfluence(weight, openingWeight, contributions);
|
||||
return new BoundaryGeometryInfluence(weight, weight, contributions);
|
||||
}
|
||||
|
||||
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 TerrainBoundarySignatureStore.Snapshot terrainSignatures;
|
||||
private final TransitionBoundarySampler terrainSampler;
|
||||
private final TransitionBandIndex terrainBand;
|
||||
private final TerrainSample newTerrain;
|
||||
|
||||
public TransitionGenerationPlan(
|
||||
Specification specification,
|
||||
@@ -34,6 +36,8 @@ public final class TransitionGenerationPlan {
|
||||
specification.widthBlocks(),
|
||||
terrainSignatures
|
||||
);
|
||||
terrainBand = new TransitionBandIndex(boundary, terrainSampler);
|
||||
newTerrain = TerrainSample.newTerrain(specification.widthBlocks());
|
||||
}
|
||||
|
||||
public long activationId() {
|
||||
@@ -93,18 +97,11 @@ public final class TransitionGenerationPlan {
|
||||
}
|
||||
|
||||
public boolean hasTransitionAtChunk(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 terrainSampler.intersectsTerrainBand(minimumX, minimumZ,
|
||||
Math.addExact(minimumX, GenerationBoundary.CHUNK_SIZE - 1),
|
||||
Math.addExact(minimumZ, GenerationBoundary.CHUNK_SIZE - 1));
|
||||
return terrainBand.contains(chunkX, chunkZ);
|
||||
}
|
||||
|
||||
public BoundaryGeometryInfluence geometryAt(int blockX, int blockZ) {
|
||||
if (boundary.isHistoricalBlock(blockX, blockZ)) {
|
||||
if (!terrainBand.contains(blockX >> 4, blockZ >> 4)) {
|
||||
return BoundaryGeometryInfluence.none();
|
||||
}
|
||||
return terrainSampler.geometryAt(blockX, blockZ);
|
||||
@@ -114,7 +111,7 @@ public final class TransitionGenerationPlan {
|
||||
if (boundary.isHistoricalBlock(blockX, blockZ)) {
|
||||
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) {
|
||||
@@ -128,12 +125,7 @@ public final class TransitionGenerationPlan {
|
||||
TerrainSample sample
|
||||
) {
|
||||
Objects.requireNonNull(sample, "Terrain sample");
|
||||
if (sample.newEpochWeight() == 1D) {
|
||||
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)
|
||||
return sample.distanceToHistoricalTerrain() < Math.min(4, widthBlocks())
|
||||
? sample.historicalPhysicalBiomeKeyAt(blockY) : Optional.empty();
|
||||
}
|
||||
|
||||
|
||||
@@ -5,11 +5,8 @@ import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class TransitionGeometryBlender {
|
||||
private static final int MAXIMUM_OPENING_HEIGHT = 64;
|
||||
private static final BoundaryColumnGeometry.Voxel AIR = new BoundaryColumnGeometry.Voxel(
|
||||
"minecraft:air", BoundaryColumnGeometry.Phase.AIR, "", false);
|
||||
private static final BoundaryColumnGeometry.Voxel FLUID_BARRIER = new BoundaryColumnGeometry.Voxel(
|
||||
"minecraft:obsidian", BoundaryColumnGeometry.Phase.SOLID, "", false);
|
||||
|
||||
private TransitionGeometryBlender() {
|
||||
}
|
||||
@@ -49,8 +46,7 @@ public final class TransitionGeometryBlender {
|
||||
boolean changed = false;
|
||||
for (int offset = 0; offset < currentGeometry.height(); offset++) {
|
||||
BoundaryColumnGeometry.Voxel value = blendVoxel(current, historical, offset,
|
||||
influence.newTerrainWeight(), influence.openingWeight(), GenerationBlend.usesHistoricalMaterial(blockX,
|
||||
currentGeometry.minimumY() + offset, blockZ, influence.newTerrainWeight()));
|
||||
influence.newTerrainWeight());
|
||||
result.add(value);
|
||||
changed |= !value.equals(current.voxelAt(offset));
|
||||
}
|
||||
@@ -61,88 +57,58 @@ public final class TransitionGeometryBlender {
|
||||
ColumnProfile current,
|
||||
List<WeightedProfile> historical,
|
||||
int offset,
|
||||
double currentWeight,
|
||||
double openingWeight,
|
||||
boolean historicalMaterial
|
||||
double currentWeight
|
||||
) {
|
||||
BoundaryColumnGeometry.Voxel currentVoxel = current.voxelAt(offset);
|
||||
if (currentVoxel.protectedContent()) {
|
||||
if (currentVoxel.protectedContent() || offset == 0) {
|
||||
return currentVoxel;
|
||||
}
|
||||
boolean historicalOpening = false;
|
||||
double solid = 0D;
|
||||
double fluid = 0D;
|
||||
double total = 0D;
|
||||
double solidMaterialWeight = -1D;
|
||||
double fluidMaterialWeight = -1D;
|
||||
BoundaryColumnGeometry.Voxel oldSolid = null;
|
||||
BoundaryColumnGeometry.Voxel oldFluid = null;
|
||||
double fluidWeight = 0D;
|
||||
for (WeightedProfile contribution : historical) {
|
||||
ColumnProfile profile = contribution.profile();
|
||||
if (profile.voxelAt(offset).protectedContent()) {
|
||||
BoundaryColumnGeometry.Voxel oldVoxel = profile.voxelAt(offset);
|
||||
if (oldVoxel.protectedContent()) {
|
||||
continue;
|
||||
}
|
||||
historicalOpening |= profile.enclosedOpeningAt(offset);
|
||||
double weight = contribution.weight();
|
||||
total += weight;
|
||||
solid += profile.solid.distanceAt(offset) * weight;
|
||||
fluid += profile.fluid.distanceAt(offset) * weight;
|
||||
if (profile.solid.materialAt(offset) != null && weight > solidMaterialWeight) {
|
||||
total += contribution.weight();
|
||||
solid += profile.solid.distanceAt(offset) * contribution.weight();
|
||||
if (oldSolid == null) {
|
||||
oldSolid = profile.solid.materialAt(offset);
|
||||
solidMaterialWeight = weight;
|
||||
}
|
||||
if (profile.fluid.materialAt(offset) != null && weight > fluidMaterialWeight) {
|
||||
oldFluid = profile.fluid.materialAt(offset);
|
||||
fluidMaterialWeight = weight;
|
||||
if (oldVoxel.phase() == BoundaryColumnGeometry.Phase.FLUID
|
||||
&& profile.geometry.hasSolidAbove(profile.geometry.minimumY() + offset)
|
||||
&& contribution.weight() > fluidWeight) {
|
||||
oldFluid = oldVoxel;
|
||||
fluidWeight = contribution.weight();
|
||||
}
|
||||
}
|
||||
if (total == 0D) {
|
||||
return currentVoxel;
|
||||
}
|
||||
double blendedSolid = GenerationBlend.interpolate(solid / total,
|
||||
current.solid.distanceAt(offset), historicalOpening ? openingWeight : currentWeight);
|
||||
current.solid.distanceAt(offset), currentWeight);
|
||||
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,
|
||||
current.fluid.distanceAt(offset), historicalOpening ? openingWeight : currentWeight);
|
||||
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);
|
||||
if (currentVoxel.phase() == BoundaryColumnGeometry.Phase.FLUID) {
|
||||
return currentVoxel;
|
||||
}
|
||||
return 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;
|
||||
return oldFluid != null && currentWeight < 0.5D ? oldFluid : AIR;
|
||||
}
|
||||
|
||||
private static final class ColumnProfile {
|
||||
private final BoundaryColumnGeometry geometry;
|
||||
private final PhaseCursor solid;
|
||||
private final PhaseCursor fluid;
|
||||
private int run;
|
||||
|
||||
private ColumnProfile(BoundaryColumnGeometry geometry) {
|
||||
this.geometry = geometry;
|
||||
solid = new PhaseCursor(geometry, BoundaryColumnGeometry.Phase.SOLID);
|
||||
fluid = new PhaseCursor(geometry, BoundaryColumnGeometry.Phase.FLUID);
|
||||
}
|
||||
|
||||
private BoundaryColumnGeometry.Voxel voxelAt(int offset) {
|
||||
@@ -152,12 +118,6 @@ public final class TransitionGeometryBlender {
|
||||
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 {
|
||||
|
||||
@@ -40,6 +40,7 @@ public final class IrisMatterSupport {
|
||||
}
|
||||
|
||||
IrisMatter.registerSliceType(new EntityMatter());
|
||||
IrisMatter.registerSliceType(new ObjectContinuationMatter());
|
||||
IrisMatter.registerSliceType("art.arcane.iris.core.link.Identifier", new IdentifierMatter());
|
||||
IrisMatter.registerSliceType("art.arcane.iris.engine.framework.NativeStructureOwnershipBundle", new NativeStructureOwnershipMatter());
|
||||
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);
|
||||
}
|
||||
}
|
||||
+61
-2
@@ -22,9 +22,17 @@ import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.block.TileData;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
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.assertSame;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
@@ -37,7 +45,8 @@ public class FloatingObjectPlacementTransactionTest {
|
||||
@Test
|
||||
public void unsupportedFinalTerrainRejectsBlocksTilesAndMarkersTogether() {
|
||||
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 second = mock(NativeBlockState.class);
|
||||
TileData tile = mock(TileData.class);
|
||||
@@ -63,7 +72,8 @@ public class FloatingObjectPlacementTransactionTest {
|
||||
@Test
|
||||
public void supportedNeighborChunkCommitsBlocksTilesAndMarkers() {
|
||||
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 second = mock(NativeBlockState.class);
|
||||
TileData tile = mock(TileData.class);
|
||||
@@ -86,10 +96,59 @@ public class FloatingObjectPlacementTransactionTest {
|
||||
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) {
|
||||
IslandObjectPlacer placer = mock(IslandObjectPlacer.class);
|
||||
Engine engine = mock(Engine.class);
|
||||
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.canWriteObjectBlock(anyInt(), anyInt(), anyInt())).thenAnswer(invocation -> {
|
||||
int x = invocation.getArgument(0);
|
||||
|
||||
-16
@@ -1,7 +1,5 @@
|
||||
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.hydrology.cave.HydrologyCaveAction;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
|
||||
@@ -12,20 +10,6 @@ import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
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
|
||||
public void biomeOwnedPlacementsRejectForeignCaveBands() {
|
||||
IrisBiome frozen = biome("carving/ice");
|
||||
|
||||
@@ -158,6 +158,17 @@ public class MantleWriterOverlayTest {
|
||||
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
|
||||
public void protectedHydrologyRejectsLaterBlockAndCavernWrites() {
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
-28
@@ -4,8 +4,6 @@ import art.arcane.iris.integration.Identifier;
|
||||
import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
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.HydrologyCaveCell;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
@@ -25,9 +23,7 @@ import org.junit.Test;
|
||||
import org.mockito.InOrder;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
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
|
||||
public void failedCommitRestoresEveryTouchedDestinationCell() {
|
||||
MantleWriter writer = writer();
|
||||
|
||||
+24
-30
@@ -2,8 +2,10 @@ package art.arcane.iris.generation.mantle;
|
||||
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
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.runtime.EngineMode;
|
||||
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.IrisPlatforms;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
@@ -21,26 +23,21 @@ import org.junit.Before;
|
||||
import org.junit.ClassRule;
|
||||
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.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
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.eq;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
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 class ResolvedTerrainPlacementTest {
|
||||
public class TerrainPlacementQueryTest {
|
||||
@ClassRule
|
||||
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
|
||||
|
||||
@@ -67,27 +64,19 @@ public class ResolvedTerrainPlacementTest {
|
||||
IrisPlatforms.bind(platform);
|
||||
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);
|
||||
when(complex.resolvedTerrainColumn(anyInt(), anyInt())).thenReturn(Optional.of(column));
|
||||
when(complex.resolvedTerrainHeight(anyInt(), anyInt(), eq(true)))
|
||||
.thenReturn(OptionalInt.of(2));
|
||||
when(complex.resolvedTerrainHeight(anyInt(), anyInt(), eq(false)))
|
||||
.thenReturn(OptionalInt.of(5));
|
||||
ProceduralStream<Integer> heights = mock(ProceduralStream.class);
|
||||
when(heights.get(anyDouble(), anyDouble())).thenReturn(2);
|
||||
when(complex.getRoundedHeighteightStream()).thenReturn(heights);
|
||||
when(complex.getRiverWaterSurfaceStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 5D));
|
||||
Engine engine = mock(Engine.class);
|
||||
when(engine.getMinHeight()).thenReturn(-64);
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
mantle = mock(Mantle.class);
|
||||
when(mantle.getLoadedRegions()).thenReturn(new KMap<>());
|
||||
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);
|
||||
when(mantle.getChunk(0, 0)).thenReturn(chunk);
|
||||
when(chunk.use()).thenReturn(chunk);
|
||||
@@ -105,7 +94,7 @@ public class ResolvedTerrainPlacementTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void placementUsesResolvedSurfaceFluidAndEnclosedOccupancy() {
|
||||
public void placementUsesTerrainStreamsAndRecordedCarving() {
|
||||
assertEquals(2, engineMantle.getHighest(0, 0, null, true));
|
||||
assertEquals(5, engineMantle.getHighest(0, 0, null, false));
|
||||
assertEquals(5, engineMantle.getFluidHeight(0, 0));
|
||||
@@ -116,22 +105,27 @@ public class ResolvedTerrainPlacementTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void scalarPlacementQueriesDoNotReloadColumnGeometry() {
|
||||
public void scalarPlacementQueriesDoNotGenerateTerrain() {
|
||||
Engine engine = mock(Engine.class, CALLS_REAL_METHODS);
|
||||
IrisComplex complex = engineMantle.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(5, engine.getHeight(0, 0, false));
|
||||
assertEquals(2, engineMantle.getHighest(0, 0, null, true));
|
||||
assertEquals(5, engineMantle.getHighest(0, 0, null, false));
|
||||
verify(complex, never()).resolvedTerrainColumn(anyInt(), anyInt());
|
||||
verifyNoInteractions(mode);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void writerPrerequisitesIgnoreContentWhileLiveOccupancyIncludesBlockOnlyFills() {
|
||||
public void writerPrerequisitesRetainNaturalCarvingBeforeContentFills() {
|
||||
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)) {
|
||||
assertSame(air, writer.getPrerequisiteBlock(0, 1, 0));
|
||||
assertSame(stone, writer.get(0, 1, 0));
|
||||
+12
-1
@@ -1,6 +1,10 @@
|
||||
package art.arcane.iris.generation.runtime;
|
||||
|
||||
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.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
@@ -291,12 +295,19 @@ public class DimensionTerrain3DCompositionTest {
|
||||
return blocks;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Hunk<NativeBlockState> render(DimensionStackLayout layout, int height) {
|
||||
Engine engine = mock(Engine.class);
|
||||
when(engine.getSeedManager()).thenReturn(mock(SeedManager.class));
|
||||
when(engine.getDimensionStackContext()).thenReturn(mock(DimensionStackContext.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);
|
||||
Hunk<NativeBlockState> output = Hunk.newArrayHunk(1, height, 1);
|
||||
for (int y = 0; y < height; y++) {
|
||||
|
||||
+268
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
-1
@@ -316,7 +316,6 @@ public class IrisComplexHydrologyNoiseCacheTest {
|
||||
complex.setBaseTerrainHeightStream(height);
|
||||
complex.setUnblendedNaturalHeightStream(height);
|
||||
complex.setHydrologyRuntime(hydrology);
|
||||
setField(complex, "resolvedTerrain", mock(ResolvedTerrainProvider.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+15
@@ -11,6 +11,7 @@ import art.arcane.iris.generation.context.IrisContext;
|
||||
import art.arcane.volmlib.util.stream.ProceduralStream;
|
||||
import art.arcane.volmlib.util.stream.interpolation.Interpolated;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.iris.world.history.TransitionDisplacementField;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
@@ -86,6 +87,20 @@ public class IrisComplexNaturalBiomeReuseTest {
|
||||
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 final IrisComplex complex = mock(IrisComplex.class, CALLS_REAL_METHODS);
|
||||
private final IrisImageMapRuntime images = mock(IrisImageMapRuntime.class);
|
||||
|
||||
+151
-5
@@ -4,12 +4,22 @@ import art.arcane.iris.generation.hydrology.HydrologyColumnLayer;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyColumnSample;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyFeatureRef;
|
||||
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 java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
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.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class IrisComplexTransitionTest {
|
||||
@Test
|
||||
@@ -20,13 +30,13 @@ public class IrisComplexTransitionTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tapersHydrologyGeometryTowardNaturalTerrain() {
|
||||
public void tapersRiverDepthWithoutRaisingItsFluidHead() {
|
||||
HydrologyColumnSample tapered = IrisComplex.taperHydrologySample(hydrologySample(), 0.5D);
|
||||
HydrologyColumnLayer layer = tapered.layers().getFirst();
|
||||
|
||||
assertEquals(73, layer.bedY());
|
||||
assertEquals(75, layer.fluidHeadY());
|
||||
assertEquals(75, layer.ceilingY());
|
||||
assertEquals(70, layer.fluidHeadY());
|
||||
assertEquals(70, layer.ceilingY());
|
||||
assertEquals(73, tapered.terrainHeight());
|
||||
assertEquals("river", layer.profileKey());
|
||||
}
|
||||
@@ -55,8 +65,144 @@ public class IrisComplexTransitionTest {
|
||||
|
||||
assertSame(cave, tapered.layers().getFirst());
|
||||
assertEquals(73, tapered.layers().getLast().bedY());
|
||||
assertEquals(75, tapered.layers().getLast().fluidHeadY());
|
||||
assertEquals(75, tapered.layers().getLast().ceilingY());
|
||||
assertEquals(70, tapered.layers().getLast().fluidHeadY());
|
||||
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() {
|
||||
|
||||
-517
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-6
@@ -41,7 +41,7 @@ public class UpperDimensionTransitionTest {
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
Terrain3DColumn terrain = Terrain3DColumnFixtures.spans(80, 0, 30, 40, 60, 100, 110);
|
||||
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));
|
||||
Engine engine = mock(Engine.class);
|
||||
when(engine.getDimension()).thenReturn(dimension);
|
||||
@@ -62,7 +62,7 @@ public class UpperDimensionTransitionTest {
|
||||
when(dimension.getLoadKey()).thenReturn("main");
|
||||
when(dimension.getUpperDimensionGap()).thenReturn(32);
|
||||
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));
|
||||
TransitionGenerationPlan plan = mock(TransitionGenerationPlan.class);
|
||||
when(complex.getTransitionGenerationPlan()).thenReturn(plan);
|
||||
@@ -80,12 +80,12 @@ public class UpperDimensionTransitionTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void upperTerrainUsesUnblendedCurrentGeneratorBeforeGeometryReconciliation() {
|
||||
public void upperTerrainUsesDisplacedNaturalGeneratorBeforeMirroring() {
|
||||
IrisDimension dimension = mock(IrisDimension.class);
|
||||
when(dimension.getLoadKey()).thenReturn("main");
|
||||
when(dimension.getUpperDimensionGap()).thenReturn(32);
|
||||
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));
|
||||
TransitionGenerationPlan plan = mock(TransitionGenerationPlan.class);
|
||||
when(complex.getTransitionGenerationPlan()).thenReturn(plan);
|
||||
@@ -98,8 +98,7 @@ public class UpperDimensionTransitionTest {
|
||||
|
||||
assertEquals(175, context.getEffectiveSurfaceY(-17, 8));
|
||||
verify(plan, never()).terrainSampleAt(anyInt(), anyInt());
|
||||
verify(complex, never()).getNaturalHeightStream();
|
||||
verify(complex, never()).getNaturalTrueBiomeStream();
|
||||
verify(complex, never()).getUnblendedNaturalHeightStream();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+6
-2
@@ -55,10 +55,12 @@ public class IrisBiomeActuatorCoordinateTest {
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void speculativeTerrainDoesNotPublishPersistentMetadata() {
|
||||
public void historicalChunkDoesNotPublishNewPersistentMetadata() {
|
||||
Engine engine = mock(Engine.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);
|
||||
|
||||
@@ -119,6 +121,7 @@ public class IrisBiomeActuatorCoordinateTest {
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
when(complex.allowsNewDiscreteContentAt(100, -200)).thenReturn(false);
|
||||
TransitionGenerationPlan transitionPlan = mock(TransitionGenerationPlan.class);
|
||||
when(transitionPlan.hasTransitionAtChunk(6, -13)).thenReturn(true);
|
||||
TransitionGenerationPlan.TerrainSample terrainSample = mock(TransitionGenerationPlan.TerrainSample.class);
|
||||
when(complex.getTransitionGenerationPlan()).thenReturn(transitionPlan);
|
||||
when(transitionPlan.terrainSampleAt(100, -200)).thenReturn(terrainSample);
|
||||
@@ -154,6 +157,7 @@ public class IrisBiomeActuatorCoordinateTest {
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
when(complex.allowsNewDiscreteContentAt(100, -200)).thenReturn(false);
|
||||
TransitionGenerationPlan transitionPlan = mock(TransitionGenerationPlan.class);
|
||||
when(transitionPlan.hasTransitionAtChunk(6, -13)).thenReturn(true);
|
||||
TransitionGenerationPlan.TerrainSample terrainSample = mock(TransitionGenerationPlan.TerrainSample.class);
|
||||
when(complex.getTransitionGenerationPlan()).thenReturn(transitionPlan);
|
||||
when(transitionPlan.terrainSampleAt(100, -200)).thenReturn(terrainSample);
|
||||
|
||||
+6
-4
@@ -26,7 +26,6 @@ import java.util.List;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -110,14 +109,16 @@ public class IrisTransitionGeometryActuatorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void refusesUnresolvableRetainedMaterialInsteadOfReplacingItWithAir() {
|
||||
public void usesNativeMaterialWhenHistoricalPaletteIsUnavailable() {
|
||||
Engine engine = mock(Engine.class);
|
||||
ChunkContext context = context("removed:stone");
|
||||
Hunk<NativeBlockState> blocks = filled();
|
||||
Hunk<NativeBiome> biomes = Hunk.newArrayHunk(16, 16, 16);
|
||||
|
||||
assertThrows(IllegalStateException.class, () -> new IrisTransitionGeometryActuator(engine)
|
||||
.generate(16, 0, blocks, biomes, false, context));
|
||||
new IrisTransitionGeometryActuator(engine).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) {
|
||||
@@ -139,6 +140,7 @@ public class IrisTransitionGeometryActuatorTest {
|
||||
when(complex.getTransitionGenerationPlan()).thenReturn(plan);
|
||||
ChunkContext context = mock(ChunkContext.class);
|
||||
when(context.getComplex()).thenReturn(complex);
|
||||
when(context.getRoundedHeight(anyInt(), anyInt())).thenReturn(3);
|
||||
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());
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -37,7 +37,7 @@ public class Terrain3DSurfaceSlopeTest {
|
||||
public void neighboringContinuousRiverTerrainUsesItsPlacementHeight() {
|
||||
IrisComplex complex = complex();
|
||||
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));
|
||||
|
||||
assertEquals(5D, complex.terrainSurfaceSlope(0, 60, 0), 0D);
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
package art.arcane.iris.structure.object;
|
||||
|
||||
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.pack.value.IrisPosition;
|
||||
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.pack.loading.ResourceLoader;
|
||||
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.spi.IrisPlatform;
|
||||
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.MantleChunk;
|
||||
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 org.junit.After;
|
||||
import org.junit.Before;
|
||||
@@ -30,10 +40,12 @@ import org.junit.Test;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
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.anyString;
|
||||
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.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -59,6 +73,7 @@ public class IrisStaticObjectLayerTest {
|
||||
IrisPlatforms.unbind();
|
||||
PlatformRegistries registries = mock(PlatformRegistries.class);
|
||||
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);
|
||||
when(platform.registries()).thenReturn(registries);
|
||||
IrisPlatforms.bind(platform);
|
||||
@@ -174,7 +189,9 @@ public class IrisStaticObjectLayerTest {
|
||||
when(engineMantle.getMantle()).thenReturn(mantle);
|
||||
when(mantle.getChunk(anyInt(), anyInt())).thenReturn(chunk);
|
||||
when(chunk.getOrCreate(anyInt())).thenReturn(section);
|
||||
when(chunk.use()).thenReturn(chunk);
|
||||
when(section.getSlice(TileWrapper.class)).thenReturn(tiles);
|
||||
when(chunk.isFlagged(MantleFlag.REAL)).thenReturn(true);
|
||||
Hunk<NativeBlockState> left = 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);
|
||||
}
|
||||
|
||||
@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
|
||||
public void placementKeepsSavedTilesWithoutSharingTheirInstances() {
|
||||
IrisObject object = object("chest", 1, 1, 1);
|
||||
@@ -214,6 +329,103 @@ public class IrisStaticObjectLayerTest {
|
||||
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
|
||||
public void repeatedCompilationDoesNotChangeSavedBlocksOrPlacement() {
|
||||
IrisObject object = object("line", 3, 1, 1);
|
||||
|
||||
+35
@@ -565,6 +565,41 @@ public class IrisStructureLocatorContractTest {
|
||||
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
|
||||
public void undergroundAssemblyIsShiftedToFitOrRejectedWhole() {
|
||||
IrisStructurePlacement placement = new IrisStructurePlacement();
|
||||
|
||||
+26
-81
@@ -5,12 +5,8 @@ import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
final class DenseTransitionGeometryReference {
|
||||
private static final int MAXIMUM_OPENING_HEIGHT = 64;
|
||||
private static final BoundaryColumnGeometry.Voxel AIR = new BoundaryColumnGeometry.Voxel(
|
||||
"minecraft:air", BoundaryColumnGeometry.Phase.AIR, "", false);
|
||||
private static final BoundaryColumnGeometry.Voxel FLUID_BARRIER = new BoundaryColumnGeometry.Voxel(
|
||||
"minecraft:obsidian", BoundaryColumnGeometry.Phase.SOLID, "", false);
|
||||
|
||||
private DenseTransitionGeometryReference() {
|
||||
}
|
||||
|
||||
@@ -39,8 +35,7 @@ final class DenseTransitionGeometryReference {
|
||||
boolean changed = false;
|
||||
for (int offset = 0; offset < currentGeometry.height(); offset++) {
|
||||
BoundaryColumnGeometry.Voxel value = blendVoxel(current, historical, offset,
|
||||
influence.newTerrainWeight(), influence.openingWeight(), GenerationBlend.usesHistoricalMaterial(blockX,
|
||||
currentGeometry.minimumY() + offset, blockZ, influence.newTerrainWeight()));
|
||||
influence.newTerrainWeight());
|
||||
result.add(value);
|
||||
changed |= !value.equals(current.voxels.get(offset));
|
||||
}
|
||||
@@ -48,79 +43,45 @@ final class DenseTransitionGeometryReference {
|
||||
}
|
||||
|
||||
private static BoundaryColumnGeometry.Voxel blendVoxel(
|
||||
ColumnProfile current,
|
||||
List<WeightedProfile> historical,
|
||||
int offset,
|
||||
double currentWeight,
|
||||
double openingWeight,
|
||||
boolean historicalMaterial
|
||||
ColumnProfile current, List<WeightedProfile> historical, int offset, double currentWeight
|
||||
) {
|
||||
BoundaryColumnGeometry.Voxel currentVoxel = current.voxels.get(offset);
|
||||
if (currentVoxel.protectedContent()) {
|
||||
if (currentVoxel.protectedContent() || offset == 0) {
|
||||
return currentVoxel;
|
||||
}
|
||||
boolean historicalOpening = false;
|
||||
double solid = 0D;
|
||||
double fluid = 0D;
|
||||
double total = 0D;
|
||||
double solidMaterialWeight = -1D;
|
||||
double fluidMaterialWeight = -1D;
|
||||
double fluidWeight = 0D;
|
||||
BoundaryColumnGeometry.Voxel oldSolid = null;
|
||||
BoundaryColumnGeometry.Voxel oldFluid = null;
|
||||
for (WeightedProfile contribution : historical) {
|
||||
ColumnProfile profile = contribution.profile();
|
||||
if (profile.voxels.get(offset).protectedContent()) {
|
||||
BoundaryColumnGeometry.Voxel voxel = profile.voxels.get(offset);
|
||||
if (voxel.protectedContent()) {
|
||||
continue;
|
||||
}
|
||||
historicalOpening |= profile.enclosedOpenings[offset];
|
||||
double weight = contribution.weight();
|
||||
total += weight;
|
||||
solid += profile.solidDistances[offset] * weight;
|
||||
fluid += profile.fluidDistances[offset] * weight;
|
||||
if (profile.solidMaterials[offset] != null && weight > solidMaterialWeight) {
|
||||
total += contribution.weight();
|
||||
solid += profile.solidDistances[offset] * contribution.weight();
|
||||
if (oldSolid == null) {
|
||||
oldSolid = profile.solidMaterials[offset];
|
||||
solidMaterialWeight = weight;
|
||||
}
|
||||
if (profile.fluidMaterials[offset] != null && weight > fluidMaterialWeight) {
|
||||
oldFluid = profile.fluidMaterials[offset];
|
||||
fluidMaterialWeight = weight;
|
||||
if (voxel.phase() == BoundaryColumnGeometry.Phase.FLUID
|
||||
&& profile.solidAbove[offset] && contribution.weight() > fluidWeight) {
|
||||
oldFluid = voxel;
|
||||
fluidWeight = contribution.weight();
|
||||
}
|
||||
}
|
||||
if (total == 0D) {
|
||||
return currentVoxel;
|
||||
}
|
||||
double blendedSolid = GenerationBlend.interpolate(solid / total,
|
||||
current.solidDistances[offset], historicalOpening ? openingWeight : currentWeight);
|
||||
if (blendedSolid > 0D) {
|
||||
return selectMaterial(oldSolid, current.solidMaterials[offset], historicalMaterial);
|
||||
if (GenerationBlend.interpolate(solid / total, current.solidDistances[offset], currentWeight) > 0D) {
|
||||
BoundaryColumnGeometry.Voxel material = current.solidMaterials[offset];
|
||||
return material != null ? material : oldSolid != null ? oldSolid : currentVoxel;
|
||||
}
|
||||
double blendedFluid = GenerationBlend.interpolate(fluid / total,
|
||||
current.fluidDistances[offset], historicalOpening ? openingWeight : currentWeight);
|
||||
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);
|
||||
if (currentVoxel.phase() == BoundaryColumnGeometry.Phase.FLUID) {
|
||||
return currentVoxel;
|
||||
}
|
||||
return 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;
|
||||
return oldFluid != null && currentWeight < 0.5D ? oldFluid : AIR;
|
||||
}
|
||||
|
||||
private static BoundaryColumnGeometry.Voxel[] nearestMaterials(
|
||||
@@ -155,38 +116,22 @@ final class DenseTransitionGeometryReference {
|
||||
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 final List<BoundaryColumnGeometry.Voxel> voxels;
|
||||
private final boolean[] enclosedOpenings;
|
||||
private final boolean[] solidAbove;
|
||||
private final double[] solidDistances;
|
||||
private final double[] fluidDistances;
|
||||
private final BoundaryColumnGeometry.Voxel[] solidMaterials;
|
||||
private final BoundaryColumnGeometry.Voxel[] fluidMaterials;
|
||||
|
||||
private ColumnProfile(BoundaryColumnGeometry geometry) {
|
||||
voxels = geometry.voxels();
|
||||
enclosedOpenings = enclosedOpenings(voxels);
|
||||
solidDistances = geometry.solidDistances();
|
||||
fluidDistances = geometry.fluidDistances();
|
||||
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());
|
||||
}
|
||||
|
||||
@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
|
||||
public void changedBuildExpandsFromSavedTerrainWithoutTheOldFactory() throws Exception {
|
||||
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.assertFalse;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
@@ -194,6 +196,42 @@ public class GenerationHistoryTest extends GenerationHistorySupport {
|
||||
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
|
||||
public void promotionIsIdempotentForEmptyAndRecoveredWorlds() throws Exception {
|
||||
Path emptyWorld = temporaryFolder.newFolder("empty-world").toPath();
|
||||
|
||||
@@ -137,7 +137,7 @@ public final class GenerationSemanticQueriesTest {
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
IrisHydrologyRuntime runtime = mock(IrisHydrologyRuntime.class);
|
||||
IrisDimension dimension = mock(IrisDimension.class);
|
||||
TransitionGenerationPlan transition = mock(TransitionGenerationPlan.class);
|
||||
TransitionDisplacementField displacement = mock(TransitionDisplacementField.class);
|
||||
GenerationHistory history = mock(GenerationHistory.class);
|
||||
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
|
||||
GenerationActivation activation = mock(GenerationActivation.class);
|
||||
@@ -159,9 +159,9 @@ public final class GenerationSemanticQueriesTest {
|
||||
when(engine.getDimension()).thenReturn(dimension);
|
||||
when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
|
||||
when(complex.getHydrologyRuntime()).thenReturn(runtime);
|
||||
when(complex.getTransitionGenerationPlan()).thenReturn(transition);
|
||||
when(complex.getTransitionDisplacement()).thenReturn(displacement);
|
||||
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);
|
||||
when(dimension.getMinHeight()).thenReturn(-64);
|
||||
when(router.history()).thenReturn(history);
|
||||
|
||||
+3
-1
@@ -92,7 +92,9 @@ public final class GenerationUnpublishedHistoryTest extends GenerationHistorySup
|
||||
}
|
||||
|
||||
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()));
|
||||
try (MockedConstruction<RandomAccessFile> ignored = mockConstruction(RandomAccessFile.class,
|
||||
withSettings().defaultAnswer(delegatesTo(actual)),
|
||||
|
||||
+114
@@ -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;
|
||||
|
||||
import art.arcane.iris.world.storage.Durability;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
@@ -57,6 +58,35 @@ public class SavedBiomeStoreWriteTest {
|
||||
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
|
||||
public void encodingAllowsOtherReadsInTheSameRegion() throws Exception {
|
||||
SavedBiomeStore store = SavedBiomeStore.open(temporaryFolder.newFolder().toPath());
|
||||
|
||||
+44
@@ -9,6 +9,7 @@ import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.ArrayList;
|
||||
@@ -30,6 +31,49 @@ public class TerrainBoundarySignatureStoreTest {
|
||||
@Rule
|
||||
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
|
||||
public void preservesPresentAndAbsentUpperCeilingsAcrossShardReloads() throws Exception {
|
||||
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;
|
||||
}
|
||||
}
|
||||
+134
@@ -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
|
||||
public void exposesTheNearestFrozenPhysicalBiomeUntilDiscreteHandoff() throws Exception {
|
||||
public void exposesTheNearestFrozenPhysicalBiomeOnlyInsideTheSeamPatch() throws Exception {
|
||||
GenerationBoundary boundary = GenerationBoundary.freeze("biomes", List.of(
|
||||
new GenerationBoundary.ChunkCoordinate(0, 0)
|
||||
));
|
||||
@@ -193,15 +193,13 @@ public class TransitionGenerationPlanTest {
|
||||
|
||||
assertEquals("iris:middle", plan.historicalPhysicalBiomeKeyAt(16, 14, 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());
|
||||
for (int blockX = -2; blockX <= 34; blockX++) {
|
||||
for (int blockZ : new int[]{-2, 8, 17}) {
|
||||
TransitionGenerationPlan.TerrainSample sample = plan.terrainSampleAt(blockX, blockZ);
|
||||
for (int blockY = -16; blockY <= 48; blockY++) {
|
||||
double weight = GenerationBlend.newEpochWeight(
|
||||
Math.max(0D, sample.distanceToHistoricalTerrain() - 1D), Math.max(1, plan.widthBlocks() - 1));
|
||||
Optional<String> expected = sample.newEpochWeight() != 1D
|
||||
&& GenerationBlend.usesHistoricalMaterial(blockX, blockY, blockZ, weight)
|
||||
Optional<String> expected = sample.distanceToHistoricalTerrain() < 4D
|
||||
? sample.historicalPhysicalBiomeKeyAt(blockY) : Optional.empty();
|
||||
assertEquals(expected, plan.historicalPhysicalBiomeKeyAt(blockX, blockY, blockZ, sample));
|
||||
assertEquals(expected, plan.historicalPhysicalBiomeKeyAt(blockX, blockY, blockZ));
|
||||
|
||||
+22
-12
@@ -38,7 +38,7 @@ public class TransitionGeometryBlenderTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void narrowHistoricalCaveContinuesBeforeTaperingIntoTallSolidTerrain() throws Exception {
|
||||
public void narrowHistoricalCaveIsPatchedOnlyAtTheSeam() throws Exception {
|
||||
ArrayList<BoundaryColumnGeometry.Voxel> oldVoxels = new ArrayList<>(768);
|
||||
for (int offset = 0; offset < 768; offset++) {
|
||||
oldVoxels.add(offset == 384 ? AIR : STONE);
|
||||
@@ -47,9 +47,8 @@ public class TransitionGeometryBlenderTest {
|
||||
BoundaryColumnGeometry current = filled(STONE, 768);
|
||||
TransitionGenerationPlan plan = plan(List.of(signature(15, 0, old)));
|
||||
|
||||
for (int x = 16; x <= 23; x++) {
|
||||
assertEquals(AIR, TransitionGeometryBlender.blendColumn(plan, x, 0, current).voxelAt(380));
|
||||
}
|
||||
assertEquals(AIR, TransitionGeometryBlender.blendColumn(plan, 16, 0, current).voxelAt(380));
|
||||
assertSame(current, TransitionGeometryBlender.blendColumn(plan, 19, 0, current));
|
||||
assertSame(current, TransitionGeometryBlender.blendColumn(plan, 47, 0, current));
|
||||
}
|
||||
|
||||
@@ -74,7 +73,7 @@ public class TransitionGeometryBlenderTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void conflictingFluidFamiliesProduceANewSideSolidPlug() throws Exception {
|
||||
public void conflictingFluidFamiliesKeepTheNativeFluidWithoutSolidPlugs() throws Exception {
|
||||
BoundaryColumnGeometry.Voxel lava = new BoundaryColumnGeometry.Voxel("minecraft:lava[level=0]",
|
||||
BoundaryColumnGeometry.Phase.FLUID, "minecraft:lava[level=0]", false);
|
||||
BoundaryColumnGeometry current = filled(lava, 8);
|
||||
@@ -83,12 +82,23 @@ public class TransitionGeometryBlenderTest {
|
||||
BoundaryColumnGeometry blended = TransitionGeometryBlender.blendColumn(plan, 16, 0, current);
|
||||
|
||||
for (BoundaryColumnGeometry.Voxel voxel : blended.voxels()) {
|
||||
assertEquals(BoundaryColumnGeometry.Phase.SOLID, voxel.phase());
|
||||
assertEquals("minecraft:obsidian", voxel.stateKey());
|
||||
assertEquals(lava, voxel);
|
||||
}
|
||||
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
|
||||
public void differentLevelsOfTheSameFluidDoNotCreateSolidPlugs() throws Exception {
|
||||
BoundaryColumnGeometry.Voxel flowing = new BoundaryColumnGeometry.Voxel("minecraft:water[level=3]",
|
||||
@@ -96,7 +106,7 @@ public class TransitionGeometryBlenderTest {
|
||||
BoundaryColumnGeometry current = filled(flowing, 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
|
||||
@@ -105,13 +115,13 @@ public class TransitionGeometryBlenderTest {
|
||||
List.of(STONE, STONE, AIR, AIR, STONE, AIR, AIR, AIR));
|
||||
BoundaryColumnGeometry current = filled(STONE, 8);
|
||||
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--) {
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -121,8 +131,8 @@ public class TransitionGeometryBlenderTest {
|
||||
List.of(STONE, STONE, AIR, AIR, AIR, AIR, AIR, AIR));
|
||||
BoundaryColumnGeometry upper = BoundaryColumnGeometry.fromVoxels(-4,
|
||||
List.of(AIR, AIR, AIR, AIR, STONE, STONE, AIR, AIR));
|
||||
TerrainBoundarySignature west = signature(15, 0, lower);
|
||||
TerrainBoundarySignature south = signature(0, 15, upper);
|
||||
TerrainBoundarySignature west = signature(15, 15, lower);
|
||||
TerrainBoundarySignature south = signature(14, 15, upper);
|
||||
TransitionGenerationPlan first = plan(List.of(west, south));
|
||||
TransitionGenerationPlan second = plan(List.of(south, west));
|
||||
BoundaryColumnGeometry current = filled(STONE, 8);
|
||||
|
||||
@@ -59,7 +59,7 @@ public class TransitionGeometryCursorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void matchesOpeningCutoffAndProtectedVoxelsInsideOpenings() {
|
||||
public void matchesOpeningHeightsAndProtectedVoxelsInsideOpenings() {
|
||||
Random random = new Random(1234);
|
||||
for (int opening : new int[]{1, 63, 64, 65, 66}) {
|
||||
for (boolean protectedMiddle : new boolean[]{false, true}) {
|
||||
|
||||
+26
-1
@@ -37,6 +37,7 @@ dependencies {
|
||||
runtimeOnly(libs.fastutil)
|
||||
runtimeOnly(libs.caffeine)
|
||||
runtimeOnly(libs.lz4)
|
||||
runtimeOnly('com.fulcrumgenomics:jlibdeflate:0.2.0')
|
||||
runtimeOnly(libs.zip)
|
||||
runtimeOnly(libs.guava)
|
||||
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 ->
|
||||
classpath += configurations.studioProbeApi
|
||||
doFirst {
|
||||
|
||||
@@ -206,7 +206,10 @@ public final class StubPlatform implements IrisPlatform {
|
||||
|
||||
@Override
|
||||
public boolean isStorageChest() {
|
||||
return false;
|
||||
return switch (blockKey()) {
|
||||
case "minecraft:chest", "minecraft:trapped_chest", "minecraft:barrel" -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
@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
|
||||
}
|
||||
]
|
||||
}
|
||||
+49
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"object": "buildings/boundary",
|
||||
"rotatable": false
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"pieces": [
|
||||
{
|
||||
"piece": "boundary",
|
||||
"weight": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user