mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 22:10:18 +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:
1 parent
9eb05fd987
commit
8c3e5d547b
159 files changed
+6092
-1905
No files matched your search
+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}) {
|
||||
|
||||
Reference in new issue
Block a user