mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 22:10:18 +00:00
Merged wave1 into terrain bake
# Conflicts: # core/src/main/java/art/arcane/iris/generation/runtime/IrisComplex.java # probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java
This commit is contained in:
commit
d2106806a5
71 files changed
+5882
-948
No files matched your search
@@ -6,8 +6,7 @@ import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
import art.arcane.iris.testsupport.PlatformLeakGuard;
|
||||
import art.arcane.iris.generation.chunk.ChunkDataHunkHolder;
|
||||
import org.bukkit.generator.ChunkGenerator;
|
||||
import art.arcane.iris.generation.chunk.SectionBuffer;
|
||||
import org.junit.After;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
@@ -37,7 +36,7 @@ public class PlatformStateRebindTest {
|
||||
"art.arcane.iris.generation.biome.IrisBiomeLayerGenerator",
|
||||
"art.arcane.iris.studio.generation.BiomeBuffetGenerator",
|
||||
"art.arcane.iris.generation.mantle.EngineMantle",
|
||||
"art.arcane.iris.generation.chunk.ChunkDataHunkHolder");
|
||||
"art.arcane.iris.generation.chunk.SectionBuffer");
|
||||
|
||||
@After
|
||||
public void unbindPlatform() {
|
||||
@@ -47,10 +46,7 @@ public class PlatformStateRebindTest {
|
||||
@Test
|
||||
public void chunkDataAirIsResolvedAgainstTheCurrentlyBoundPlatform() {
|
||||
NativeBlockState first = bindPlatform("first");
|
||||
ChunkGenerator.ChunkData chunk = mock(ChunkGenerator.ChunkData.class);
|
||||
when(chunk.getMinHeight()).thenReturn(-64);
|
||||
when(chunk.getMaxHeight()).thenReturn(320);
|
||||
ChunkDataHunkHolder holder = new ChunkDataHunkHolder(chunk);
|
||||
SectionBuffer holder = new SectionBuffer(384);
|
||||
|
||||
assertSame(first, holder.getRaw(0, -1, 0));
|
||||
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
package art.arcane.iris.generation.chunk;
|
||||
|
||||
import art.arcane.iris.platform.bukkit.nms.INMS;
|
||||
import art.arcane.iris.platform.bukkit.nms.INMSBinding;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.generator.ChunkGenerator;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class ChunkDataHunkHolderTest {
|
||||
@Test
|
||||
public void columnTopTracksTheHighestStoredValueAndIgnoresNulls() {
|
||||
ChunkDataHunkHolder holder = holder(64);
|
||||
NativeBlockState stone = mock(NativeBlockState.class);
|
||||
|
||||
assertEquals(-1, holder.highestStoredY(3, 7));
|
||||
holder.setRaw(3, 40, 7, null);
|
||||
assertEquals(-1, holder.highestStoredY(3, 7));
|
||||
holder.setRaw(3, 12, 7, stone);
|
||||
holder.setRaw(3, 30, 7, stone);
|
||||
holder.setRaw(3, 5, 7, stone);
|
||||
|
||||
assertEquals(30, holder.highestStoredY(3, 7));
|
||||
assertEquals(-1, holder.highestStoredY(7, 3));
|
||||
assertSame(stone, holder.getStoredRaw(3, 30, 7));
|
||||
assertNull(holder.getStoredRaw(3, 31, 7));
|
||||
assertEquals(30, ColumnExtent.highestStoredY(new ColumnExtentListeningHunk<>(holder, (x, y, z, state) -> {
|
||||
}), 3, 7));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cellsKeepTheZMajorLayout() {
|
||||
ChunkDataHunkHolder holder = holder(32);
|
||||
List<NativeBlockState> states = new ArrayList<>();
|
||||
for (int x = 0; x < 16; x++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
for (int y = 0; y < 32; y++) {
|
||||
NativeBlockState state = mock(NativeBlockState.class);
|
||||
states.add(state);
|
||||
holder.setRaw(x, y, z, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
int index = 0;
|
||||
for (int x = 0; x < 16; x++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
for (int y = 0; y < 32; y++) {
|
||||
assertSame(states.get(index++), holder.getStoredRaw(x, y, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void concurrentWritersNeverLoseTheColumnMaximum() throws InterruptedException {
|
||||
for (int trial = 0; trial < 20; trial++) {
|
||||
ChunkDataHunkHolder holder = holder(768);
|
||||
NativeBlockState stone = mock(NativeBlockState.class);
|
||||
int threads = 8;
|
||||
CountDownLatch start = new CountDownLatch(1);
|
||||
List<Thread> writers = new ArrayList<>();
|
||||
for (int thread = 0; thread < threads; thread++) {
|
||||
int offset = thread;
|
||||
Thread writer = new Thread(() -> {
|
||||
try {
|
||||
start.await();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
}
|
||||
for (int y = offset; y < 768; y += threads) {
|
||||
holder.setRaw(9, y, 4, stone);
|
||||
}
|
||||
});
|
||||
writer.start();
|
||||
writers.add(writer);
|
||||
}
|
||||
start.countDown();
|
||||
for (Thread writer : writers) {
|
||||
writer.join();
|
||||
}
|
||||
assertEquals(767, holder.highestStoredY(9, 4));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fallbackAppliesExplicitCaveAirBetweenSolidRunsWithNegativeMinimumHeight() {
|
||||
ChunkGenerator.ChunkData chunk = mock(ChunkGenerator.ChunkData.class);
|
||||
when(chunk.getMinHeight()).thenReturn(-256);
|
||||
when(chunk.getMaxHeight()).thenReturn(512);
|
||||
ChunkDataHunkHolder holder = new ChunkDataHunkHolder(chunk);
|
||||
BlockData dirt = mock(BlockData.class);
|
||||
BlockData coal = mock(BlockData.class);
|
||||
BlockData caveAir = mock(BlockData.class);
|
||||
NativeBlockState dirtState = state(dirt);
|
||||
NativeBlockState coalState = state(coal);
|
||||
NativeBlockState caveAirState = state(caveAir);
|
||||
BlockData[] applied = new BlockData[768];
|
||||
Arrays.fill(applied, dirt);
|
||||
for (int y = 0; y <= 75; y++) {
|
||||
holder.setRaw(14, y + 256, 6, y < 33 ? dirtState : y <= 54 ? caveAirState : coalState);
|
||||
}
|
||||
doAnswer(call -> {
|
||||
int y = call.getArgument(1);
|
||||
applied[y + 256] = call.getArgument(3);
|
||||
return null;
|
||||
}).when(chunk).setBlock(anyInt(), anyInt(), anyInt(), any(BlockData.class));
|
||||
doAnswer(call -> {
|
||||
int from = call.getArgument(1);
|
||||
int to = call.getArgument(4);
|
||||
BlockData block = call.getArgument(6);
|
||||
Arrays.fill(applied, from + 256, to + 256, block);
|
||||
return null;
|
||||
}).when(chunk).setRegion(anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), any(BlockData.class));
|
||||
|
||||
try (MockedStatic<INMS> nms = mockStatic(INMS.class)) {
|
||||
nms.when(INMS::get).thenReturn(mock(INMSBinding.class));
|
||||
holder.apply();
|
||||
}
|
||||
|
||||
for (int y = -256; y < 512; y++) {
|
||||
BlockData expected = y >= 33 && y <= 54 ? caveAir : y >= 55 && y <= 75 ? coal : dirt;
|
||||
assertSame("column at " + y, expected, applied[y + 256]);
|
||||
}
|
||||
}
|
||||
|
||||
private static NativeBlockState state(BlockData data) {
|
||||
NativeBlockState state = mock(NativeBlockState.class);
|
||||
when(state.nativeHandle()).thenReturn(data);
|
||||
return state;
|
||||
}
|
||||
|
||||
private static ChunkDataHunkHolder holder(int height) {
|
||||
ChunkGenerator.ChunkData chunk = mock(ChunkGenerator.ChunkData.class);
|
||||
when(chunk.getMinHeight()).thenReturn(0);
|
||||
when(chunk.getMaxHeight()).thenReturn(height);
|
||||
return new ChunkDataHunkHolder(chunk);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package art.arcane.iris.generation.chunk;
|
||||
|
||||
import art.arcane.iris.platform.bukkit.nms.INMS;
|
||||
import art.arcane.iris.platform.bukkit.nms.INMSBinding;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.generator.ChunkGenerator;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class ChunkDataOutputTest {
|
||||
@Test
|
||||
public void nativeWriterReceivesTheSectionBuffer() {
|
||||
ChunkGenerator.ChunkData chunk = mock(ChunkGenerator.ChunkData.class);
|
||||
SectionBuffer blocks = new SectionBuffer(64);
|
||||
INMSBinding binding = mock(INMSBinding.class);
|
||||
when(binding.applyChunkDataBlocks(chunk, blocks)).thenReturn(true);
|
||||
|
||||
try (MockedStatic<INMS> nms = mockStatic(INMS.class)) {
|
||||
nms.when(INMS::get).thenReturn(binding);
|
||||
ChunkDataOutput.apply(chunk, blocks);
|
||||
}
|
||||
|
||||
verify(binding).applyChunkDataBlocks(chunk, blocks);
|
||||
verify(chunk, never()).setRegion(anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), any(BlockData.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fallbackAppliesExplicitCaveAirBetweenSolidRunsWithNegativeMinimumHeight() {
|
||||
ChunkGenerator.ChunkData chunk = mock(ChunkGenerator.ChunkData.class);
|
||||
when(chunk.getMinHeight()).thenReturn(-256);
|
||||
when(chunk.getMaxHeight()).thenReturn(512);
|
||||
SectionBuffer blocks = new SectionBuffer(768);
|
||||
BlockData dirt = mock(BlockData.class);
|
||||
BlockData coal = mock(BlockData.class);
|
||||
BlockData caveAir = mock(BlockData.class);
|
||||
NativeBlockState dirtState = state(dirt);
|
||||
NativeBlockState coalState = state(coal);
|
||||
NativeBlockState caveAirState = state(caveAir);
|
||||
BlockData[] applied = new BlockData[768];
|
||||
Arrays.fill(applied, dirt);
|
||||
for (int y = 0; y <= 75; y++) {
|
||||
blocks.setRaw(14, y + 256, 6, y < 33 ? dirtState : y <= 54 ? caveAirState : coalState);
|
||||
}
|
||||
doAnswer(call -> {
|
||||
int y = call.getArgument(1);
|
||||
applied[y + 256] = call.getArgument(3);
|
||||
return null;
|
||||
}).when(chunk).setBlock(anyInt(), anyInt(), anyInt(), any(BlockData.class));
|
||||
doAnswer(call -> {
|
||||
int from = call.getArgument(1);
|
||||
int to = call.getArgument(4);
|
||||
BlockData block = call.getArgument(6);
|
||||
Arrays.fill(applied, from + 256, to + 256, block);
|
||||
return null;
|
||||
}).when(chunk).setRegion(anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), any(BlockData.class));
|
||||
|
||||
try (MockedStatic<INMS> nms = mockStatic(INMS.class)) {
|
||||
nms.when(INMS::get).thenReturn(mock(INMSBinding.class));
|
||||
ChunkDataOutput.apply(chunk, blocks);
|
||||
}
|
||||
|
||||
for (int y = -256; y < 512; y++) {
|
||||
BlockData expected = y >= 33 && y <= 54 ? caveAir : y >= 55 && y <= 75 ? coal : dirt;
|
||||
assertSame("column at " + y, expected, applied[y + 256]);
|
||||
}
|
||||
assertTrue(blocks.highestStoredY(14, 6) == 331);
|
||||
}
|
||||
|
||||
private static NativeBlockState state(BlockData data) {
|
||||
NativeBlockState state = mock(NativeBlockState.class);
|
||||
when(state.nativeHandle()).thenReturn(data);
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
package art.arcane.iris.generation.chunk;
|
||||
|
||||
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.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.hunk.Hunk;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class SectionBufferTest {
|
||||
@ClassRule
|
||||
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
|
||||
|
||||
private NativeBlockState air;
|
||||
|
||||
@Before
|
||||
public void bindPlatform() {
|
||||
air = mock(NativeBlockState.class);
|
||||
PlatformRegistries registries = mock(PlatformRegistries.class);
|
||||
when(registries.block(anyString())).thenReturn(air);
|
||||
IrisPlatform platform = mock(IrisPlatform.class);
|
||||
when(platform.registries()).thenReturn(registries);
|
||||
IrisPlatforms.bind(platform);
|
||||
}
|
||||
|
||||
@After
|
||||
public void unbindPlatform() {
|
||||
IrisPlatforms.unbind();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void columnTopTracksTheHighestStoredValueAndIgnoresNulls() {
|
||||
SectionBuffer buffer = new SectionBuffer(64);
|
||||
NativeBlockState stone = mock(NativeBlockState.class);
|
||||
|
||||
assertEquals(-1, buffer.highestStoredY(3, 7));
|
||||
buffer.setRaw(3, 40, 7, null);
|
||||
assertEquals(-1, buffer.highestStoredY(3, 7));
|
||||
buffer.setRaw(3, 12, 7, stone);
|
||||
buffer.setRaw(3, 30, 7, stone);
|
||||
buffer.setRaw(3, 5, 7, stone);
|
||||
|
||||
assertEquals(30, buffer.highestStoredY(3, 7));
|
||||
assertEquals(-1, buffer.highestStoredY(7, 3));
|
||||
assertSame(stone, buffer.getStoredRaw(3, 30, 7));
|
||||
assertNull(buffer.getStoredRaw(3, 31, 7));
|
||||
assertEquals(30, ColumnExtent.highestStoredY(new ColumnExtentListeningHunk<>(buffer, (x, y, z, state) -> {
|
||||
}), 3, 7));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unwrittenAndOutOfRangeCellsReadAsAir() {
|
||||
SectionBuffer buffer = new SectionBuffer(48);
|
||||
NativeBlockState stone = mock(NativeBlockState.class);
|
||||
buffer.setRaw(2, 20, 9, stone);
|
||||
|
||||
assertSame(air, buffer.getRaw(2, 21, 9));
|
||||
assertSame(air, buffer.getRaw(2, 3, 9));
|
||||
assertSame(air, buffer.getRaw(2, -1, 9));
|
||||
assertSame(air, buffer.getRaw(2, 48, 9));
|
||||
assertSame(stone, buffer.getRaw(2, 20, 9));
|
||||
assertNull(buffer.getStoredRaw(2, 21, 9));
|
||||
assertNull(buffer.getStoredRaw(2, 3, 9));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void paletteKeepsEveryDistinctInstance() {
|
||||
SectionBuffer buffer = new SectionBuffer(32);
|
||||
NativeBlockState first = mock(NativeBlockState.class);
|
||||
NativeBlockState second = mock(NativeBlockState.class);
|
||||
when(first.matches(second)).thenReturn(true);
|
||||
when(second.matches(first)).thenReturn(true);
|
||||
|
||||
buffer.setRaw(0, 0, 0, first);
|
||||
buffer.setRaw(1, 0, 0, second);
|
||||
buffer.setRaw(2, 0, 0, first);
|
||||
|
||||
assertSame(first, buffer.getRaw(0, 0, 0));
|
||||
assertSame(second, buffer.getRaw(1, 0, 0));
|
||||
assertSame(first, buffer.getRaw(2, 0, 0));
|
||||
assertEquals(buffer.paletteId(first), buffer.paletteId(first));
|
||||
assertNotEquals(buffer.paletteId(first), buffer.paletteId(second));
|
||||
assertEquals(0, buffer.paletteId(null));
|
||||
NativeBlockState[] palette = buffer.palette();
|
||||
assertEquals(3, palette.length);
|
||||
assertNull(palette[0]);
|
||||
assertSame(first, palette[buffer.paletteId(first)]);
|
||||
assertSame(second, palette[buffer.paletteId(second)]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sectionsUseTheYzxLayoutAndStayUnallocatedUntilWritten() {
|
||||
SectionBuffer buffer = new SectionBuffer(64);
|
||||
NativeBlockState stone = mock(NativeBlockState.class);
|
||||
NativeBlockState dirt = mock(NativeBlockState.class);
|
||||
buffer.setRaw(5, 37, 11, stone);
|
||||
buffer.setRaw(15, 47, 15, dirt);
|
||||
|
||||
assertEquals(4, buffer.sectionCount());
|
||||
assertNull(buffer.section(0));
|
||||
assertNull(buffer.section(1));
|
||||
assertNull(buffer.section(3));
|
||||
short[] section = buffer.section(2);
|
||||
assertEquals(4096, section.length);
|
||||
assertEquals(buffer.paletteId(stone), section[(5 << 8) | (11 << 4) | 5]);
|
||||
assertEquals(buffer.paletteId(dirt), section[(15 << 8) | (15 << 4) | 15]);
|
||||
int written = 0;
|
||||
for (short id : section) {
|
||||
written += id == 0 ? 0 : 1;
|
||||
}
|
||||
assertEquals(2, written);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void columnRunsMatchRepeatedSingleWrites() {
|
||||
Random random = new Random(41L);
|
||||
List<NativeBlockState> pool = new ArrayList<>();
|
||||
for (int index = 0; index < 9; index++) {
|
||||
pool.add(mock(NativeBlockState.class));
|
||||
}
|
||||
pool.add(null);
|
||||
for (int trial = 0; trial < 40; trial++) {
|
||||
int height = 16 * (1 + random.nextInt(12));
|
||||
SectionBuffer runs = new SectionBuffer(height);
|
||||
SectionBuffer cells = new SectionBuffer(height);
|
||||
for (int write = 0; write < 300; write++) {
|
||||
int x = random.nextInt(16);
|
||||
int z = random.nextInt(16);
|
||||
int from = random.nextInt(height);
|
||||
int to = Math.min(height - 1, from + random.nextInt(70));
|
||||
NativeBlockState state = pool.get(random.nextInt(pool.size()));
|
||||
switch (random.nextInt(3)) {
|
||||
case 0 -> runs.fillColumnRun(x, z, from, to, state);
|
||||
case 1 -> runs.fillColumnRun(x, z, from, to, runs.paletteId(state));
|
||||
default -> ColumnRunHunk.fillColumnRun(runs, x, z, from, to, state);
|
||||
}
|
||||
for (int y = to; y >= from; y--) {
|
||||
cells.setRaw(x, y, z, state);
|
||||
}
|
||||
}
|
||||
assertSameContents(cells, runs);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void runsWithAnEmptyRangeOrNoStateWriteNothing() {
|
||||
SectionBuffer buffer = new SectionBuffer(32);
|
||||
NativeBlockState stone = mock(NativeBlockState.class);
|
||||
buffer.fillColumnRun(1, 1, 10, 9, stone);
|
||||
buffer.fillColumnRun(1, 1, 3, 9, (NativeBlockState) null);
|
||||
buffer.fillColumnRun(1, 1, 3, 9, 0);
|
||||
|
||||
assertEquals(-1, buffer.highestStoredY(1, 1));
|
||||
assertNull(buffer.section(0));
|
||||
assertEquals(1, buffer.palette().length);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void concurrentWritersNeverLoseTheColumnMaximum() throws InterruptedException {
|
||||
for (int trial = 0; trial < 20; trial++) {
|
||||
SectionBuffer buffer = new SectionBuffer(768);
|
||||
NativeBlockState stone = mock(NativeBlockState.class);
|
||||
int threads = 8;
|
||||
runConcurrently(threads, thread -> {
|
||||
for (int y = thread; y < 768; y += threads) {
|
||||
buffer.setRaw(9, y, 4, stone);
|
||||
}
|
||||
});
|
||||
assertEquals(767, buffer.highestStoredY(9, 4));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void concurrentPaletteGrowthKeepsOneIdPerInstance() throws InterruptedException {
|
||||
int threads = 8;
|
||||
int statesPerThread = 300;
|
||||
NativeBlockState[][] states = new NativeBlockState[threads][statesPerThread];
|
||||
NativeBlockState[] shared = new NativeBlockState[40];
|
||||
for (int index = 0; index < shared.length; index++) {
|
||||
shared[index] = mock(NativeBlockState.class);
|
||||
}
|
||||
for (int thread = 0; thread < threads; thread++) {
|
||||
for (int index = 0; index < statesPerThread; index++) {
|
||||
states[thread][index] = mock(NativeBlockState.class);
|
||||
}
|
||||
}
|
||||
for (int trial = 0; trial < 10; trial++) {
|
||||
SectionBuffer buffer = new SectionBuffer(384);
|
||||
runConcurrently(threads, thread -> {
|
||||
for (int index = 0; index < statesPerThread; index++) {
|
||||
int x = thread * 2 + (index & 1);
|
||||
int z = (index >> 1) & 15;
|
||||
int y = index;
|
||||
buffer.setRaw(x, y, z, states[thread][index]);
|
||||
buffer.setRaw(x, y + 1, z, shared[index % shared.length]);
|
||||
}
|
||||
});
|
||||
for (int thread = 0; thread < threads; thread++) {
|
||||
for (int index = 0; index < statesPerThread; index++) {
|
||||
int x = thread * 2 + (index & 1);
|
||||
int z = (index >> 1) & 15;
|
||||
assertSame(states[thread][index], buffer.getStoredRaw(x, index, z));
|
||||
assertSame(shared[index % shared.length], buffer.getRaw(x, index + 1, z));
|
||||
}
|
||||
}
|
||||
assertEquals(1 + threads * statesPerThread + shared.length, buffer.palette().length);
|
||||
for (NativeBlockState state : shared) {
|
||||
assertSame(state, buffer.palette()[buffer.paletteId(state)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void listeningHunksForwardRunsInTopDownOrder() {
|
||||
SectionBuffer buffer = new SectionBuffer(32);
|
||||
NativeBlockState stone = mock(NativeBlockState.class);
|
||||
List<Integer> heard = new ArrayList<>();
|
||||
ColumnExtentListeningHunk<NativeBlockState> listening = new ColumnExtentListeningHunk<>(buffer,
|
||||
(x, y, z, state) -> heard.add(y));
|
||||
|
||||
ColumnRunHunk.fillColumnRun(listening, 4, 6, 10, 13, stone);
|
||||
|
||||
assertEquals(List.of(13, 12, 11, 10), heard);
|
||||
for (int y = 10; y <= 13; y++) {
|
||||
assertSame(stone, buffer.getStoredRaw(4, y, 6));
|
||||
}
|
||||
assertNull(buffer.getStoredRaw(4, 9, 6));
|
||||
assertEquals(13, buffer.highestStoredY(4, 6));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void plainHunksReceiveRunsAsTopDownSingleWrites() {
|
||||
Hunk<NativeBlockState> array = Hunk.newArrayHunk(16, 32, 16);
|
||||
NativeBlockState stone = mock(NativeBlockState.class);
|
||||
array.setRaw(2, 5, 2, stone);
|
||||
List<Integer> order = new ArrayList<>();
|
||||
Hunk<NativeBlockState> recording = new ColumnExtentListeningHunk<>(array, (x, y, z, state) -> order.add(y));
|
||||
|
||||
ColumnRunHunk.fillColumnRun(Hunk.newArrayHunk(16, 32, 16), 0, 0, 0, 3, stone);
|
||||
ColumnRunHunk.fillColumnRun(array, 2, 2, 4, 6, null);
|
||||
ColumnRunHunk.fillColumnRun(recording, 1, 1, 7, 9, stone);
|
||||
|
||||
assertNull(array.getRaw(2, 5, 2));
|
||||
assertSame(stone, array.getRaw(1, 8, 1));
|
||||
assertEquals(List.of(9, 8, 7), order);
|
||||
}
|
||||
|
||||
private static void assertSameContents(SectionBuffer expected, SectionBuffer actual) {
|
||||
assertEquals(expected.sectionCount(), actual.sectionCount());
|
||||
NativeBlockState[] expectedPalette = expected.palette();
|
||||
NativeBlockState[] actualPalette = actual.palette();
|
||||
for (int sectionIndex = 0; sectionIndex < expected.sectionCount(); sectionIndex++) {
|
||||
short[] expectedSection = expected.section(sectionIndex);
|
||||
short[] actualSection = actual.section(sectionIndex);
|
||||
if (expectedSection == null) {
|
||||
assertNull(actualSection);
|
||||
continue;
|
||||
}
|
||||
NativeBlockState[] expectedStates = new NativeBlockState[4096];
|
||||
NativeBlockState[] actualStates = new NativeBlockState[4096];
|
||||
for (int index = 0; index < 4096; index++) {
|
||||
expectedStates[index] = expectedPalette[expectedSection[index] & 0xFFFF];
|
||||
actualStates[index] = actualPalette[actualSection[index] & 0xFFFF];
|
||||
}
|
||||
assertArrayEquals(expectedStates, actualStates);
|
||||
}
|
||||
for (int x = 0; x < 16; x++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
assertEquals(expected.highestStoredY(x, z), actual.highestStoredY(x, z));
|
||||
}
|
||||
}
|
||||
assertEquals(Arrays.stream(expectedPalette).filter(state -> state != null).distinct().count(),
|
||||
Arrays.stream(actualPalette).filter(state -> state != null).distinct().count());
|
||||
}
|
||||
|
||||
private static void runConcurrently(int threads, ThreadTask task) throws InterruptedException {
|
||||
CountDownLatch start = new CountDownLatch(1);
|
||||
List<Thread> workers = new ArrayList<>();
|
||||
for (int thread = 0; thread < threads; thread++) {
|
||||
int index = thread;
|
||||
Thread worker = new Thread(() -> {
|
||||
try {
|
||||
start.await();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
}
|
||||
task.run(index);
|
||||
});
|
||||
worker.start();
|
||||
workers.add(worker);
|
||||
}
|
||||
start.countDown();
|
||||
for (Thread worker : workers) {
|
||||
worker.join();
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface ThreadTask {
|
||||
void run(int thread);
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import java.util.concurrent.ForkJoinTask;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
@@ -60,6 +61,13 @@ public class HydrologyBurstTest {
|
||||
assertTrue(pool.isTerminated());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void plannerPoolFollowsThePerformanceCores() {
|
||||
assertEquals(12, MultiBurst.HydrologyBurst.plannerParallelism(12));
|
||||
assertEquals(4, MultiBurst.HydrologyBurst.plannerParallelism(1));
|
||||
assertEquals(32, MultiBurst.HydrologyBurst.plannerParallelism(32));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void closedHydrologyWorkersTerminateAndReopenCreatesANewPool() throws Exception {
|
||||
IrisSettings previousSettings = IrisSettings.settings;
|
||||
|
||||
@@ -696,6 +696,29 @@ public class HydrologyTileCacheTest {
|
||||
cache.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pregenSpeculationStaysWithinTheGenerationReachOfTheArea() {
|
||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||
HydrologyTile tile = mock(HydrologyTile.class);
|
||||
HydrologyPlannerSettings settings = stalePrefetchSettings();
|
||||
when(planner.settings()).thenReturn(settings);
|
||||
ArrayList<HydrologyTileKey> planned = new ArrayList<>();
|
||||
doAnswer(invocation -> {
|
||||
planned.add(invocation.getArgument(0));
|
||||
return tile;
|
||||
}).when(planner).plan(any(HydrologyTileKey.class));
|
||||
HydrologyTileCache cache = new HydrologyTileCache(planner, 128, Runnable::run);
|
||||
try (HydrologyTileCache.PregenerationScope ignored = cache.preparePregeneration(
|
||||
new HydrologyTileCache.PregenerationArea(1024, 1024, 736, 736, 1311, 1311))) {
|
||||
assertEquals(prefetchRectangle(0, 1, 0, 1), new HashSet<>(planned));
|
||||
cache.prepareChunkColumns(800, 800);
|
||||
cache.prepareChunkColumns(1300, 1300);
|
||||
assertEquals(prefetchRectangle(0, 1, 0, 1), new HashSet<>(planned));
|
||||
assertEquals(4, planned.size());
|
||||
}
|
||||
cache.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pregenScopesRestoreStudioPrefetchAndIgnoreSupersededClose() {
|
||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
package art.arcane.iris.generation.mantle;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.SplittableRandom;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class CaveCellKernelTest {
|
||||
private static final int TRIALS = 200_000;
|
||||
|
||||
@Test
|
||||
public void decidedCellsAgreeWithEveryInterpolatedColumn() {
|
||||
SplittableRandom random = new SplittableRandom(91_337L);
|
||||
long decided = 0L;
|
||||
for (int trial = 0; trial < TRIALS; trial++) {
|
||||
double scale = Math.scalb(1D, random.nextInt(-30, 4));
|
||||
double base = (random.nextDouble() * 2D - 1D) * scale;
|
||||
double spread = random.nextInt(4) == 0 ? 0D : random.nextDouble() * scale * Math.scalb(1D, -random.nextInt(0, 40));
|
||||
double d00 = base + spread * (random.nextDouble() * 2D - 1D);
|
||||
double d01 = random.nextInt(8) == 0 ? d00 : base + spread * (random.nextDouble() * 2D - 1D);
|
||||
double d10 = base + spread * (random.nextDouble() * 2D - 1D);
|
||||
double d11 = random.nextInt(8) == 0 ? d10 : base + spread * (random.nextDouble() * 2D - 1D);
|
||||
double low = Math.min(Math.min(d00, d01), Math.min(d10, d11));
|
||||
double high = Math.max(Math.max(d00, d01), Math.max(d10, d11));
|
||||
int mode = random.nextInt(6);
|
||||
double[] thresholds = new double[64];
|
||||
for (int column = 0; column < 64; column++) {
|
||||
thresholds[column] = pickThreshold(random, low, high, mode);
|
||||
}
|
||||
double thresholdLow = Double.POSITIVE_INFINITY;
|
||||
double thresholdHigh = Double.NEGATIVE_INFINITY;
|
||||
for (double threshold : thresholds) {
|
||||
thresholdLow = Math.min(thresholdLow, threshold);
|
||||
thresholdHigh = Math.max(thresholdHigh, threshold);
|
||||
}
|
||||
int decision = CaveCellKernel.decide(d00, d01, d10, d11, thresholdLow, thresholdHigh);
|
||||
if (decision == CaveCellKernel.MIXED) {
|
||||
continue;
|
||||
}
|
||||
decided++;
|
||||
for (int column = 0; column < 64; column++) {
|
||||
double tx = (column >> 3) / 8D;
|
||||
double tz = (column & 7) / 8D;
|
||||
boolean carved = bilinear(d00, d01, d10, d11, tx, tz) <= thresholds[column];
|
||||
assertEquals("trial " + trial + " column " + column, decision == CaveCellKernel.ALL, carved);
|
||||
}
|
||||
}
|
||||
assertTrue("decided " + decided, decided > TRIALS / 4);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void thresholdsAtTheCornerExtremesStayMixed() {
|
||||
double d00 = 0.1D;
|
||||
double d01 = -0.2D;
|
||||
double d10 = 0.3D;
|
||||
double d11 = 0.05D;
|
||||
assertEquals(CaveCellKernel.MIXED, CaveCellKernel.decide(d00, d01, d10, d11, 0.3D, 0.3D));
|
||||
assertEquals(CaveCellKernel.MIXED, CaveCellKernel.decide(d00, d01, d10, d11, Math.nextUp(0.3D), 0.4D));
|
||||
assertEquals(CaveCellKernel.MIXED, CaveCellKernel.decide(d00, d01, d10, d11, -0.3D, Math.nextDown(-0.2D)));
|
||||
assertEquals(CaveCellKernel.ALL, CaveCellKernel.decide(d00, d01, d10, d11, 0.3D + 1.0E-12D, 0.5D));
|
||||
assertEquals(CaveCellKernel.NONE, CaveCellKernel.decide(d00, d01, d10, d11, -0.9D, -0.2D - 1.0E-12D));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nonFiniteCornersAreNeverDecided() {
|
||||
assertEquals(CaveCellKernel.MIXED, CaveCellKernel.decide(Double.NaN, 0D, 0D, 0D, 5D, 5D));
|
||||
assertEquals(CaveCellKernel.MIXED, CaveCellKernel.decide(0D, Double.POSITIVE_INFINITY, 0D, 0D, -5D, -5D));
|
||||
assertEquals(CaveCellKernel.MIXED, CaveCellKernel.decide(0D, 0D, Double.NEGATIVE_INFINITY, 0D,
|
||||
Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY));
|
||||
assertEquals(CaveCellKernel.MIXED, CaveCellKernel.decide(0D, 0D, 0D, 0D, Double.NaN, 1D));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void monotoneThresholdMappingPreservesBounds() {
|
||||
SplittableRandom random = new SplittableRandom(7_771L);
|
||||
for (int trial = 0; trial < TRIALS; trial++) {
|
||||
double normalization = 0.25D + random.nextDouble() * 3D;
|
||||
double inverse = 1D / normalization;
|
||||
double a = (random.nextDouble() * 2D - 1D) * Math.scalb(1D, random.nextInt(-20, 3));
|
||||
double b = random.nextInt(4) == 0 ? Math.nextUp(a) : a + random.nextDouble() * Math.scalb(1D, random.nextInt(-50, 0));
|
||||
assertTrue(CaveCellKernel.threshold(a, normalization, inverse) <= CaveCellKernel.threshold(b, normalization, inverse));
|
||||
assertEquals(Double.doubleToRawLongBits((a * normalization) * inverse),
|
||||
Double.doubleToRawLongBits(CaveCellKernel.threshold(a, normalization, inverse)));
|
||||
}
|
||||
}
|
||||
|
||||
private static double pickThreshold(SplittableRandom random, double low, double high, int mode) {
|
||||
double width = Math.max(high - low, Math.ulp(Math.max(Math.abs(low), Math.abs(high))));
|
||||
return switch (mode) {
|
||||
case 0 -> high + Math.ulp(high) * random.nextInt(0, 64);
|
||||
case 1 -> low - Math.ulp(low) * random.nextInt(0, 64);
|
||||
case 2 -> high + width * random.nextDouble() * 0.5D;
|
||||
case 3 -> low - width * random.nextDouble() * 0.5D;
|
||||
case 4 -> high + width * (1D + random.nextDouble() * 4D);
|
||||
default -> low - width * (1D + random.nextDouble() * 4D);
|
||||
};
|
||||
}
|
||||
|
||||
private static double bilinear(double d00, double d01, double d10, double d11, double tx, double tz) {
|
||||
double dx0 = d00 + ((d10 - d00) * tx);
|
||||
double dx1 = d01 + ((d11 - d01) * tx);
|
||||
return dx0 + ((dx1 - dx0) * tz);
|
||||
}
|
||||
}
|
||||
+414
@@ -0,0 +1,414 @@
|
||||
package art.arcane.iris.generation.mantle;
|
||||
|
||||
import art.arcane.iris.generation.cave.IrisCaveFieldModule;
|
||||
import art.arcane.iris.generation.cave.IrisCaveProfile;
|
||||
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
|
||||
import art.arcane.iris.generation.noise.IrisStyledRange;
|
||||
import art.arcane.iris.generation.noise.NoiseStyle;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.EngineMetrics;
|
||||
import art.arcane.iris.generation.runtime.SeedManager;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.pack.value.IrisRange;
|
||||
import art.arcane.iris.world.IrisWorld;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
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.MatterCavern;
|
||||
import art.arcane.volmlib.util.matter.MatterSlice;
|
||||
import art.arcane.volmlib.util.matter.slices.CavernMatter;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.withSettings;
|
||||
|
||||
public class IrisCaveCarver3DCellKernelTest {
|
||||
private static final long CARVE_SEED = -54_777_863_784_492_918L;
|
||||
private static final int WORLD_HEIGHT = 160;
|
||||
private static final int CHUNK_SPAN = 4;
|
||||
|
||||
@Test
|
||||
public void warpModulesProfileMatchesGoldenWriteSequence() throws Exception {
|
||||
assertScenario(Scenario.WARP_MODULES, 0xdf617c77d3c3cc5eL, 67798);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void warpOnlyTerrainProfileMatchesGoldenWriteSequence() throws Exception {
|
||||
assertScenario(Scenario.WARP_TERRAIN, 0x7e0b63cab5bb3904L, 67036);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void styledThresholdBlendProfileMatchesGoldenWriteSequence() throws Exception {
|
||||
assertScenario(Scenario.STYLED_BLEND, 0x25792d49143774faL, 30387);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fluidBoundaryProfileMatchesGoldenWriteSequence() throws Exception {
|
||||
assertScenario(Scenario.FLUID_BOUNDARY, 0xb102145856119afeL, 17584);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noWarpModulesProfileMatchesGoldenWriteSequence() throws Exception {
|
||||
assertScenario(Scenario.NO_WARP_MODULES, 0x5a83dc20c89c6398L, 47534);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verticalStepProfileMatchesGoldenWriteSequence() throws Exception {
|
||||
assertScenario(Scenario.VERTICAL_STEP, 0xac0413031ed65b61L, 48011);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void latticeProfileMatchesGoldenWriteSequence() throws Exception {
|
||||
assertScenario(Scenario.LATTICE, 0xc7ffab155e6bdef9L, 72714);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void floorFluidProfilesSharingChunksMatchGoldenWriteSequence() throws Exception {
|
||||
assertScenario(Scenario.FLOOR_FLUID_LAYERED, 0xb4c0fb23e071617dL, 68747);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void packStyleProfilesSharingFieldsMatchGoldenWriteSequence() throws Exception {
|
||||
assertScenario(Scenario.PACK_SHARED, 0xdbe20a1ca87f9cc7L, 269035);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cellKernelDecidesCellsWithoutColumnEvaluation() throws Exception {
|
||||
IrisCaveCarver3D.CellCounters before = IrisCaveCarver3D.cellCounters();
|
||||
run(Scenario.WARP_MODULES, false);
|
||||
IrisCaveCarver3D.CellCounters after = IrisCaveCarver3D.cellCounters();
|
||||
long decided = after.decided() - before.decided();
|
||||
long evaluated = after.evaluated() - before.evaluated();
|
||||
assertTrue("decided=" + decided + " evaluated=" + evaluated, decided > 0L && evaluated > 0L);
|
||||
}
|
||||
|
||||
private void assertScenario(Scenario scenario, long expectedHash, int expectedWrites) throws Exception {
|
||||
Result serial = run(scenario, false);
|
||||
Result parallel = run(scenario, true);
|
||||
assertTrue(serial.carved > 0);
|
||||
assertEquals(serial.hash, parallel.hash);
|
||||
assertEquals(serial.writes, parallel.writes);
|
||||
assertEquals(serial.carved, parallel.carved);
|
||||
assertEquals(expectedHash, serial.hash);
|
||||
assertEquals(expectedWrites, serial.writes);
|
||||
}
|
||||
|
||||
private Result run(Scenario scenario, boolean parallel) throws Exception {
|
||||
if (!parallel) {
|
||||
return carveArea(scenario);
|
||||
}
|
||||
ForkJoinPool pool = new ForkJoinPool(4);
|
||||
try {
|
||||
return pool.submit(() -> carveArea(scenario)).get(120, TimeUnit.SECONDS);
|
||||
} finally {
|
||||
pool.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
private Result carveArea(Scenario scenario) {
|
||||
Engine engine = createEngine(scenario);
|
||||
CaveDensityGrid grid = new CaveDensityGrid(4096);
|
||||
IrisCaveProfile[] profiles = scenario.profiles();
|
||||
IrisCaveCarver3D[] carvers = new IrisCaveCarver3D[profiles.length];
|
||||
for (int index = 0; index < profiles.length; index++) {
|
||||
carvers[index] = new IrisCaveCarver3D(engine, profiles[index], grid);
|
||||
}
|
||||
Capture capture = new Capture();
|
||||
int carved = 0;
|
||||
for (int chunkX = -2; chunkX < CHUNK_SPAN - 2; chunkX++) {
|
||||
for (int chunkZ = -1; chunkZ < CHUNK_SPAN - 1; chunkZ++) {
|
||||
MantleWriter writer = capture.writer(chunkX, chunkZ);
|
||||
CaveFluidSupportPlan supportPlan = new CaveFluidSupportPlan();
|
||||
for (int index = 0; index < carvers.length; index++) {
|
||||
carved += carvers[index].carve(writer, chunkX, chunkZ, scenario.weights(index, chunkX, chunkZ),
|
||||
0.08D, 0.24D, scenario.worldYRange(), scenario.surfaceHeights(chunkX, chunkZ),
|
||||
scenario.boundaries(chunkX, chunkZ), null, supportPlan);
|
||||
}
|
||||
capture.resolving = true;
|
||||
supportPlan.resolve(writer.acquireChunk(chunkX, chunkZ));
|
||||
capture.finishResolve();
|
||||
}
|
||||
}
|
||||
return new Result(capture.hash, capture.writes, carved);
|
||||
}
|
||||
|
||||
private Engine createEngine(Scenario scenario) {
|
||||
Engine engine = mock(Engine.class, withSettings().stubOnly());
|
||||
IrisData data = mock(IrisData.class, withSettings().stubOnly());
|
||||
IrisDimension dimension = mock(IrisDimension.class, withSettings().stubOnly());
|
||||
SeedManager seedManager = mock(SeedManager.class, withSettings().stubOnly());
|
||||
doReturn(data).when(engine).getData();
|
||||
doReturn(dimension).when(engine).getDimension();
|
||||
doReturn(seedManager).when(engine).getSeedManager();
|
||||
doReturn(CARVE_SEED).when(seedManager).getCarve();
|
||||
doReturn(new EngineMetrics(16)).when(engine).getMetrics();
|
||||
doReturn(IrisWorld.builder().minHeight(-32).maxHeight(WORLD_HEIGHT - 32).build()).when(engine).getWorld();
|
||||
doReturn(WORLD_HEIGHT).when(engine).getHeight();
|
||||
doReturn(110).when(engine).getHeight(anyInt(), anyInt());
|
||||
doReturn(24).when(dimension).getCaveLavaHeight();
|
||||
doReturn(70).when(dimension).getFluidHeight();
|
||||
doReturn(scenario == Scenario.VERTICAL_STEP ? 4 : 1).when(dimension).getCaveDensitySamplingStep();
|
||||
return engine;
|
||||
}
|
||||
|
||||
private static IrisCaveProfile baseProfile(boolean warp, boolean modules) {
|
||||
IrisCaveProfile profile = new IrisCaveProfile();
|
||||
profile.setEnabled(true);
|
||||
profile.setVerticalRange(new IrisRange(4D, 150D));
|
||||
profile.setVerticalEdgeFade(14);
|
||||
profile.setVerticalEdgeFadeStrength(0.21D);
|
||||
profile.setBaseDensityStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.21D));
|
||||
profile.setDetailDensityStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.43D));
|
||||
profile.setWarpStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.37D));
|
||||
profile.setSurfaceBreakStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.140625D));
|
||||
profile.setBaseWeight(1D);
|
||||
profile.setDetailWeight(0.42D);
|
||||
profile.setWarpStrength(warp ? 3.7D : 0D);
|
||||
profile.setDensityThreshold(new IrisStyledRange(-0.18D, -0.18D, new IrisGeneratorStyle(NoiseStyle.FLAT)));
|
||||
profile.setThresholdBias(0D);
|
||||
profile.setSampleStep(1);
|
||||
profile.setSurfaceClearance(5);
|
||||
profile.setAllowSurfaceBreak(true);
|
||||
profile.setSurfaceBreakNoiseThreshold(0.3D);
|
||||
profile.setSurfaceBreakDepth(14);
|
||||
profile.setSurfaceBreakThresholdBoost(0.21D);
|
||||
profile.setAllowFluid(false);
|
||||
profile.setAllowLava(true);
|
||||
KList<IrisCaveFieldModule> caveModules = new KList<>();
|
||||
if (modules) {
|
||||
caveModules.add(new IrisCaveFieldModule(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.33D),
|
||||
0.27D, 0.05D, new IrisRange(0D, 90D), false));
|
||||
caveModules.add(new IrisCaveFieldModule(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.51D),
|
||||
0.19D, -0.07D, new IrisRange(40D, 150D), true));
|
||||
}
|
||||
profile.setModules(caveModules);
|
||||
return profile;
|
||||
}
|
||||
|
||||
private static IrisCaveProfile packProfile(NoiseStyle warp, double warpZoom, double warpStrength, NoiseStyle base,
|
||||
double baseZoom, double threshold, IrisCaveFieldModule... modules) {
|
||||
IrisCaveProfile profile = baseProfile(true, false);
|
||||
profile.setWarpStyle(new IrisGeneratorStyle(warp).zoomed(warpZoom));
|
||||
profile.setWarpStrength(warpStrength);
|
||||
profile.setBaseDensityStyle(new IrisGeneratorStyle(base).zoomed(baseZoom));
|
||||
profile.setDetailDensityStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.5D));
|
||||
profile.setBaseWeight(0.82D);
|
||||
profile.setDetailWeight(0.15D);
|
||||
profile.setDensityThreshold(new IrisStyledRange(threshold - 0.06D, threshold + 0.05D,
|
||||
new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.78D)));
|
||||
profile.setThresholdBias(0.12D);
|
||||
profile.setSampleStep(2);
|
||||
profile.setModules(new KList<>(modules));
|
||||
return profile;
|
||||
}
|
||||
|
||||
private static IrisCaveFieldModule packModule(NoiseStyle style, double zoom, double weight, double threshold,
|
||||
double minY, double maxY, boolean invert) {
|
||||
return new IrisCaveFieldModule(new IrisGeneratorStyle(style).zoomed(zoom), weight, threshold,
|
||||
new IrisRange(minY, maxY), invert);
|
||||
}
|
||||
|
||||
private enum Scenario {
|
||||
PACK_SHARED,
|
||||
WARP_MODULES,
|
||||
WARP_TERRAIN,
|
||||
STYLED_BLEND,
|
||||
FLUID_BOUNDARY,
|
||||
NO_WARP_MODULES,
|
||||
VERTICAL_STEP,
|
||||
LATTICE,
|
||||
FLOOR_FLUID_LAYERED;
|
||||
|
||||
IrisCaveProfile[] profiles() {
|
||||
if (this == PACK_SHARED) {
|
||||
return new IrisCaveProfile[]{
|
||||
packProfile(NoiseStyle.FRACTAL_WATER, 0.44D, 3.2D, NoiseStyle.PERLIN_IRIS, 0.72D, -0.02D,
|
||||
packModule(NoiseStyle.FRACTAL_FBM_SIMPLEX, 0.24D, 0.08D, 0.03D, 10D, 140D, false),
|
||||
packModule(NoiseStyle.SIMPLEX_VASCULAR, 1.02D, 0.05D, 0.07D, 4D, 90D, true)),
|
||||
packProfile(NoiseStyle.FRACTAL_WATER, 0.44D, 3.2D, NoiseStyle.FRACTAL_BILLOW_SIMPLEX, 0.656D, 0.01D,
|
||||
packModule(NoiseStyle.CELLULAR_BILINEAR, 1.32D, 0.06D, 0.02D, 20D, 150D, false)),
|
||||
packProfile(NoiseStyle.FRACTAL_WATER, 0.44D, 3.2D, NoiseStyle.PERLIN_IRIS, 0.72D, 0.04D,
|
||||
packModule(NoiseStyle.FRACTAL_FBM_SIMPLEX, 0.24D, 0.08D, 0.03D, 10D, 140D, false),
|
||||
packModule(NoiseStyle.SIMPLEX_VASCULAR, 1.02D, 0.05D, 0.07D, 4D, 90D, true)),
|
||||
packProfile(NoiseStyle.PERLIN_IRIS, 0.35D, 2.4D, NoiseStyle.IRIS_DOUBLE, 0.68D, -0.04D,
|
||||
packModule(NoiseStyle.VASCULAR_THIN, 1.28D, 0.04D, 0.05D, 8D, 120D, false))
|
||||
};
|
||||
}
|
||||
if (this == FLOOR_FLUID_LAYERED) {
|
||||
return new IrisCaveProfile[]{
|
||||
baseProfile(true, true).setAllowFluid(true).setFluidRequiresFloor(true).setFluidMinDepthBelowSurface(4),
|
||||
baseProfile(true, false).setAllowFluid(true).setFluidRequiresFloor(true).setFluidMinDepthBelowSurface(2)
|
||||
.setAllowLava(false).setDensityThreshold(new IrisStyledRange(-0.24D, -0.08D,
|
||||
new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.45D)))
|
||||
};
|
||||
}
|
||||
return new IrisCaveProfile[]{profile()};
|
||||
}
|
||||
|
||||
IrisCaveProfile profile() {
|
||||
return switch (this) {
|
||||
case WARP_MODULES, VERTICAL_STEP -> baseProfile(true, true);
|
||||
case WARP_TERRAIN -> baseProfile(true, false);
|
||||
case STYLED_BLEND -> baseProfile(false, false).setDensityThreshold(new IrisStyledRange(-0.32D, -0.04D,
|
||||
new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.6D)));
|
||||
case FLUID_BOUNDARY -> baseProfile(true, true).setAllowFluid(true).setFluidRequiresFloor(false)
|
||||
.setFluidMinDepthBelowSurface(6).setAllowLava(false);
|
||||
case NO_WARP_MODULES -> baseProfile(false, true);
|
||||
case LATTICE -> baseProfile(true, true).setSampleStep(3);
|
||||
case FLOOR_FLUID_LAYERED, PACK_SHARED -> baseProfile(true, true);
|
||||
};
|
||||
}
|
||||
|
||||
double[] weights(int layer, int chunkX, int chunkZ) {
|
||||
double[] weights = new double[256];
|
||||
for (int index = 0; index < 256; index++) {
|
||||
int worldX = (chunkX << 4) + (index >> 4);
|
||||
int worldZ = (chunkZ << 4) + (index & 15);
|
||||
double blend = 0.5D + 0.6D * Math.sin(worldX * 0.11D) * Math.cos(worldZ * 0.07D);
|
||||
weights[index] = switch (this) {
|
||||
case PACK_SHARED -> 0.5D + 0.6D * Math.sin(worldX * 0.13D + layer * 1.7D) * Math.cos(worldZ * 0.09D - layer);
|
||||
case STYLED_BLEND, FLUID_BOUNDARY -> blend;
|
||||
case FLOOR_FLUID_LAYERED -> layer == 0 ? blend : 1D - blend;
|
||||
default -> 1D;
|
||||
};
|
||||
}
|
||||
return weights;
|
||||
}
|
||||
|
||||
int[] surfaceHeights(int chunkX, int chunkZ) {
|
||||
int[] heights = new int[256];
|
||||
for (int index = 0; index < 256; index++) {
|
||||
int worldX = (chunkX << 4) + (index >> 4);
|
||||
int worldZ = (chunkZ << 4) + (index & 15);
|
||||
heights[index] = this == WARP_MODULES || this == LATTICE || this == FLOOR_FLUID_LAYERED
|
||||
? 118
|
||||
: 96 + (int) Math.round(22D * Math.sin(worldX * 0.09D) + 15D * Math.cos(worldZ * 0.13D));
|
||||
}
|
||||
return heights;
|
||||
}
|
||||
|
||||
IrisRange worldYRange() {
|
||||
return this == FLUID_BOUNDARY || this == FLOOR_FLUID_LAYERED ? new IrisRange(-20D, 100D) : null;
|
||||
}
|
||||
|
||||
long[] boundaries(int chunkX, int chunkZ) {
|
||||
long[] boundaries = new long[256];
|
||||
Arrays.fill(boundaries, SurfaceFluidBoundaryPlan.boundary(SurfaceFluidBoundaryPlan.NO_BOUNDARY, Integer.MIN_VALUE));
|
||||
if (this != FLUID_BOUNDARY && this != WARP_TERRAIN) {
|
||||
return boundaries;
|
||||
}
|
||||
for (int index = 0; index < 256; index++) {
|
||||
int worldX = (chunkX << 4) + (index >> 4);
|
||||
if (Math.floorMod(worldX + chunkZ, 5) == 0) {
|
||||
boundaries[index] = SurfaceFluidBoundaryPlan.boundary(60 + Math.floorMod(index, 7), 82);
|
||||
}
|
||||
}
|
||||
return boundaries;
|
||||
}
|
||||
}
|
||||
|
||||
private record Result(long hash, int writes, int carved) {
|
||||
}
|
||||
|
||||
private static final class Capture {
|
||||
private final Map<Long, Map<Integer, Matter>> chunks = new HashMap<>();
|
||||
private final IdentityHashMap<MatterCavern, Integer> identities = new IdentityHashMap<>();
|
||||
private final List<Long> resolved = new ArrayList<>();
|
||||
private long hash = 0xcbf29ce484222325L;
|
||||
private int writes;
|
||||
private boolean resolving;
|
||||
|
||||
private MantleWriter writer(int chunkX, int chunkZ) {
|
||||
MantleWriter writer = mock(MantleWriter.class, withSettings().stubOnly());
|
||||
@SuppressWarnings("unchecked")
|
||||
Mantle<Matter> mantle = mock(Mantle.class, withSettings().stubOnly());
|
||||
@SuppressWarnings("unchecked")
|
||||
MantleChunk<Matter> chunk = mock(MantleChunk.class, withSettings().stubOnly());
|
||||
long key = ((long) chunkX << 32) ^ (chunkZ & 0xffffffffL);
|
||||
Map<Integer, Matter> sections = chunks.computeIfAbsent(key, ignored -> new HashMap<>());
|
||||
doReturn(mantle).when(writer).getMantle();
|
||||
doReturn(WORLD_HEIGHT).when(mantle).getWorldHeight();
|
||||
doReturn(chunk).when(writer).acquireChunk(anyInt(), anyInt());
|
||||
doAnswer(invocation -> sections.get(invocation.getArgument(0, Integer.class))).when(chunk).get(anyInt());
|
||||
doAnswer(invocation -> sections.computeIfAbsent(invocation.getArgument(0, Integer.class),
|
||||
sectionIndex -> createSection(chunkX, chunkZ, sectionIndex))).when(chunk).getOrCreate(anyInt());
|
||||
return writer;
|
||||
}
|
||||
|
||||
private Matter createSection(int chunkX, int chunkZ, int sectionIndex) {
|
||||
mix(0x5EC7L);
|
||||
mix(chunkX);
|
||||
mix(chunkZ);
|
||||
mix(sectionIndex);
|
||||
Matter matter = mock(Matter.class, withSettings().stubOnly());
|
||||
MatterSlice<MatterCavern> slice = new RecordingCavernMatter(this, sectionIndex);
|
||||
doReturn(slice).when(matter).slice(MatterCavern.class);
|
||||
doReturn(slice).when(matter).getSlice(MatterCavern.class);
|
||||
return matter;
|
||||
}
|
||||
|
||||
private void record(int sectionIndex, int x, int y, int z, MatterCavern value) {
|
||||
writes++;
|
||||
if (resolving) {
|
||||
resolved.add(((long) sectionIndex << 20) | ((long) x << 16) | ((long) y << 12) | ((long) z << 8) | value.getLiquid());
|
||||
return;
|
||||
}
|
||||
mix(sectionIndex);
|
||||
mix((x << 8) | (y << 4) | z);
|
||||
mix(value == null ? -1 : value.getLiquid());
|
||||
Integer identity = identities.get(value);
|
||||
if (identity == null) {
|
||||
identity = identities.size();
|
||||
identities.put(value, identity);
|
||||
}
|
||||
mix(identity);
|
||||
}
|
||||
|
||||
private void finishResolve() {
|
||||
resolving = false;
|
||||
resolved.sort(Long::compare);
|
||||
for (long entry : resolved) {
|
||||
mix(entry);
|
||||
}
|
||||
resolved.clear();
|
||||
}
|
||||
|
||||
private void mix(long value) {
|
||||
hash ^= value;
|
||||
hash *= 0x100000001b3L;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class RecordingCavernMatter extends CavernMatter {
|
||||
private final Capture capture;
|
||||
private final int sectionIndex;
|
||||
|
||||
private RecordingCavernMatter(Capture capture, int sectionIndex) {
|
||||
super(16, 16, 16);
|
||||
this.capture = capture;
|
||||
this.sectionIndex = sectionIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void set(int x, int y, int z, MatterCavern value) {
|
||||
super.set(x, y, z, value);
|
||||
capture.record(sectionIndex, x, y, z, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
package art.arcane.iris.generation.mantle;
|
||||
|
||||
import art.arcane.iris.generation.cave.IrisCaveFieldModule;
|
||||
import art.arcane.iris.generation.cave.IrisCaveProfile;
|
||||
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
|
||||
import art.arcane.iris.generation.noise.IrisStyledRange;
|
||||
import art.arcane.iris.generation.noise.NoiseStyle;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.EngineMetrics;
|
||||
import art.arcane.iris.generation.runtime.SeedManager;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.pack.value.IrisRange;
|
||||
import art.arcane.iris.world.IrisWorld;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Random;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.withSettings;
|
||||
|
||||
public class IrisCaveCarver3DLatticeTest {
|
||||
private static final long CARVE_SEED = 7_340_119_802_557L;
|
||||
private static final int WORLD_HEIGHT = 384;
|
||||
|
||||
@Test
|
||||
public void batchedLatticeColumnsMatchScalarPlaneDensityBitForBit() throws Exception {
|
||||
Engine engine = createEngine();
|
||||
CaveDensityGrid grid = new CaveDensityGrid(256);
|
||||
IrisCaveProfile[] profiles = {
|
||||
profile(NoiseStyle.FRACTAL_WATER, 0.44D, 3.2D, NoiseStyle.PERLIN_IRIS, 0.72D,
|
||||
module(NoiseStyle.FRACTAL_FBM_SIMPLEX, 0.24D, 10D, 140D, false),
|
||||
module(NoiseStyle.CELLULAR_BILINEAR, 1.32D, 4D, 300D, true)),
|
||||
profile(NoiseStyle.PERLIN_IRIS, 0.35D, 2.4D, NoiseStyle.IRIS_DOUBLE, 0.68D,
|
||||
module(NoiseStyle.VASCULAR_THIN, 1.28D, 8D, 220D, false)),
|
||||
profile(NoiseStyle.SIMPLEX, 0.5D, 0D, NoiseStyle.FRACTAL_BILLOW_SIMPLEX, 0.656D)
|
||||
};
|
||||
Method sampleLatticeColumn = IrisCaveCarver3D.class.getDeclaredMethod("sampleLatticeColumn",
|
||||
CaveCarveScratch.class, CaveDensityGrid.Column.class, int.class, int.class, int.class);
|
||||
sampleLatticeColumn.setAccessible(true);
|
||||
Method samplePlaneDensity = IrisCaveCarver3D.class.getDeclaredMethod("samplePlaneDensity",
|
||||
CaveCarveScratch.class, int.class, int.class, int.class);
|
||||
samplePlaneDensity.setAccessible(true);
|
||||
Field warpOwnerField = IrisCaveCarver3D.class.getDeclaredField("warpOwner");
|
||||
warpOwnerField.setAccessible(true);
|
||||
Random random = new Random(91_337L);
|
||||
for (IrisCaveProfile profile : profiles) {
|
||||
IrisCaveCarver3D carver = new IrisCaveCarver3D(engine, profile, grid);
|
||||
CaveDensityGrid.Owner warpOwner = (CaveDensityGrid.Owner) warpOwnerField.get(carver);
|
||||
CaveCarveScratch batch = new CaveCarveScratch();
|
||||
CaveCarveScratch reference = new CaveCarveScratch();
|
||||
for (int trial = 0; trial < 48; trial++) {
|
||||
int latticeX = random.nextInt(4001) - 2000;
|
||||
int latticeZ = random.nextInt(4001) - 2000;
|
||||
int x = latticeX << CaveDensityGrid.LATTICE_SHIFT;
|
||||
int z = latticeZ << CaveDensityGrid.LATTICE_SHIFT;
|
||||
int count = selectRows(random, batch);
|
||||
CaveDensityGrid.Column warpColumn = trial % 3 == 0 ? null
|
||||
: grid.warpColumn(warpOwner, WORLD_HEIGHT + 1, latticeX, latticeZ);
|
||||
for (int pass = 0; pass < 2; pass++) {
|
||||
sampleLatticeColumn.invoke(carver, batch, warpColumn, x, z, count);
|
||||
for (int index = 0; index < count; index++) {
|
||||
int y = batch.latticeY[index];
|
||||
double expected = (double) samplePlaneDensity.invoke(carver, reference, x, y, z);
|
||||
assertEquals("x=" + x + " y=" + y + " z=" + z + " pass=" + pass,
|
||||
Double.doubleToRawLongBits(expected), Double.doubleToRawLongBits(batch.latticeDensity[index]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void identicalFieldsShareGridOwnersAcrossProfiles() throws Exception {
|
||||
Engine engine = createEngine();
|
||||
CaveDensityGrid grid = new CaveDensityGrid(256);
|
||||
Field densityOwnerField = IrisCaveCarver3D.class.getDeclaredField("densityOwner");
|
||||
densityOwnerField.setAccessible(true);
|
||||
Field warpOwnerField = IrisCaveCarver3D.class.getDeclaredField("warpOwner");
|
||||
warpOwnerField.setAccessible(true);
|
||||
IrisCaveCarver3D first = new IrisCaveCarver3D(engine, profile(NoiseStyle.FRACTAL_WATER, 0.44D, 3.2D,
|
||||
NoiseStyle.PERLIN_IRIS, 0.72D, module(NoiseStyle.SIMPLEX_VASCULAR, 1.02D, 4D, 90D, true)), grid);
|
||||
IrisCaveCarver3D sameDensity = new IrisCaveCarver3D(engine, profile(NoiseStyle.FRACTAL_WATER, 0.44D, 3.2D,
|
||||
NoiseStyle.PERLIN_IRIS, 0.72D, module(NoiseStyle.SIMPLEX_VASCULAR, 1.02D, 4D, 90D, true))
|
||||
.setDensityThreshold(new IrisStyledRange(0.2D, 0.2D, new IrisGeneratorStyle(NoiseStyle.FLAT))), grid);
|
||||
IrisCaveCarver3D sameWarp = new IrisCaveCarver3D(engine, profile(NoiseStyle.FRACTAL_WATER, 0.44D, 1.7D,
|
||||
NoiseStyle.FRACTAL_BILLOW_SIMPLEX, 0.656D), grid);
|
||||
IrisCaveCarver3D otherWarp = new IrisCaveCarver3D(engine, profile(NoiseStyle.FRACTAL_WATER, 0.46D, 3.2D,
|
||||
NoiseStyle.PERLIN_IRIS, 0.72D, module(NoiseStyle.SIMPLEX_VASCULAR, 1.02D, 4D, 90D, true)), grid);
|
||||
IrisCaveCarver3D otherModule = new IrisCaveCarver3D(engine, profile(NoiseStyle.FRACTAL_WATER, 0.44D, 3.2D,
|
||||
NoiseStyle.PERLIN_IRIS, 0.72D, module(NoiseStyle.SIMPLEX_VASCULAR, 1.02D, 4D, 91D, true)), grid);
|
||||
assertSame(densityOwnerField.get(first), densityOwnerField.get(sameDensity));
|
||||
assertSame(warpOwnerField.get(first), warpOwnerField.get(sameDensity));
|
||||
assertSame(warpOwnerField.get(first), warpOwnerField.get(sameWarp));
|
||||
assertSame(warpOwnerField.get(first), warpOwnerField.get(otherModule));
|
||||
assertNotSame(densityOwnerField.get(first), densityOwnerField.get(sameWarp));
|
||||
assertNotSame(densityOwnerField.get(first), densityOwnerField.get(otherModule));
|
||||
assertNotSame(densityOwnerField.get(first), densityOwnerField.get(otherWarp));
|
||||
assertNotSame(warpOwnerField.get(first), warpOwnerField.get(otherWarp));
|
||||
}
|
||||
|
||||
private static int selectRows(Random random, CaveCarveScratch scratch) {
|
||||
int start = random.nextInt(40);
|
||||
int end = start + 20 + random.nextInt(WORLD_HEIGHT - start - 20);
|
||||
scratch.ensureLatticeBatch(end - start + 1);
|
||||
int count = 0;
|
||||
for (int y = start; y <= end; y++) {
|
||||
if (random.nextInt(5) != 0) {
|
||||
scratch.latticeY[count++] = y;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private static IrisCaveProfile profile(NoiseStyle warp, double warpZoom, double warpStrength, NoiseStyle base,
|
||||
double baseZoom, IrisCaveFieldModule... modules) {
|
||||
IrisCaveProfile profile = new IrisCaveProfile();
|
||||
profile.setEnabled(true);
|
||||
profile.setVerticalRange(new IrisRange(4D, 360D));
|
||||
profile.setWarpStyle(new IrisGeneratorStyle(warp).zoomed(warpZoom));
|
||||
profile.setWarpStrength(warpStrength);
|
||||
profile.setBaseDensityStyle(new IrisGeneratorStyle(base).zoomed(baseZoom));
|
||||
profile.setDetailDensityStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.5D));
|
||||
profile.setSurfaceBreakStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.82D));
|
||||
profile.setBaseWeight(0.82D);
|
||||
profile.setDetailWeight(0.15D);
|
||||
profile.setDensityThreshold(new IrisStyledRange(-0.1D, 0.02D, new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.78D)));
|
||||
profile.setSampleStep(2);
|
||||
profile.setModules(new KList<>(modules));
|
||||
return profile;
|
||||
}
|
||||
|
||||
private static IrisCaveFieldModule module(NoiseStyle style, double zoom, double minY, double maxY, boolean invert) {
|
||||
return new IrisCaveFieldModule(new IrisGeneratorStyle(style).zoomed(zoom), 0.07D, 0.04D, new IrisRange(minY, maxY), invert);
|
||||
}
|
||||
|
||||
private static Engine createEngine() {
|
||||
Engine engine = mock(Engine.class, withSettings().stubOnly());
|
||||
IrisData data = mock(IrisData.class, withSettings().stubOnly());
|
||||
IrisDimension dimension = mock(IrisDimension.class, withSettings().stubOnly());
|
||||
SeedManager seedManager = mock(SeedManager.class, withSettings().stubOnly());
|
||||
doReturn(data).when(engine).getData();
|
||||
doReturn(dimension).when(engine).getDimension();
|
||||
doReturn(seedManager).when(engine).getSeedManager();
|
||||
doReturn(CARVE_SEED).when(seedManager).getCarve();
|
||||
doReturn(new EngineMetrics(16)).when(engine).getMetrics();
|
||||
doReturn(IrisWorld.builder().minHeight(-64).maxHeight(WORLD_HEIGHT - 64).build()).when(engine).getWorld();
|
||||
doReturn(WORLD_HEIGHT).when(engine).getHeight();
|
||||
doReturn(200).when(engine).getHeight(anyInt(), anyInt());
|
||||
doReturn(20).when(dimension).getCaveLavaHeight();
|
||||
doReturn(64).when(dimension).getFluidHeight();
|
||||
doReturn(1).when(dimension).getCaveDensitySamplingStep();
|
||||
return engine;
|
||||
}
|
||||
}
|
||||
+27
-20
@@ -13,6 +13,7 @@ import art.arcane.iris.generation.noise.IrisStyledRange;
|
||||
import art.arcane.iris.world.IrisWorld;
|
||||
import art.arcane.iris.generation.noise.NoiseStyle;
|
||||
import art.arcane.volmlib.util.noise.CNG;
|
||||
import art.arcane.volmlib.util.noise.program.NoiseProgram;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
@@ -148,7 +149,7 @@ public class IrisCaveCarver3DNearParityTest {
|
||||
sampledY.add((int) y);
|
||||
return x * 0.0001D + y * 0.01D + z * 0.0002D;
|
||||
}).when(density).noiseFastSigned3D(anyDouble(), anyDouble(), anyDouble());
|
||||
baseDensityField.set(carver, density);
|
||||
baseDensityField.set(carver, NoiseProgram.compileSigned3D(density));
|
||||
Method prepare = IrisCaveCarver3D.class.getDeclaredMethod("prepareVerticalDensityPlane",
|
||||
CaveCarveScratch.class, int.class, int.class, int.class);
|
||||
prepare.setAccessible(true);
|
||||
@@ -168,7 +169,7 @@ public class IrisCaveCarver3DNearParityTest {
|
||||
doReturn(1).when(dimension).getCaveDensitySamplingStep();
|
||||
IrisCaveCarver3D scalar = new IrisCaveCarver3D(engine,
|
||||
createProfile(false, false).setDetailWeight(0D).setAllowFluid(false));
|
||||
baseDensityField.set(scalar, density);
|
||||
baseDensityField.set(scalar, NoiseProgram.compileSigned3D(density));
|
||||
CaveCarveScratch scalarScratch = new CaveCarveScratch();
|
||||
for (int index = 0; index < 256; index++) {
|
||||
scalarScratch.planeColumnIndices[index] = index;
|
||||
@@ -292,10 +293,10 @@ public class IrisCaveCarver3DNearParityTest {
|
||||
IrisCaveCarver3D carver = new IrisCaveCarver3D(engine, profile);
|
||||
CNG base = mock(CNG.class, withSettings().stubOnly());
|
||||
doReturn(1D).when(base).noiseFastSigned3D(anyDouble(), anyDouble(), anyDouble());
|
||||
baseDensityField.set(carver, base);
|
||||
baseDensityField.set(carver, NoiseProgram.compileSigned3D(base));
|
||||
CNG module = mock(CNG.class, withSettings().stubOnly());
|
||||
doReturn(-1D).when(module).noiseFastSigned3D(anyDouble(), anyDouble(), anyDouble());
|
||||
moduleDensityField.set(((CaveFieldModuleState[]) modulesField.get(carver))[0], module);
|
||||
moduleDensityField.set(((CaveFieldModuleState[]) modulesField.get(carver))[0], NoiseProgram.compileSigned3D(module));
|
||||
WriterCapture capture = createWriterCapture(128);
|
||||
carver.carve(capture.writer, -1, 2, fullWeights(), 0D, 0D, null, filledHeights(110));
|
||||
assertEquals(512, capture.carvedCells.size());
|
||||
@@ -364,13 +365,13 @@ public class IrisCaveCarver3DNearParityTest {
|
||||
}
|
||||
return -1D;
|
||||
}).when(density).noiseFastSigned3D(anyDouble(), anyDouble(), anyDouble());
|
||||
baseDensityField.set(carver, density);
|
||||
baseDensityField.set(carver, NoiseProgram.compileSigned3D(density));
|
||||
assertThrows(IllegalStateException.class, () -> carver.carve(capture.writer, 0, 0,
|
||||
fullWeights(), 0D, 0D, null, filledHeights(110)));
|
||||
} else {
|
||||
carver.carve(capture.writer, 0, 0, fullWeights(), 0D, 0D, null, filledHeights(110));
|
||||
assertFalse(capture.carvedCells.isEmpty());
|
||||
}
|
||||
assertFalse(capture.carvedCells.isEmpty());
|
||||
Field scratchField = IrisCaveCarver3D.class.getDeclaredField("scratchCache");
|
||||
scratchField.setAccessible(true);
|
||||
ThreadLocal<?> scratchCache = (ThreadLocal<?>) scratchField.get(carver);
|
||||
@@ -385,6 +386,12 @@ public class IrisCaveCarver3DNearParityTest {
|
||||
for (CaveDensityGrid.Column column : scratch.densityColumns) {
|
||||
assertNull(column);
|
||||
}
|
||||
for (CaveDensityGrid.Column column : scratch.cellLattice.densityColumns) {
|
||||
assertNull(column);
|
||||
}
|
||||
for (CaveDensityGrid.Column column : scratch.cellLattice.warpColumns) {
|
||||
assertNull(column);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -439,7 +446,7 @@ public class IrisCaveCarver3DNearParityTest {
|
||||
}).when(detail).noiseFastSigned3D(anyDouble(), anyDouble(), anyDouble());
|
||||
Field detailField = IrisCaveCarver3D.class.getDeclaredField("detailDensity");
|
||||
detailField.setAccessible(true);
|
||||
detailField.set(carver, detail);
|
||||
detailField.set(carver, NoiseProgram.compileSigned3D(detail));
|
||||
WriterCapture capture = createWriterCapture(128);
|
||||
try (ForkJoinPool pool = new ForkJoinPool(4)) {
|
||||
assertEquals(0, pool.submit(() -> carver.carve(capture.writer, 0, 0, fullWeights(),
|
||||
@@ -772,7 +779,7 @@ public class IrisCaveCarver3DNearParityTest {
|
||||
rejectedDensity.put("11:10:10", -1D);
|
||||
rejectedDensity.put("9:10:10", -1D);
|
||||
CoordinateDensityCNG rejectedNoise = new CoordinateDensityCNG(rejectedDensity);
|
||||
baseDensityField.set(carver, rejectedNoise);
|
||||
baseDensityField.set(carver, NoiseProgram.compileSigned3D(rejectedNoise));
|
||||
|
||||
boolean rejected = (boolean) aquiferCupSupportMethod.invoke(
|
||||
carver, new CaveCarveScratch(), 10, 10, 10, 0D);
|
||||
@@ -781,7 +788,7 @@ public class IrisCaveCarver3DNearParityTest {
|
||||
assertEquals(List.of("10:9:10", "10:8:10", "11:10:10", "9:10:10"), rejectedNoise.sampledCoordinates);
|
||||
|
||||
CoordinateDensityCNG acceptedNoise = new CoordinateDensityCNG(Map.of());
|
||||
baseDensityField.set(carver, acceptedNoise);
|
||||
baseDensityField.set(carver, NoiseProgram.compileSigned3D(acceptedNoise));
|
||||
|
||||
boolean accepted = (boolean) aquiferCupSupportMethod.invoke(
|
||||
carver, new CaveCarveScratch(), 10, 10, 10, 0D);
|
||||
@@ -1100,7 +1107,7 @@ public class IrisCaveCarver3DNearParityTest {
|
||||
AtomicLong isolatedSamples = new AtomicLong();
|
||||
IrisCaveCarver3D shared = new IrisCaveCarver3D(engine,
|
||||
createProfile(warp, modules).setAdaptiveSampling(true), new CaveDensityGrid(4096));
|
||||
baseDensityField.set(shared, new CountingCNG((CNG) baseDensityField.get(shared), sharedSamples));
|
||||
baseDensityField.set(shared, NoiseProgram.compileSigned3D(new CountingCNG((NoiseProgram) baseDensityField.get(shared), sharedSamples)));
|
||||
for (int chunkX = -1; chunkX <= 1; chunkX++) {
|
||||
for (int chunkZ = -1; chunkZ <= 1; chunkZ++) {
|
||||
WriterCapture sharedCapture = createWriterCapture(96);
|
||||
@@ -1108,7 +1115,7 @@ public class IrisCaveCarver3DNearParityTest {
|
||||
null, filledHeights(90));
|
||||
IrisCaveCarver3D isolated = new IrisCaveCarver3D(engine,
|
||||
createProfile(warp, modules).setAdaptiveSampling(true));
|
||||
baseDensityField.set(isolated, new CountingCNG((CNG) baseDensityField.get(isolated), isolatedSamples));
|
||||
baseDensityField.set(isolated, NoiseProgram.compileSigned3D(new CountingCNG((NoiseProgram) baseDensityField.get(isolated), isolatedSamples)));
|
||||
WriterCapture isolatedCapture = createWriterCapture(96);
|
||||
int isolatedCarved = isolated.carve(isolatedCapture.writer, chunkX, chunkZ, fullWeights(), 0D, 0D,
|
||||
null, filledHeights(90));
|
||||
@@ -1241,7 +1248,7 @@ public class IrisCaveCarver3DNearParityTest {
|
||||
new IrisCaveCarver3D(engine, profile).carve(expected.writer, -1, 2,
|
||||
fullWeights(), 0D, 0D, null, filledHeights(110));
|
||||
IrisCaveCarver3D carver = new IrisCaveCarver3D(engine, profile);
|
||||
CNG original = (CNG) baseDensityField.get(carver);
|
||||
NoiseProgram original = (NoiseProgram) baseDensityField.get(carver);
|
||||
CNG density = mock(CNG.class, withSettings().stubOnly());
|
||||
AtomicBoolean entered = new AtomicBoolean();
|
||||
WriterCapture nested = createWriterCapture(128);
|
||||
@@ -1252,9 +1259,9 @@ public class IrisCaveCarver3DNearParityTest {
|
||||
double x = invocation.getArgument(0);
|
||||
double y = invocation.getArgument(1);
|
||||
double z = invocation.getArgument(2);
|
||||
return original.noiseFastSigned3D(x, y, z);
|
||||
return original.sampleSigned3D(x, y, z);
|
||||
}).when(density).noiseFastSigned3D(anyDouble(), anyDouble(), anyDouble());
|
||||
baseDensityField.set(carver, density);
|
||||
baseDensityField.set(carver, NoiseProgram.compileSigned3D(density));
|
||||
WriterCapture actual = createWriterCapture(128);
|
||||
carver.carve(actual.writer, -1, 2, fullWeights(), 0D, 0D, null, filledHeights(110));
|
||||
assertTrue(entered.get());
|
||||
@@ -1268,7 +1275,7 @@ public class IrisCaveCarver3DNearParityTest {
|
||||
doAnswer(invocation -> {
|
||||
lookups.incrementAndGet();
|
||||
return invocation.callRealMethod();
|
||||
}).when(grid).column(any(), anyInt(), anyInt(), anyInt(), anyInt());
|
||||
}).when(grid).column(any(), anyInt(), anyInt(), anyInt());
|
||||
return grid;
|
||||
}
|
||||
|
||||
@@ -1292,10 +1299,10 @@ public class IrisCaveCarver3DNearParityTest {
|
||||
}
|
||||
|
||||
private void countSkippableSamples(IrisCaveCarver3D carver, AtomicLong samples) throws Exception {
|
||||
detailDensityField.set(carver, new CountingCNG((CNG) detailDensityField.get(carver), samples));
|
||||
detailDensityField.set(carver, NoiseProgram.compileSigned3D(new CountingCNG((NoiseProgram) detailDensityField.get(carver), samples)));
|
||||
CaveFieldModuleState[] modules = (CaveFieldModuleState[]) modulesField.get(carver);
|
||||
for (CaveFieldModuleState module : modules) {
|
||||
moduleDensityField.set(module, new CountingCNG((CNG) moduleDensityField.get(module), samples));
|
||||
moduleDensityField.set(module, NoiseProgram.compileSigned3D(new CountingCNG((NoiseProgram) moduleDensityField.get(module), samples)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1893,10 +1900,10 @@ public class IrisCaveCarver3DNearParityTest {
|
||||
}
|
||||
|
||||
private static final class CountingCNG extends CNG {
|
||||
private final CNG delegate;
|
||||
private final NoiseProgram delegate;
|
||||
private final AtomicLong samples;
|
||||
|
||||
private CountingCNG(CNG delegate, AtomicLong samples) {
|
||||
private CountingCNG(NoiseProgram delegate, AtomicLong samples) {
|
||||
super(new RNG(41_117L));
|
||||
this.delegate = delegate;
|
||||
this.samples = samples;
|
||||
@@ -1905,7 +1912,7 @@ public class IrisCaveCarver3DNearParityTest {
|
||||
@Override
|
||||
public double noiseFastSigned3D(double x, double y, double z) {
|
||||
samples.incrementAndGet();
|
||||
return delegate.noiseFastSigned3D(x, y, z);
|
||||
return delegate.sampleSigned3D(x, y, z);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-20
@@ -31,11 +31,6 @@ import art.arcane.iris.testsupport.PlatformBinding;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@@ -45,22 +40,12 @@ public class MantleFloatingObjectComponentBoundaryRadiusTest {
|
||||
@Rule
|
||||
public final PlatformBinding platform = PlatformBinding.mockPlatform();
|
||||
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void transformedFloatingPlacementExpandsOwnerRadius() throws Exception {
|
||||
File objectFile = temporaryFolder.newFile("floating.iob");
|
||||
try (DataOutputStream output = new DataOutputStream(new FileOutputStream(objectFile))) {
|
||||
output.writeInt(1);
|
||||
output.writeInt(1);
|
||||
output.writeInt(1);
|
||||
}
|
||||
|
||||
public void transformedFloatingPlacementExpandsOwnerRadius() {
|
||||
IrisObjectPlacement placement = new IrisObjectPlacement()
|
||||
.setPlace(new KList<>("test/floating"))
|
||||
.setTranslate(new IrisObjectTranslate().setX(32));
|
||||
.setTranslate(new IrisObjectTranslate().setX(-32));
|
||||
IrisFloatingChildBiomes floatingChild = new IrisFloatingChildBiomes()
|
||||
.setFloatingObjects(new KList<>(placement));
|
||||
IrisBiome biome = new IrisBiome().setFloatingChildBiomes(new KList<>(floatingChild));
|
||||
@@ -69,7 +54,7 @@ public class MantleFloatingObjectComponentBoundaryRadiusTest {
|
||||
when(dimension.getReachableBiomes(org.mockito.ArgumentMatchers.any())).thenReturn(new KList<>(biome));
|
||||
|
||||
ResourceLoader<IrisObject> objectLoader = mock(ResourceLoader.class);
|
||||
when(objectLoader.findFile("test/floating")).thenReturn(objectFile);
|
||||
when(objectLoader.load("test/floating")).thenReturn(new IrisObject(1, 1, 1));
|
||||
IrisData data = mock(IrisData.class);
|
||||
when(data.getObjectLoader()).thenReturn(objectLoader);
|
||||
|
||||
@@ -79,8 +64,8 @@ public class MantleFloatingObjectComponentBoundaryRadiusTest {
|
||||
when(engineMantle.getEngine()).thenReturn(engine);
|
||||
when(engineMantle.getData()).thenReturn(data);
|
||||
|
||||
assertEquals(33, new MantleFloatingObjectComponent(engineMantle).getRadius());
|
||||
assertEquals(32, new MantleFloatingObjectComponent(engineMantle).getRadius());
|
||||
when(dimension.getAllObjectScaleFactor()).thenReturn(2D);
|
||||
assertEquals(34, new MantleFloatingObjectComponent(engineMantle).getRadius());
|
||||
assertEquals(33, new MantleFloatingObjectComponent(engineMantle).getRadius());
|
||||
}
|
||||
}
|
||||
+32
-52
@@ -24,7 +24,6 @@ import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
import art.arcane.iris.testsupport.PlatformLeakGuard;
|
||||
import art.arcane.iris.generation.geometry.IrisBlockVector;
|
||||
import art.arcane.iris.generation.context.ChunkContext;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
@@ -33,13 +32,8 @@ import art.arcane.volmlib.util.matter.Matter;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
@@ -60,9 +54,6 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
||||
@ClassRule
|
||||
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
|
||||
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
@Before
|
||||
public void bindPlatform() {
|
||||
IrisPlatforms.unbind();
|
||||
@@ -92,7 +83,7 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
||||
.setRotation(IrisObjectRotation.of(90, 0, 0))
|
||||
.setTranslate(new IrisObjectTranslate().setX(3).setY(4));
|
||||
|
||||
assertEquals(53, MantleObjectComponent.calculatePlacementReach(new IrisBlockVector(5, 48, 7), placement, 1D));
|
||||
assertEquals(27, PlacementReach.of(new IrisObject(5, 48, 7), placement, 1D).horizontal());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -103,7 +94,7 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
||||
.setMode(ObjectPlaceMode.VACUUM)
|
||||
.setVacuumSettings(new IrisVacuumSettings().setRadius(12));
|
||||
|
||||
assertEquals(39, MantleObjectComponent.calculatePlacementReach(new IrisBlockVector(5, 4, 3), placement, placement.getMaximumScale(null)));
|
||||
assertEquals(22, PlacementReach.of(new IrisObject(5, 4, 3), placement, placement.getMaximumScale(null)).horizontal());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -126,15 +117,12 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
||||
public void inheritedRadiusMatchesScaledFrameAndExplicitScaleOverridesIt() {
|
||||
IrisDimension dimension = new IrisDimension().setAllObjectScaleFactor(2D);
|
||||
IrisObjectPlacement inherited = new IrisObjectPlacement();
|
||||
IrisBlockVector size = new IrisBlockVector(8, 8, 8);
|
||||
assertEquals(16, MantleObjectComponent.calculatePlacementReach(size, inherited,
|
||||
inherited.getMaximumScale(dimension)));
|
||||
IrisObject object = new IrisObject(8, 8, 8);
|
||||
assertEquals(8, PlacementReach.of(object, inherited, inherited.getMaximumScale(dimension)).horizontal());
|
||||
dimension.setAllObjectScaleFactor(0.5D);
|
||||
assertEquals(4, MantleObjectComponent.calculatePlacementReach(size, inherited,
|
||||
inherited.getMaximumScale(dimension)));
|
||||
assertEquals(2, PlacementReach.of(object, inherited, inherited.getMaximumScale(dimension)).horizontal());
|
||||
inherited.setScale(new IrisObjectScale().setSize(1D));
|
||||
assertEquals(8, MantleObjectComponent.calculatePlacementReach(size, inherited,
|
||||
inherited.getMaximumScale(dimension)));
|
||||
assertEquals(4, PlacementReach.of(object, inherited, inherited.getMaximumScale(dimension)).horizontal());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -183,10 +171,11 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
||||
|
||||
@Test
|
||||
public void collisionInputRadiusCoversBothSourceTraversalLegs() {
|
||||
assertEquals(82, MantleObjectComponent.calculateInputRadius(33, false));
|
||||
assertEquals(130, MantleObjectComponent.calculateInputRadius(33, true));
|
||||
assertEquals(84, MantleObjectComponent.calculateInputRadius(33, 35, false));
|
||||
assertEquals(132, MantleObjectComponent.calculateInputRadius(33, 35, true));
|
||||
assertEquals(82, MantleObjectComponent.calculateInputRadius(33, 0, false));
|
||||
assertEquals(Integer.MAX_VALUE,
|
||||
MantleObjectComponent.calculateInputRadius(Integer.MAX_VALUE, true));
|
||||
MantleObjectComponent.calculateInputRadius(Integer.MAX_VALUE, Integer.MAX_VALUE, true));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -199,7 +188,7 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
||||
for (int radius = 0; radius < 300; radius++) {
|
||||
for (boolean collisions : new boolean[]{false, true}) {
|
||||
assertTrue(MantleObjectComponent.calculateEagerInputRadius(radius, collisions)
|
||||
<= MantleObjectComponent.calculateInputRadius(radius, collisions));
|
||||
<= MantleObjectComponent.calculateInputRadius(radius, radius, collisions));
|
||||
}
|
||||
}
|
||||
assertEquals(Integer.MAX_VALUE,
|
||||
@@ -231,27 +220,25 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
||||
when(engineMantle.getEngine()).thenReturn(engine);
|
||||
when(engineMantle.getData()).thenReturn(data);
|
||||
|
||||
assertEquals(53, new MantleObjectComponent(engineMantle).getRadius());
|
||||
assertEquals(27, new MantleObjectComponent(engineMantle).getRadius());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void upperDimensionObjectsDriveRadiusAndCollisionMode() {
|
||||
IrisObjectPlacement placement = new IrisObjectPlacement()
|
||||
.setPlace(new KList<>("upper/tree"))
|
||||
.setRotation(IrisObjectRotation.of(90, 0, 0))
|
||||
.setTranslate(new IrisObjectTranslate().setX(3).setY(4));
|
||||
placement.getForbiddenCollisions().add("upper/blocker");
|
||||
IrisData baseData = mock(IrisData.class);
|
||||
IrisData upperData = mock(IrisData.class);
|
||||
IrisProceduralPlacement proceduralPlacement = mock(IrisProceduralPlacement.class);
|
||||
when(proceduralPlacement.getVariantObjects(upperData))
|
||||
.thenReturn(new KList<>(new IrisObject(5, 48, 7)));
|
||||
when(proceduralPlacement.asPlacement()).thenReturn(placement);
|
||||
IrisProceduralObjects proceduralObjects = mock(IrisProceduralObjects.class);
|
||||
when(proceduralObjects.isEmpty()).thenReturn(false);
|
||||
when(proceduralObjects.getAllPlacements()).thenReturn(new KList<>(proceduralPlacement));
|
||||
ResourceLoader<IrisObject> upperObjects = mock(ResourceLoader.class);
|
||||
when(upperObjects.load("upper/tree")).thenReturn(new IrisObject(5, 48, 7));
|
||||
when(upperData.getObjectLoader()).thenReturn(upperObjects);
|
||||
IrisRegion upperRegion = mock(IrisRegion.class);
|
||||
when(upperRegion.getObjects()).thenReturn(new KList<>());
|
||||
when(upperRegion.getProceduralObjects()).thenReturn(proceduralObjects);
|
||||
when(upperRegion.getObjects()).thenReturn(new KList<>(placement));
|
||||
when(upperRegion.getSurfaceObjects()).thenReturn(new KList<>(placement));
|
||||
IrisDimension baseDimension = mock(IrisDimension.class);
|
||||
when(baseDimension.getAllObjectScaleFactor()).thenReturn(1D);
|
||||
when(baseDimension.isUpperDimensionObjects()).thenReturn(true);
|
||||
@@ -273,20 +260,13 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
||||
|
||||
MantleObjectComponent component = new MantleObjectComponent(engineMantle);
|
||||
|
||||
assertEquals(53, component.getRadius());
|
||||
assertEquals(182, component.getInputRadius());
|
||||
assertEquals(5, component.getRadius());
|
||||
assertEquals(41, component.getInputRadius());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void translatedTreeCachesDistantSourcesAndSchedulesOnlyItsDestination() throws Exception {
|
||||
File objectFile = temporaryFolder.newFile("tree.iob");
|
||||
try (DataOutputStream output = new DataOutputStream(new FileOutputStream(objectFile))) {
|
||||
output.writeInt(1);
|
||||
output.writeInt(1);
|
||||
output.writeInt(1);
|
||||
}
|
||||
|
||||
public void translatedTreeCachesDistantSourcesAndSchedulesOnlyItsDestination() {
|
||||
IrisObjectPlacement placement = new IrisObjectPlacement()
|
||||
.setPlace(new KList<>("test/tree"))
|
||||
.setTranslate(new IrisObjectTranslate().setX(32));
|
||||
@@ -298,7 +278,7 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
||||
when(dimension.getReachableBiomes(any())).thenReturn(new KList<>(biome));
|
||||
|
||||
ResourceLoader<IrisObject> objectLoader = mock(ResourceLoader.class);
|
||||
when(objectLoader.findFile("test/tree")).thenReturn(objectFile);
|
||||
when(objectLoader.load("test/tree")).thenReturn(new IrisObject(1, 1, 1));
|
||||
IrisData data = mock(IrisData.class);
|
||||
when(data.getObjectLoader()).thenReturn(objectLoader);
|
||||
|
||||
@@ -310,9 +290,9 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
||||
when(engineMantle.getData()).thenReturn(data);
|
||||
|
||||
MantleObjectComponent component = spy(new MantleObjectComponent(engineMantle));
|
||||
assertEquals(33, component.getRadius());
|
||||
assertEquals(32, component.getRadius());
|
||||
assertEquals(0, component.getOutputRadius());
|
||||
assertEquals(82, component.getInputRadius());
|
||||
assertEquals(68, component.getInputRadius());
|
||||
|
||||
Mantle<Matter> mantle = mock(Mantle.class);
|
||||
MantleChunk<Matter> chunk = mock(MantleChunk.class);
|
||||
@@ -341,9 +321,9 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
||||
|
||||
component.generateLayer(directWriter, 0, 0, mock(ChunkContext.class));
|
||||
|
||||
assertEquals(49, directOrigins.get());
|
||||
assertEquals(25, directOrigins.get());
|
||||
component.generateLayer(directWriter, 0, 0, mock(ChunkContext.class));
|
||||
assertEquals(49, directOrigins.get());
|
||||
assertEquals(25, directOrigins.get());
|
||||
|
||||
List<String> generatedChunks = new ArrayList<>();
|
||||
doAnswer(invocation -> {
|
||||
@@ -370,14 +350,14 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
||||
component.getRadius(),
|
||||
(chunkX, chunkZ) -> replayedSources.add(chunkX + "," + chunkZ)
|
||||
);
|
||||
assertEquals(49, replayedSources.size());
|
||||
assertEquals("-3,-3", replayedSources.getFirst());
|
||||
assertEquals("-3,-2", replayedSources.get(1));
|
||||
assertEquals("3,3", replayedSources.getLast());
|
||||
assertEquals(25, replayedSources.size());
|
||||
assertEquals("-2,-2", replayedSources.getFirst());
|
||||
assertEquals("-2,-1", replayedSources.get(1));
|
||||
assertEquals("2,2", replayedSources.getLast());
|
||||
|
||||
placement.getForbiddenCollisions().add("test/tree");
|
||||
component.hotload();
|
||||
assertEquals(130, component.getInputRadius());
|
||||
assertEquals(100, component.getInputRadius());
|
||||
}
|
||||
|
||||
private static final class TestMatterGenerator implements MatterGenerator {
|
||||
|
||||
+90
-7
@@ -76,7 +76,7 @@ public class MatterGeneratorConcurrencyTest {
|
||||
@Test
|
||||
public void externalWindowsBoundManagedBlockedComponentSubmissions() throws Exception {
|
||||
GeneratorFixture fixture = new GeneratorFixture(true);
|
||||
int maximum = 2 * MatterGenerator.COMPONENT_SUBMISSION_BATCH_SIZE;
|
||||
int maximum = 2 * MatterGenerator.COMPONENT_SUBMISSION_LIMIT;
|
||||
CountDownLatch admitted = new CountDownLatch(maximum);
|
||||
CountDownLatch excess = new CountDownLatch(1);
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
@@ -115,10 +115,10 @@ public class MatterGeneratorConcurrencyTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void externalWindowDefersSharedClaimsUntilItsOwnBatchesComplete() throws Exception {
|
||||
public void externalWindowDefersSharedClaimsUntilItsOwnSubmissionsComplete() throws Exception {
|
||||
GeneratorFixture fixture = new GeneratorFixture(true);
|
||||
List<MatterGenerator.MatterComponentTask> shared = new ArrayList<>();
|
||||
for (int z = -4; z < -4 + MatterGenerator.COMPONENT_SUBMISSION_BATCH_SIZE; z++) {
|
||||
for (int z = -4; z < -4 + MatterGenerator.COMPONENT_SUBMISSION_LIMIT; z++) {
|
||||
MatterGenerator.MatterTaskKey key = new MatterGenerator.MatterTaskKey(
|
||||
fixture.mantle, -4, z, ReservedFlag.OBJECT);
|
||||
MatterGenerator.MatterComponentTask task = new MatterGenerator.MatterComponentTask(key);
|
||||
@@ -137,7 +137,7 @@ public class MatterGeneratorConcurrencyTest {
|
||||
CompletableFuture<Void> generation = CompletableFuture.runAsync(
|
||||
() -> generator.generateMatter(0, 0, true, fixture.context));
|
||||
try {
|
||||
Await.until("all privately owned component batches", Duration.ofSeconds(10L),
|
||||
Await.until("all privately owned component submissions", Duration.ofSeconds(10L),
|
||||
() -> completed.get() == 81 - shared.size());
|
||||
assertFalse(generation.isDone());
|
||||
} finally {
|
||||
@@ -149,16 +149,17 @@ public class MatterGeneratorConcurrencyTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failedSubmissionBatchDrainsItsWritersBeforeReturning() throws Exception {
|
||||
public void failedSubmissionDrainsItsWritersBeforeReturning() throws Exception {
|
||||
GeneratorFixture fixture = new GeneratorFixture(true);
|
||||
CountDownLatch failed = new CountDownLatch(1);
|
||||
CountDownLatch started = new CountDownLatch(MatterGenerator.COMPONENT_SUBMISSION_BATCH_SIZE - 1);
|
||||
CountDownLatch started = new CountDownLatch(MatterGenerator.COMPONENT_SUBMISSION_LIMIT - 1);
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
IllegalStateException failure = new IllegalStateException("Component rejected its input");
|
||||
RecordingComponent component = new RecordingComponent(ReservedFlag.OBJECT, 0, 64) {
|
||||
@Override
|
||||
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
|
||||
if (x == -4 && z == -4) {
|
||||
await(started);
|
||||
failed.countDown();
|
||||
throw failure;
|
||||
}
|
||||
@@ -172,7 +173,7 @@ public class MatterGeneratorConcurrencyTest {
|
||||
try {
|
||||
await(failed);
|
||||
await(started);
|
||||
if (MatterGenerator.COMPONENT_SUBMISSION_BATCH_SIZE > 1) {
|
||||
if (MatterGenerator.COMPONENT_SUBMISSION_LIMIT > 1) {
|
||||
assertFalse(generation.isDone());
|
||||
verify(fixture.mantle.getChunk(-4, -4), never()).release();
|
||||
}
|
||||
@@ -186,6 +187,69 @@ public class MatterGeneratorConcurrencyTest {
|
||||
assertEquals(0, activeClaims(fixture, 0, 4));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void externalWindowKeepsSubmittingPastAStragglingComponent() throws Exception {
|
||||
GeneratorFixture fixture = new GeneratorFixture(true);
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
AtomicInteger completed = new AtomicInteger();
|
||||
RecordingComponent component = new RecordingComponent(ReservedFlag.OBJECT, 0, 64) {
|
||||
@Override
|
||||
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
|
||||
if (x == -4 && z == -4) {
|
||||
awaitManaged(release);
|
||||
}
|
||||
completed.incrementAndGet();
|
||||
}
|
||||
};
|
||||
TestMatterGenerator generator = fixture.generator(List.of(new MantlePass(List.of(component), 4, 0)));
|
||||
CompletableFuture<Void> generation = CompletableFuture.runAsync(
|
||||
() -> generator.generateMatter(0, 0, true, fixture.context));
|
||||
try {
|
||||
Await.until("every chunk but the straggler", Duration.ofSeconds(10L), () -> completed.get() == 80);
|
||||
assertFalse(generation.isDone());
|
||||
} finally {
|
||||
release.countDown();
|
||||
}
|
||||
generation.get(10L, TimeUnit.SECONDS);
|
||||
assertEquals(81, completed.get());
|
||||
assertEquals(0, activeClaims(fixture, 0, 4));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inlineWindowFinishesItsOwnChunksBeforeJoiningAClaimAnotherGenerationHolds() throws Exception {
|
||||
GeneratorFixture fixture = new GeneratorFixture(true);
|
||||
MantleChunk<Matter> held = fixture.mantle.getChunk(1, 0);
|
||||
CountDownLatch claimed = new CountDownLatch(1);
|
||||
CountDownLatch releaseClaim = new CountDownLatch(1);
|
||||
Set<Long> completed = ConcurrentHashMap.newKeySet();
|
||||
RecordingComponent component = new RecordingComponent(ReservedFlag.OBJECT, 0, 16) {
|
||||
@Override
|
||||
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
|
||||
completed.add((((long) x) << 32) ^ (z & 0xffffffffL));
|
||||
}
|
||||
};
|
||||
TestMatterGenerator generator = fixture.generator(List.of(new MantlePass(List.of(component), 1, 0)));
|
||||
CompletableFuture<Void> holder = CompletableFuture.runAsync(() -> {
|
||||
assertTrue(held.tryClaim(ReservedFlag.OBJECT));
|
||||
claimed.countDown();
|
||||
await(releaseClaim);
|
||||
held.completeClaim(ReservedFlag.OBJECT);
|
||||
});
|
||||
await(claimed);
|
||||
CompletableFuture<Void> generation = CompletableFuture.runAsync(
|
||||
() -> generator.generateMatter(0, 0, false, fixture.context));
|
||||
try {
|
||||
Await.until("the eight unclaimed chunks", Duration.ofSeconds(10L), () -> completed.size() == 8);
|
||||
assertFalse(generation.isDone());
|
||||
} finally {
|
||||
releaseClaim.countDown();
|
||||
}
|
||||
holder.get(10L, TimeUnit.SECONDS);
|
||||
generation.get(10L, TimeUnit.SECONDS);
|
||||
assertEquals(8, completed.size());
|
||||
assertTrue(held.isFlagged(ReservedFlag.OBJECT));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void boundedAndReentrantGenerationMatchSerialComponentResults() throws Exception {
|
||||
Map<Long, Long> serial = componentResults(false, false);
|
||||
@@ -812,6 +876,25 @@ public class MatterGeneratorConcurrencyTest {
|
||||
}
|
||||
return null;
|
||||
}).when(chunk).raiseFlagSuspend(any(), any(Runnable.class));
|
||||
when(chunk.tryClaim(any())).thenAnswer(invocation -> {
|
||||
if (flagged.contains(invocation.getArgument(0)) || !lock.tryLock()) {
|
||||
return false;
|
||||
}
|
||||
if (flagged.contains(invocation.getArgument(0))) {
|
||||
lock.unlock();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
doAnswer(invocation -> {
|
||||
flagged.add(invocation.getArgument(0));
|
||||
lock.unlock();
|
||||
return null;
|
||||
}).when(chunk).completeClaim(any());
|
||||
doAnswer(invocation -> {
|
||||
lock.unlock();
|
||||
return null;
|
||||
}).when(chunk).abandonClaim(any());
|
||||
return chunk;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,517 @@
|
||||
package art.arcane.iris.generation.mantle;
|
||||
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.biome.IrisFloatingChildBiomes;
|
||||
import art.arcane.iris.generation.decoration.IrisProceduralObjects;
|
||||
import art.arcane.iris.generation.decoration.IrisProceduralPlacement;
|
||||
import art.arcane.iris.generation.geometry.IrisBlockVector;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.MeteredCache;
|
||||
import art.arcane.iris.generation.runtime.PreservationRegistry;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
import art.arcane.iris.structure.object.FloatingObjectFootprint;
|
||||
import art.arcane.iris.structure.object.IrisAxisRotationClamp;
|
||||
import art.arcane.iris.structure.object.IrisObject;
|
||||
import art.arcane.iris.structure.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.structure.object.IrisObjectRotation;
|
||||
import art.arcane.iris.structure.object.IrisObjectScale;
|
||||
import art.arcane.iris.structure.object.IrisObjectTranslate;
|
||||
import art.arcane.iris.structure.object.IrisObjectPlacementScaleInterpolator;
|
||||
import art.arcane.iris.testsupport.PlatformLeakGuard;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.math.Vector3i;
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
|
||||
import org.junit.After;
|
||||
import org.junit.Assume;
|
||||
import org.junit.Before;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Answers;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.withSettings;
|
||||
|
||||
public class PlacementReachPackTest {
|
||||
@ClassRule
|
||||
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
|
||||
|
||||
private static final int SPIN_PERIOD = 360;
|
||||
private static final int MAXIMUM_CHECKED_KERNELS = 4096;
|
||||
private static final String V2_HEADER = "Iris V2 IOB;";
|
||||
|
||||
private File pack;
|
||||
private IrisData data;
|
||||
private IrisDimension dimension;
|
||||
|
||||
@Before
|
||||
public void loadPack() throws IOException {
|
||||
String configured = System.getenv("IRIS_TEST_PACK");
|
||||
Assume.assumeTrue(configured != null && !configured.isBlank());
|
||||
pack = new File(configured).getAbsoluteFile();
|
||||
Assume.assumeTrue(new File(pack, "objects").isDirectory());
|
||||
File[] dimensions = new File(pack, "dimensions").listFiles((folder, name) -> name.endsWith(".json"));
|
||||
Assume.assumeTrue(dimensions != null && dimensions.length > 0);
|
||||
bindPlatform(Files.createTempDirectory("iris-placement-reach").toFile());
|
||||
data = IrisData.get(pack);
|
||||
String dimensionKey = System.getenv().getOrDefault("IRIS_TEST_PACK_DIMENSION",
|
||||
dimensions[0].getName().substring(0, dimensions[0].getName().length() - 5));
|
||||
dimension = data.getDimensionLoader().load(dimensionKey);
|
||||
assertTrue("dimension " + dimensionKey + " must load", dimension != null);
|
||||
}
|
||||
|
||||
@After
|
||||
public void unbindPlatform() {
|
||||
IrisServices.remove(PreservationRegistry.class);
|
||||
IrisPlatforms.unbind();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void everyObjectFileStaysInsideItsReachUnderRightAngleRotations() throws IOException {
|
||||
List<IrisObjectPlacement> placements = List.of(
|
||||
new IrisObjectPlacement(),
|
||||
new IrisObjectPlacement().setRotation(rotation(new IrisAxisRotationClamp(true, false, 0D, 0D, 90D),
|
||||
new IrisAxisRotationClamp(true, false, 0D, 0D, 90D), new IrisAxisRotationClamp())),
|
||||
new IrisObjectPlacement().setRotation(rotation(new IrisAxisRotationClamp(),
|
||||
new IrisAxisRotationClamp(true, false, 0D, 0D, 90D), new IrisAxisRotationClamp(true, false, 0D, 0D, 90D))),
|
||||
new IrisObjectPlacement().setRotation(IrisObjectRotation.of(0D, 0D, 0D)),
|
||||
new IrisObjectPlacement().setRotation(rotation(new IrisAxisRotationClamp(),
|
||||
new IrisAxisRotationClamp(true, false, 0D, 0D, 45D), new IrisAxisRotationClamp()))
|
||||
.setTranslate(new IrisObjectTranslate().setX(7).setZ(-3)));
|
||||
List<Path> files;
|
||||
try (Stream<Path> walk = Files.walk(new File(pack, "objects").toPath())) {
|
||||
files = walk.filter(path -> path.toString().endsWith(".iob")).sorted().toList();
|
||||
}
|
||||
assertTrue(!files.isEmpty());
|
||||
Statistics statistics = new Statistics();
|
||||
NativeBlockState marker = mock(NativeBlockState.class, withSettings().stubOnly());
|
||||
for (Path file : files) {
|
||||
IrisObject object = readPositions(file, marker);
|
||||
for (IrisObjectPlacement placement : placements) {
|
||||
verifyCentered(file.toString(), object, placement, PlacementReach.of(object, placement, 1D), statistics);
|
||||
}
|
||||
}
|
||||
System.out.println("PLACEMENT_REACH_FILES objects=" + files.size() + " " + statistics);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void everyPackPlacementStaysInsideItsReach() {
|
||||
Statistics statistics = new Statistics();
|
||||
Set<IrisObjectPlacement> placements = new LinkedHashSet<>();
|
||||
for (IrisRegion region : dimension.getAllRegions(() -> data)) {
|
||||
placements.addAll(region.getObjects());
|
||||
}
|
||||
for (IrisBiome biome : dimension.getReachableBiomes(() -> data)) {
|
||||
placements.addAll(biome.getObjects());
|
||||
}
|
||||
for (IrisObjectPlacement placement : placements) {
|
||||
for (String key : placement.getPlace()) {
|
||||
IrisObject object = data.getObjectLoader().load(key);
|
||||
if (object == null) {
|
||||
continue;
|
||||
}
|
||||
PlacementReach reach = PlacementReach.of(object, placement, placement.getMaximumScale(dimension));
|
||||
forEachVariant(object, placement, 1D, variant -> verifyCentered(key, variant, placement, reach, statistics));
|
||||
}
|
||||
}
|
||||
for (IrisProceduralPlacement procedural : proceduralPlacements()) {
|
||||
KList<IrisObject> variants = procedural.getVariantObjects(data);
|
||||
if (variants == null) {
|
||||
continue;
|
||||
}
|
||||
IrisObjectPlacement placement = procedural.asPlacement();
|
||||
for (IrisObject variant : variants) {
|
||||
PlacementReach reach = PlacementReach.of(variant, placement, placement.getMaximumScale(dimension));
|
||||
forEachVariant(variant, placement, 1D,
|
||||
scaled -> verifyCentered(variant.getLoadKey(), scaled, placement, reach, statistics));
|
||||
}
|
||||
}
|
||||
assertTrue(statistics.checked > 0);
|
||||
System.out.println("PLACEMENT_REACH_PLACEMENTS placements=" + placements.size() + " " + statistics);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void floatingPlacementsStayInsideTheirReach() {
|
||||
Statistics statistics = new Statistics();
|
||||
for (IrisBiome biome : dimension.getReachableBiomes(() -> data)) {
|
||||
KList<IrisFloatingChildBiomes> entries = biome.getFloatingChildBiomes();
|
||||
if (entries == null) {
|
||||
continue;
|
||||
}
|
||||
for (IrisFloatingChildBiomes entry : entries) {
|
||||
double shrink = entry.hasObjectShrink() ? entry.getShrinkScale().getMaxScale() : 1D;
|
||||
IrisBiome target = entry.getRealBiome(biome, data);
|
||||
for (IrisObjectPlacement placement : listOrEmpty(entry.getFloatingObjects())) {
|
||||
verifyFloating(placement, shrink, false, statistics);
|
||||
}
|
||||
List<IrisObjectPlacement> anchored = new ArrayList<>();
|
||||
anchored.addAll(listOrEmpty(entry.getExtraObjects()));
|
||||
anchored.addAll(listOrEmpty(entry.getTopObjectOverrides()));
|
||||
anchored.addAll(listOrEmpty(entry.getBottomObjectOverrides()));
|
||||
if (target != null) {
|
||||
anchored.addAll(listOrEmpty(entry.resolveTopObjects(target)));
|
||||
anchored.addAll(listOrEmpty(entry.resolveBottomObjects(target)));
|
||||
}
|
||||
for (IrisObjectPlacement placement : anchored) {
|
||||
verifyFloating(placement, shrink, true, statistics);
|
||||
}
|
||||
}
|
||||
}
|
||||
System.out.println("PLACEMENT_REACH_FLOATING " + statistics);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reportsTheMantleWindow() {
|
||||
Engine engine = mock(Engine.class);
|
||||
when(engine.getDimension()).thenReturn(dimension);
|
||||
EngineMantle engineMantle = mock(EngineMantle.class);
|
||||
when(engineMantle.getEngine()).thenReturn(engine);
|
||||
when(engineMantle.getData()).thenReturn(data);
|
||||
MantleObjectComponent objects = new MantleObjectComponent(engineMantle);
|
||||
MantleFloatingObjectComponent floating = new MantleFloatingObjectComponent(engineMantle);
|
||||
int objectRadius = objects.getRadius();
|
||||
int objectInput = objects.getInputRadius();
|
||||
int objectEager = objects.getEagerInputRadius(objectInput);
|
||||
int floatingRadius = floating.getRadius();
|
||||
int floatingPassRadius = chunkRadius(floatingRadius);
|
||||
int objectPassRadius = chunkRadius(floatingRadius);
|
||||
int terrainRadius = chunkRadius(floatingRadius + objectEager + 1);
|
||||
int writerRadius = 2 * chunkRadius(floatingRadius + objectInput + 1);
|
||||
System.out.println("PLACEMENT_REACH_WINDOW objectReach=" + objectRadius
|
||||
+ " objectInput=" + objectInput
|
||||
+ " objectEager=" + objectEager
|
||||
+ " sourceChunkRadius=" + MantleObjectComponent.sourceChunkRadius(objectRadius)
|
||||
+ " floatingReach=" + floatingRadius
|
||||
+ " floatingPassRadius=" + floatingPassRadius
|
||||
+ " objectPassRadius=" + objectPassRadius
|
||||
+ " terrainRadius=" + terrainRadius
|
||||
+ " terrainWindow=" + ((2 * terrainRadius) + 1) + "x" + ((2 * terrainRadius) + 1)
|
||||
+ " writerRadius=" + writerRadius);
|
||||
assertTrue(objectRadius > 0);
|
||||
}
|
||||
|
||||
private void verifyFloating(IrisObjectPlacement placement, double shrink, boolean anchored, Statistics statistics) {
|
||||
if (placement == null || placement.getPlace() == null) {
|
||||
return;
|
||||
}
|
||||
for (String key : placement.getPlace()) {
|
||||
IrisObject object = data.getObjectLoader().load(key);
|
||||
if (object == null) {
|
||||
continue;
|
||||
}
|
||||
PlacementReach.Extent extent = PlacementReach.Extent.of(object)
|
||||
.scaled(placement.getMaximumScale(dimension)).scaled(shrink);
|
||||
if (!anchored) {
|
||||
PlacementReach reach = PlacementReach.of(extent, placement);
|
||||
forEachVariant(object, placement, shrink, variant -> verifyCentered(key, variant, placement, reach, statistics));
|
||||
continue;
|
||||
}
|
||||
PlacementReach reach = PlacementReach.anchored(extent, placement);
|
||||
forEachVariant(object, placement, shrink, variant -> verifyAnchored(key, variant, reach, statistics));
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyAnchored(String key, IrisObject variant, PlacementReach reach, Statistics statistics) {
|
||||
FloatingObjectFootprint footprint = FloatingObjectFootprint.compute(variant);
|
||||
List<IrisBlockVector> positions = positions(variant);
|
||||
List<IrisObjectRotation> rotations = List.of(IrisObjectRotation.of(0D, 0D, 0D),
|
||||
IrisObjectRotation.xFlip180WithY(0D), IrisObjectRotation.xFlip180WithY(90D),
|
||||
IrisObjectRotation.xFlip180WithY(180D), IrisObjectRotation.xFlip180WithY(270D));
|
||||
for (IrisObjectRotation rotation : rotations) {
|
||||
IrisBlockVector anchor = rotation.rotate(new IrisBlockVector(footprint.getTallestKx(), 0, footprint.getTallestKz()), 0, 0, 0);
|
||||
int actual = 0;
|
||||
for (IrisBlockVector position : positions) {
|
||||
IrisBlockVector rotated = rotation.rotate(position, 0, 0, 0);
|
||||
actual = Math.max(actual, Math.abs((int) Math.round(rotated.getX()) - anchor.getBlockX()));
|
||||
actual = Math.max(actual, Math.abs((int) Math.round(rotated.getZ()) - anchor.getBlockZ()));
|
||||
}
|
||||
record(key, actual, reach.horizontal(), statistics);
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyCentered(String key, IrisObject object, IrisObjectPlacement placement, PlacementReach reach,
|
||||
Statistics statistics) {
|
||||
IrisObjectRotation rotation = placement.getRotation();
|
||||
IrisObjectTranslate translate = placement.getTranslate();
|
||||
boolean translating = translate != null && translate.canTranslate();
|
||||
int[] spinsX = spins(rotation, 0);
|
||||
int[] spinsY = spins(rotation, 1);
|
||||
int[] spinsZ = spins(rotation, 2);
|
||||
List<IrisBlockVector> positions = positions(object);
|
||||
long kernels = (long) spinsX.length * spinsY.length * spinsZ.length;
|
||||
if (kernels > MAXIMUM_CHECKED_KERNELS) {
|
||||
fail("rotation for " + key + " has " + kernels + " kernels");
|
||||
}
|
||||
for (int spinX : spinsX) {
|
||||
for (int spinY : spinsY) {
|
||||
for (int spinZ : spinsZ) {
|
||||
IrisBlockVector offset = translating
|
||||
? rotation.rotate(new IrisBlockVector(translate.getX(), translate.getY(), translate.getZ()), spinX, spinY, spinZ)
|
||||
: null;
|
||||
int actual = 0;
|
||||
for (IrisBlockVector position : positions) {
|
||||
IrisBlockVector moved = rotation.rotate(position, spinX, spinY, spinZ).clone();
|
||||
if (offset != null) {
|
||||
moved.add(offset);
|
||||
}
|
||||
actual = Math.max(actual, Math.abs((int) Math.round(moved.getX())));
|
||||
actual = Math.max(actual, Math.abs((int) Math.round(moved.getZ())));
|
||||
}
|
||||
record(key, actual, reach.horizontal(), statistics);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IrisObject readPositions(Path file, NativeBlockState marker) throws IOException {
|
||||
try (DataInputStream input = new DataInputStream(new BufferedInputStream(Files.newInputStream(file)))) {
|
||||
IrisObject object = new IrisObject(input.readInt(), input.readInt(), input.readInt());
|
||||
input.mark(Short.BYTES + V2_HEADER.length());
|
||||
int headerLength = input.readUnsignedShort();
|
||||
boolean v2 = headerLength == V2_HEADER.length()
|
||||
&& V2_HEADER.equals(new String(input.readNBytes(headerLength), StandardCharsets.US_ASCII));
|
||||
if (!v2) {
|
||||
input.reset();
|
||||
}
|
||||
int paletteSize = v2 ? input.readShort() : 0;
|
||||
for (int index = 0; index < paletteSize; index++) {
|
||||
input.readUTF();
|
||||
}
|
||||
int blocks = input.readInt();
|
||||
for (int index = 0; index < blocks; index++) {
|
||||
IrisBlockVector position = new IrisBlockVector(input.readShort(), input.readShort(), input.readShort());
|
||||
if (v2) {
|
||||
input.readShort();
|
||||
} else {
|
||||
input.readUTF();
|
||||
}
|
||||
object.getBlocks().put(position, marker);
|
||||
}
|
||||
return object;
|
||||
}
|
||||
}
|
||||
|
||||
private static void record(String key, int actual, int reach, Statistics statistics) {
|
||||
statistics.checked++;
|
||||
statistics.maximumActual = Math.max(statistics.maximumActual, actual);
|
||||
statistics.maximumReach = Math.max(statistics.maximumReach, reach);
|
||||
statistics.slack += reach - actual;
|
||||
if (actual > reach) {
|
||||
fail(key + " places a block " + actual + " blocks from its origin, beyond its reach " + reach);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<IrisBlockVector> positions(IrisObject object) {
|
||||
List<IrisBlockVector> positions = new ArrayList<>(object.getBlocks().size() + 8);
|
||||
object.getBlocks().forEach((position, state) -> positions.add(position));
|
||||
Vector3i center = object.getCenter();
|
||||
int[] xs = {-center.getBlockX(), object.getW() - center.getBlockX() - 1};
|
||||
int[] ys = {-center.getBlockY(), object.getH() - center.getBlockY() - 1};
|
||||
int[] zs = {-center.getBlockZ(), object.getD() - center.getBlockZ() - 1};
|
||||
for (int x : xs) {
|
||||
for (int y : ys) {
|
||||
for (int z : zs) {
|
||||
positions.add(new IrisBlockVector(x, y, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
return positions;
|
||||
}
|
||||
|
||||
private void forEachVariant(IrisObject object, IrisObjectPlacement placement, double shrink, Consumer<IrisObject> consumer) {
|
||||
IrisObjectScale scale = placement.getScale();
|
||||
IrisObjectPlacementScaleInterpolator interpolation = scale == null || scale.getInterpolation() == null
|
||||
? IrisObjectPlacementScaleInterpolator.NONE : scale.getInterpolation();
|
||||
for (double factor : scales(placement)) {
|
||||
IrisObject variant = factor == 1D && (scale == null || !scale.shouldScale())
|
||||
? object
|
||||
: scale == null ? IrisObjectScale.getFixed(object, factor) : object.scaled(factor, interpolation);
|
||||
consumer.accept(shrink < 1D ? variant.scaled(shrink, IrisObjectPlacementScaleInterpolator.NONE) : variant);
|
||||
}
|
||||
}
|
||||
|
||||
private double[] scales(IrisObjectPlacement placement) {
|
||||
IrisObjectScale scale = placement.getScale();
|
||||
if (scale == null) {
|
||||
return new double[]{dimension.getAllObjectScaleFactor()};
|
||||
}
|
||||
if (!scale.shouldScale()) {
|
||||
return new double[]{1D};
|
||||
}
|
||||
if (scale.getSize() != 1D) {
|
||||
return new double[]{scale.getSize()};
|
||||
}
|
||||
if (scale.getMinimumScale() == scale.getMaximumScale()) {
|
||||
return new double[]{scale.getMinimumScale()};
|
||||
}
|
||||
int count = Math.max(1, Math.min(scale.getVariations(), 32));
|
||||
double[] scales = new double[count];
|
||||
for (int index = 0; index < count; index++) {
|
||||
scales[index] = scale.getMinimumScale() + (((scale.getMaximumScale() - scale.getMinimumScale()) / count) * index);
|
||||
}
|
||||
return scales;
|
||||
}
|
||||
|
||||
private List<IrisProceduralPlacement> proceduralPlacements() {
|
||||
List<IrisProceduralPlacement> placements = new ArrayList<>();
|
||||
for (IrisRegion region : dimension.getAllRegions(() -> data)) {
|
||||
addProcedural(placements, region.getProceduralObjects());
|
||||
}
|
||||
for (IrisBiome biome : dimension.getReachableBiomes(() -> data)) {
|
||||
addProcedural(placements, biome.getProceduralObjects());
|
||||
}
|
||||
return placements;
|
||||
}
|
||||
|
||||
private static void addProcedural(List<IrisProceduralPlacement> placements, IrisProceduralObjects procedural) {
|
||||
if (procedural != null && !procedural.isEmpty()) {
|
||||
placements.addAll(procedural.getAllPlacements());
|
||||
}
|
||||
}
|
||||
|
||||
private static int[] spins(IrisObjectRotation rotation, int axis) {
|
||||
if (rotation == null || !rotation.canRotate()) {
|
||||
return new int[]{0};
|
||||
}
|
||||
boolean rotates = switch (axis) {
|
||||
case 0 -> rotation.canRotateX();
|
||||
case 1 -> rotation.canRotateY();
|
||||
default -> rotation.canRotateZ();
|
||||
};
|
||||
if (!rotates) {
|
||||
return new int[]{0};
|
||||
}
|
||||
LongOpenHashSet angles = new LongOpenHashSet();
|
||||
IntArrayList spins = new IntArrayList();
|
||||
for (int spin = -SPIN_PERIOD + 1; spin < SPIN_PERIOD; spin++) {
|
||||
double angle = switch (axis) {
|
||||
case 0 -> rotation.getXRotation(spin);
|
||||
case 1 -> rotation.getYRotation(spin);
|
||||
default -> rotation.getZRotation(spin);
|
||||
};
|
||||
if (angles.add(Double.doubleToLongBits(angle))) {
|
||||
spins.add(spin);
|
||||
}
|
||||
}
|
||||
return spins.toIntArray();
|
||||
}
|
||||
|
||||
private static <T> List<T> listOrEmpty(KList<T> list) {
|
||||
return list == null ? List.of() : list;
|
||||
}
|
||||
|
||||
private static IrisObjectRotation rotation(IrisAxisRotationClamp x, IrisAxisRotationClamp y, IrisAxisRotationClamp z) {
|
||||
IrisObjectRotation rotation = new IrisObjectRotation();
|
||||
rotation.setEnabled(true);
|
||||
rotation.setXAxis(x);
|
||||
rotation.setYAxis(y);
|
||||
rotation.setZAxis(z);
|
||||
return rotation;
|
||||
}
|
||||
|
||||
private static int chunkRadius(int blockRadius) {
|
||||
return blockRadius > 0 ? Math.ceilDiv(blockRadius, 16) : 0;
|
||||
}
|
||||
|
||||
private static void bindPlatform(File dataFolder) {
|
||||
IrisPlatforms.unbind();
|
||||
Map<String, NativeBlockState> states = new ConcurrentHashMap<>();
|
||||
PlatformRegistries registries = mock(PlatformRegistries.class, withSettings().stubOnly());
|
||||
when(registries.block(anyString())).thenAnswer(invocation -> state(states, invocation.getArgument(0)));
|
||||
when(registries.blockOrNull(anyString())).thenAnswer(invocation -> state(states, invocation.getArgument(0)));
|
||||
when(registries.blockOrNull(anyString(), anyBoolean())).thenAnswer(invocation -> state(states, invocation.getArgument(0)));
|
||||
when(registries.air()).thenAnswer(invocation -> state(states, "minecraft:air"));
|
||||
IrisPlatform platform = mock(IrisPlatform.class, Answers.CALLS_REAL_METHODS);
|
||||
when(platform.dataFolder()).thenReturn(dataFolder);
|
||||
when(platform.dataFile(any(String[].class))).thenAnswer(invocation -> {
|
||||
Object[] path = invocation.getRawArguments();
|
||||
return new File(dataFolder, String.join(File.separator, (String[]) path[0]));
|
||||
});
|
||||
when(platform.registries()).thenReturn(registries);
|
||||
IrisPlatforms.bind(platform);
|
||||
IrisServices.register(PreservationRegistry.class, new IgnoredPreservation());
|
||||
}
|
||||
|
||||
private static NativeBlockState state(Map<String, NativeBlockState> states, String key) {
|
||||
return states.computeIfAbsent(key, ignored -> {
|
||||
String material = materialOf(key);
|
||||
boolean air = material.endsWith(":air") || material.endsWith("cave_air") || material.endsWith("void_air");
|
||||
NativeBlockState state = mock(NativeBlockState.class, withSettings().stubOnly());
|
||||
when(state.key()).thenReturn(key);
|
||||
when(state.materialKey()).thenReturn(material);
|
||||
when(state.isAir()).thenReturn(air);
|
||||
when(state.isSolid()).thenReturn(!air);
|
||||
when(state.isOccluding()).thenReturn(!air);
|
||||
return state;
|
||||
});
|
||||
}
|
||||
|
||||
private static String materialOf(String key) {
|
||||
String trimmed = key.trim().toLowerCase(Locale.ROOT);
|
||||
int properties = trimmed.indexOf('[');
|
||||
String head = properties < 0 ? trimmed : trimmed.substring(0, properties);
|
||||
return head.indexOf(':') < 0 ? "minecraft:" + head : head;
|
||||
}
|
||||
|
||||
private static final class IgnoredPreservation implements PreservationRegistry {
|
||||
@Override
|
||||
public void register(Thread thread) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(ExecutorService service) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerCache(MeteredCache cache) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dereference() {
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Statistics {
|
||||
private long checked;
|
||||
private long slack;
|
||||
private int maximumActual;
|
||||
private int maximumReach;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "checked=" + checked + " maxActual=" + maximumActual + " maxReach=" + maximumReach
|
||||
+ " meanSlack=" + (checked == 0 ? 0D : (double) slack / checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,416 @@
|
||||
package art.arcane.iris.generation.mantle;
|
||||
|
||||
import art.arcane.iris.generation.block.TileData;
|
||||
import art.arcane.iris.generation.decoration.IrisStiltSettings;
|
||||
import art.arcane.iris.generation.decoration.IrisVacuumSettings;
|
||||
import art.arcane.iris.generation.geometry.IrisBlockVector;
|
||||
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
|
||||
import art.arcane.iris.generation.noise.NoiseStyle;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
import art.arcane.iris.structure.object.FloatingObjectFootprint;
|
||||
import art.arcane.iris.structure.object.IObjectPlacer;
|
||||
import art.arcane.iris.structure.object.IrisAxisRotationClamp;
|
||||
import art.arcane.iris.structure.object.IrisObject;
|
||||
import art.arcane.iris.structure.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.structure.object.IrisObjectPlacementScaleInterpolator;
|
||||
import art.arcane.iris.structure.object.IrisObjectRotation;
|
||||
import art.arcane.iris.structure.object.IrisObjectScale;
|
||||
import art.arcane.iris.structure.object.IrisObjectTranslate;
|
||||
import art.arcane.iris.structure.object.ObjectPlaceMode;
|
||||
import art.arcane.iris.testsupport.PlatformLeakGuard;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.stream.ProceduralStream;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.withSettings;
|
||||
|
||||
public class PlacementReachTest {
|
||||
@ClassRule
|
||||
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
|
||||
|
||||
private static final int ORIGIN_X = 1000;
|
||||
private static final int ORIGIN_Y = 90;
|
||||
private static final int ORIGIN_Z = -2000;
|
||||
private static final int GROUND_Y = 64;
|
||||
|
||||
private final Map<String, NativeBlockState> states = new ConcurrentHashMap<>();
|
||||
private IrisData data;
|
||||
private IrisData objectData;
|
||||
private Engine engine;
|
||||
|
||||
@Before
|
||||
public void bindPlatform() {
|
||||
IrisPlatforms.unbind();
|
||||
PlatformRegistries registries = mock(PlatformRegistries.class, withSettings().stubOnly());
|
||||
when(registries.block(anyString())).thenAnswer(invocation -> state(invocation.getArgument(0)));
|
||||
when(registries.blockOrNull(anyString())).thenAnswer(invocation -> state(invocation.getArgument(0)));
|
||||
when(registries.blockOrNull(anyString(), anyBoolean())).thenAnswer(invocation -> state(invocation.getArgument(0)));
|
||||
when(registries.air()).thenAnswer(invocation -> state("minecraft:air"));
|
||||
IrisPlatform platform = mock(IrisPlatform.class, withSettings().stubOnly());
|
||||
when(platform.registries()).thenReturn(registries);
|
||||
IrisPlatforms.bind(platform);
|
||||
|
||||
ProceduralStream<NativeBlockState> rock = mock(ProceduralStream.class, withSettings().stubOnly());
|
||||
IrisComplex complex = mock(IrisComplex.class, withSettings().stubOnly());
|
||||
when(complex.getRockStream()).thenReturn(rock);
|
||||
engine = mock(Engine.class, withSettings().stubOnly());
|
||||
when(engine.getHeight()).thenReturn(256);
|
||||
when(engine.getMinHeight()).thenReturn(0);
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
data = mock(IrisData.class, withSettings().stubOnly());
|
||||
when(data.getEngine()).thenReturn(engine);
|
||||
objectData = mock(IrisData.class, withSettings().stubOnly());
|
||||
}
|
||||
|
||||
@After
|
||||
public void unbindPlatform() {
|
||||
IrisPlatforms.unbind();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void halfExtentReachIsTheCenteredFrameNotTheFullWidth() {
|
||||
IrisObject object = singleBlockObject(103, 75, 73);
|
||||
IrisObjectPlacement placement = new IrisObjectPlacement();
|
||||
|
||||
assertEquals(51, PlacementReach.of(object, placement, 1D).horizontal());
|
||||
assertEquals(54, PlacementReach.of(object, placement, 1D).input());
|
||||
assertEquals(51, PlacementReach.of(object, placement.setRotation(IrisObjectRotation.of(0D, 0D, 0D)), 1D).horizontal());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void arbitraryAngleRotationUsesTheRotatedFrame() {
|
||||
IrisObject object = singleBlockObject(21, 5, 21);
|
||||
IrisObjectPlacement placement = new IrisObjectPlacement().setRotation(rotation(new IrisAxisRotationClamp(),
|
||||
new IrisAxisRotationClamp(true, false, 0D, 0D, 45D), new IrisAxisRotationClamp()));
|
||||
|
||||
assertEquals(14, PlacementReach.of(object, placement, 1D).horizontal());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void slopeRotationIsBoundedEvenWhenTheConfiguredRotationIsDisabled() {
|
||||
IrisObject object = singleBlockObject(21, 9, 21);
|
||||
IrisObjectPlacement placement = new IrisObjectPlacement().setRotation(IrisObjectRotation.of(0D, 0D, 0D));
|
||||
|
||||
assertEquals(10, PlacementReach.of(object, placement, 1D).horizontal());
|
||||
assertEquals(15, PlacementReach.of(object, placement.setRotateTowardsSlope(true), 1D).horizontal());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unionKeepsTheLargestReachAndTheLargestInput() {
|
||||
PlacementReach union = new PlacementReach(10, 2).union(new PlacementReach(4, 12));
|
||||
|
||||
assertEquals(10, union.horizontal());
|
||||
assertEquals(16, union.input());
|
||||
assertEquals(19, union.withReadMargin(9).input());
|
||||
assertEquals(16, union.withReadMargin(1).input());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void scaledSpanBoundsEveryScaledVariant() {
|
||||
Random random = new Random(91L);
|
||||
double[] scales = {0.01D, 0.25D, 0.45D, 0.5D, 0.75D, 0.95D, 1D, 1.3D, 1.5D, 2D, 2.6D, 3.7D, 4D};
|
||||
for (int trial = 0; trial < 120; trial++) {
|
||||
int outside = trial % 3 == 0 ? 1 + random.nextInt(3) : 0;
|
||||
IrisObject object = randomObject(random, outside);
|
||||
PlacementReach.Extent extent = PlacementReach.Extent.of(object);
|
||||
for (double scale : scales) {
|
||||
for (IrisObjectPlacementScaleInterpolator interpolation : List.of(IrisObjectPlacementScaleInterpolator.NONE,
|
||||
IrisObjectPlacementScaleInterpolator.TRILINEAR)) {
|
||||
if (interpolation != IrisObjectPlacementScaleInterpolator.NONE && scale > 2D) {
|
||||
continue;
|
||||
}
|
||||
IrisObject scaled = object.scaled(scale, interpolation);
|
||||
for (double maximum : new double[]{scale, Math.min(IrisObjectScale.MAXIMUM_FACTOR, scale * 1.4D)}) {
|
||||
assertContains(extent.scaled(maximum), scaled, "scale " + scale + " bounded at " + maximum);
|
||||
for (double shrink : new double[]{0.45D, 0.8D}) {
|
||||
assertContains(extent.scaled(maximum).scaled(shrink),
|
||||
scaled.scaled(shrink, IrisObjectPlacementScaleInterpolator.NONE),
|
||||
"scale " + scale + " shrink " + shrink);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void runnerWritesAndReadsStayInsideTheReach() {
|
||||
Random random = new Random(1337L);
|
||||
for (int trial = 0; trial < 260; trial++) {
|
||||
IrisObject object = randomObject(random, trial % 5 == 0 ? 2 : 0);
|
||||
object.setLoader(objectData);
|
||||
IrisObjectPlacement placement = randomPlacement(random);
|
||||
PlacementReach reach = PlacementReach.of(object, placement, placement.getMaximumScale(null));
|
||||
for (int seed = 0; seed < 12; seed++) {
|
||||
RNG rng = new RNG((trial * 7919L) + seed);
|
||||
IrisObject variant = placement.getScale() == null ? object : placement.getScale().get(rng, object);
|
||||
RecordingPlacer placer = new RecordingPlacer(engine);
|
||||
variant.place(ORIGIN_X, ORIGIN_Y, ORIGIN_Z, placer, placement, rng, null, null, data);
|
||||
String context = "trial " + trial + " seed " + seed + " mode " + placement.getMode()
|
||||
+ " rotation " + placement.getRotation() + " translate " + placement.getTranslate()
|
||||
+ " scale " + placement.getScale() + " warp " + placement.getWarp().getMultiplier();
|
||||
assertTrue(context + " writes " + placer.writeReach + " > " + reach.horizontal(),
|
||||
placer.writeReach <= reach.horizontal());
|
||||
assertTrue(context + " reads " + placer.readReach + " > " + reach.input(),
|
||||
placer.readReach <= reach.input());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void anchoredReachCoversTheFloatingFootprintAnchor() {
|
||||
Random random = new Random(7L);
|
||||
for (int trial = 0; trial < 80; trial++) {
|
||||
IrisObject object = randomObject(random, trial % 4 == 0 ? 1 : 0);
|
||||
PlacementReach reach = PlacementReach.anchored(PlacementReach.Extent.of(object), new IrisObjectPlacement());
|
||||
FloatingObjectFootprint footprint = FloatingObjectFootprint.compute(object);
|
||||
for (IrisObjectRotation rotation : List.of(IrisObjectRotation.of(0D, 0D, 0D),
|
||||
IrisObjectRotation.xFlip180WithY(0D), IrisObjectRotation.xFlip180WithY(90D),
|
||||
IrisObjectRotation.xFlip180WithY(180D), IrisObjectRotation.xFlip180WithY(270D))) {
|
||||
IrisBlockVector anchor = rotation.rotate(new IrisBlockVector(footprint.getTallestKx(), 0, footprint.getTallestKz()), 0, 0, 0);
|
||||
object.getBlocks().forEach((position, state) -> {
|
||||
IrisBlockVector rotated = rotation.rotate(position, 0, 0, 0);
|
||||
assertTrue(Math.abs((int) Math.round(rotated.getX()) - anchor.getBlockX()) <= reach.horizontal());
|
||||
assertTrue(Math.abs((int) Math.round(rotated.getZ()) - anchor.getBlockZ()) <= reach.horizontal());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IrisObjectPlacement randomPlacement(Random random) {
|
||||
IrisObjectPlacement placement = new IrisObjectPlacement();
|
||||
placement.setRotation(randomRotation(random));
|
||||
if (random.nextInt(3) > 0) {
|
||||
placement.setTranslate(new IrisObjectTranslate()
|
||||
.setX(random.nextInt(41) - 20)
|
||||
.setY(random.nextInt(21) - 10)
|
||||
.setZ(random.nextInt(41) - 20));
|
||||
}
|
||||
if (random.nextInt(3) == 0) {
|
||||
placement.setWarp(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).setMultiplier(1D + random.nextInt(9)));
|
||||
}
|
||||
List<ObjectPlaceMode> modes = List.of(ObjectPlaceMode.CENTER_HEIGHT, ObjectPlaceMode.STILT, ObjectPlaceMode.FAST_STILT,
|
||||
ObjectPlaceMode.CENTER_STILT, ObjectPlaceMode.ERODE_STILT, ObjectPlaceMode.ORGANIC_STILT, ObjectPlaceMode.CEILING_HANG,
|
||||
ObjectPlaceMode.VACUUM, ObjectPlaceMode.VACUUM_HIGH, ObjectPlaceMode.VACUUM_FAST, ObjectPlaceMode.VACUUM_ORGANIC,
|
||||
ObjectPlaceMode.VACUUM_WAVY, ObjectPlaceMode.STRUCTURE_PIECE);
|
||||
placement.setMode(modes.get(random.nextInt(modes.size())));
|
||||
if (random.nextInt(4) == 0) {
|
||||
placement.setVacuumSettings(new IrisVacuumSettings().setRadius(1 + random.nextInt(16)));
|
||||
}
|
||||
if (random.nextInt(4) == 0) {
|
||||
placement.setStiltSettings(new IrisStiltSettings());
|
||||
}
|
||||
placement.setBore(random.nextInt(4) == 0);
|
||||
placement.setBoreExtendMaxY(random.nextInt(3));
|
||||
placement.setSnow(random.nextInt(4) == 0 ? 0.6D : 0D);
|
||||
placement.setSurfaceSupportBuffer(random.nextInt(5));
|
||||
switch (random.nextInt(5)) {
|
||||
case 0 -> placement.setScale(new IrisObjectScale().setSize(0.3D + (random.nextDouble() * 1.6D)));
|
||||
case 1 -> placement.setScale(new IrisObjectScale().setMinimumScale(0.5D).setMaximumScale(2.3D)
|
||||
.setVariations(5).setInterpolation(IrisObjectPlacementScaleInterpolator.TRILINEAR));
|
||||
case 2 -> placement.setScale(new IrisObjectScale().setMinimumScale(0.4D).setMaximumScale(0.9D).setVariations(3));
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
return placement;
|
||||
}
|
||||
|
||||
private static IrisObjectRotation randomRotation(Random random) {
|
||||
return switch (random.nextInt(9)) {
|
||||
case 0 -> new IrisObjectRotation();
|
||||
case 1 -> IrisObjectRotation.of(0D, 0D, 0D);
|
||||
case 2 -> rotation(new IrisAxisRotationClamp(), new IrisAxisRotationClamp(true, false, 0D, 0D, 45D), new IrisAxisRotationClamp());
|
||||
case 3 -> rotation(new IrisAxisRotationClamp(), new IrisAxisRotationClamp(true, false, 0D, 0D, 0D), new IrisAxisRotationClamp());
|
||||
case 4 -> IrisObjectRotation.of(90D, 0D, 0D);
|
||||
case 5 -> rotation(new IrisAxisRotationClamp(true, false, 0D, 0D, 90D),
|
||||
new IrisAxisRotationClamp(true, false, 0D, 0D, 90D), new IrisAxisRotationClamp(true, false, 0D, 0D, 90D));
|
||||
case 6 -> rotation(new IrisAxisRotationClamp(true, false, 0D, 0D, 0D),
|
||||
new IrisAxisRotationClamp(true, false, 0D, 0D, 0D), new IrisAxisRotationClamp(true, false, 0D, 0D, 30D));
|
||||
case 7 -> IrisObjectRotation.of(30D, 135D, 0D);
|
||||
default -> rotation(new IrisAxisRotationClamp(), new IrisAxisRotationClamp(true, false, 20D, 110D, 15D),
|
||||
new IrisAxisRotationClamp(true, true, 180D, 180D, 0D));
|
||||
};
|
||||
}
|
||||
|
||||
private IrisObject randomObject(Random random, int outside) {
|
||||
int width = 1 + random.nextInt(18);
|
||||
int height = 1 + random.nextInt(14);
|
||||
int depth = 1 + random.nextInt(18);
|
||||
IrisObject object = new IrisObject(width, height, depth);
|
||||
int count = 1 + random.nextInt(Math.max(1, (width * height * depth) / 3));
|
||||
for (int index = 0; index < count; index++) {
|
||||
int x = random.nextInt(width) - (width / 2);
|
||||
int y = random.nextInt(height) - (height / 2);
|
||||
int z = random.nextInt(depth) - (depth / 2);
|
||||
object.getBlocks().put(new IrisBlockVector(x, y, z), state(random.nextInt(5) == 0 ? "minecraft:oak_leaves" : "minecraft:stone"));
|
||||
}
|
||||
for (int index = 0; index < outside; index++) {
|
||||
int x = random.nextInt(width + 10) - ((width + 10) / 2);
|
||||
int y = random.nextInt(height + 6) - ((height + 6) / 2);
|
||||
int z = random.nextInt(depth + 10) - ((depth + 10) / 2);
|
||||
object.getBlocks().put(new IrisBlockVector(x, y, z), state("minecraft:stone"));
|
||||
}
|
||||
return object;
|
||||
}
|
||||
|
||||
private IrisObject singleBlockObject(int width, int height, int depth) {
|
||||
IrisObject object = new IrisObject(width, height, depth);
|
||||
object.getBlocks().put(new IrisBlockVector(0, 0, 0), state("minecraft:stone"));
|
||||
return object;
|
||||
}
|
||||
|
||||
private static void assertContains(PlacementReach.Extent extent, IrisObject object, String context) {
|
||||
object.getBlocks().forEach((position, state) -> {
|
||||
assertTrue(context + " x " + position.getBlockX() + " outside " + extent.x(),
|
||||
position.getBlockX() >= extent.x().minimum() && position.getBlockX() <= extent.x().maximum());
|
||||
assertTrue(context + " y " + position.getBlockY() + " outside " + extent.y(),
|
||||
position.getBlockY() >= extent.y().minimum() && position.getBlockY() <= extent.y().maximum());
|
||||
assertTrue(context + " z " + position.getBlockZ() + " outside " + extent.z(),
|
||||
position.getBlockZ() >= extent.z().minimum() && position.getBlockZ() <= extent.z().maximum());
|
||||
});
|
||||
int centerX = object.getCenter().getBlockX();
|
||||
int centerZ = object.getCenter().getBlockZ();
|
||||
assertTrue(context + " frame x", -centerX >= extent.x().minimum() && object.getW() - 1 - centerX <= extent.x().maximum());
|
||||
assertTrue(context + " frame z", -centerZ >= extent.z().minimum() && object.getD() - 1 - centerZ <= extent.z().maximum());
|
||||
}
|
||||
|
||||
private static IrisObjectRotation rotation(IrisAxisRotationClamp x, IrisAxisRotationClamp y, IrisAxisRotationClamp z) {
|
||||
IrisObjectRotation rotation = new IrisObjectRotation();
|
||||
rotation.setEnabled(true);
|
||||
rotation.setXAxis(x);
|
||||
rotation.setYAxis(y);
|
||||
rotation.setZAxis(z);
|
||||
return rotation;
|
||||
}
|
||||
|
||||
private NativeBlockState state(String key) {
|
||||
return states.computeIfAbsent(key, ignored -> {
|
||||
String material = key.toLowerCase(Locale.ROOT);
|
||||
boolean air = material.endsWith("air");
|
||||
NativeBlockState state = mock(NativeBlockState.class, withSettings().stubOnly());
|
||||
when(state.key()).thenReturn(key);
|
||||
when(state.materialKey()).thenReturn(material);
|
||||
when(state.isAir()).thenReturn(air);
|
||||
when(state.isSolid()).thenReturn(!air);
|
||||
when(state.isOccluding()).thenReturn(!air);
|
||||
return state;
|
||||
});
|
||||
}
|
||||
|
||||
private static final class RecordingPlacer implements IObjectPlacer {
|
||||
private final Engine engine;
|
||||
private int writeReach;
|
||||
private int readReach;
|
||||
|
||||
private RecordingPlacer(Engine engine) {
|
||||
this.engine = engine;
|
||||
}
|
||||
|
||||
private void write(int x, int z) {
|
||||
writeReach = Math.max(writeReach, Math.max(Math.abs(x - ORIGIN_X), Math.abs(z - ORIGIN_Z)));
|
||||
read(x, z);
|
||||
}
|
||||
|
||||
private void read(int x, int z) {
|
||||
readReach = Math.max(readReach, Math.max(Math.abs(x - ORIGIN_X), Math.abs(z - ORIGIN_Z)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHighest(int x, int z, IrisData data) {
|
||||
read(x, z);
|
||||
return GROUND_Y;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) {
|
||||
read(x, z);
|
||||
return GROUND_Y;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void set(int x, int y, int z, NativeBlockState d) {
|
||||
write(x, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativeBlockState get(int x, int y, int z) {
|
||||
read(x, z);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPreventingDecay() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCarved(int x, int y, int z) {
|
||||
read(x, z);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSolid(int x, int y, int z) {
|
||||
read(x, z);
|
||||
return y < GROUND_Y;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUnderwater(int x, int z) {
|
||||
read(x, z);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFluidHeight() {
|
||||
return GROUND_Y - 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDebugSmartBore() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTile(int xx, int yy, int zz, TileData tile) {
|
||||
write(xx, zz);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> void setData(int xx, int yy, int zz, T data) {
|
||||
write(xx, zz);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T getData(int xx, int yy, int zz, Class<T> t) {
|
||||
read(xx, zz);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Engine getEngine() {
|
||||
return engine;
|
||||
}
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package art.arcane.iris.generation.runtime;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public final class EngineMetricsStageTotalsTest {
|
||||
@Test
|
||||
public void stageTotalsAccumulatePastTheRollingWindow() {
|
||||
EngineMetrics metrics = new EngineMetrics(2);
|
||||
metrics.getTerrain().put(1.5D);
|
||||
metrics.getTerrain().put(2.5D);
|
||||
metrics.getTerrain().put(4D);
|
||||
|
||||
EngineMetrics.StageTotal terrain = metrics.stageTotals().get("terrain");
|
||||
|
||||
assertEquals(8D, terrain.millis(), 0D);
|
||||
assertEquals(3L, terrain.samples());
|
||||
assertEquals(3.25D, metrics.getTerrain().getAverage(), 0D);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stageTotalsSinceAnEarlierSnapshotCoverOnlyTheLaterPuts() {
|
||||
EngineMetrics metrics = new EngineMetrics(4);
|
||||
metrics.getCarveResolve().put(3D);
|
||||
EngineMetrics.StageTotal before = metrics.stageTotals().get("carve.resolve");
|
||||
metrics.getCarveResolve().put(5D);
|
||||
metrics.getCarveResolve().put(7D);
|
||||
|
||||
EngineMetrics.StageTotal window = metrics.stageTotals().get("carve.resolve").since(before);
|
||||
|
||||
assertEquals(12D, window.millis(), 0D);
|
||||
assertEquals(2L, window.samples());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stageTotalsNameEveryPulledTimer() {
|
||||
EngineMetrics metrics = new EngineMetrics(1);
|
||||
Map<String, EngineMetrics.StageTotal> totals = metrics.stageTotals();
|
||||
|
||||
assertEquals(Set.copyOf(metrics.pull().keySet()), Set.copyOf(totals.keySet()));
|
||||
assertEquals(metrics.pull().size(), totals.size());
|
||||
}
|
||||
}
|
||||
+15
-43
@@ -8,7 +8,6 @@ import art.arcane.iris.generation.runtime.GenerationRuntime.BiomeMaxes;
|
||||
import art.arcane.iris.world.history.SavedBiomeRuntime;
|
||||
import art.arcane.iris.world.history.SavedBiomeUnavailableException;
|
||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||
import art.arcane.iris.testsupport.Await;
|
||||
import art.arcane.iris.testsupport.PlatformBinding;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -37,13 +36,11 @@ import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.FutureTask;
|
||||
@@ -680,30 +677,19 @@ public class IrisEngineGenerationRuntimeScopeTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missingRouterReadWaitsForAttachmentPublication() throws Exception {
|
||||
public void missingRouterReadsDoNotWaitForTheAttachmentMonitor() throws Exception {
|
||||
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
|
||||
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
|
||||
GenerationHistoryRuntimeRouter router = historyRouter(engine);
|
||||
GenerationHistoryRuntimeRouter.CoordinateScope scope = mock(GenerationHistoryRuntimeRouter.CoordinateScope.class);
|
||||
when(router.openCoordinateScope(-17, 32)).thenReturn(scope);
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
FutureTask<GenerationHistoryRuntimeRouter.CoordinateScope> read = new FutureTask<>(() -> {
|
||||
started.countDown();
|
||||
return engine.openGenerationHistoryCoordinateScope(-17, 32);
|
||||
});
|
||||
Thread reader = new Thread(read, "Pending history attachment");
|
||||
|
||||
ExecutorService reader = Executors.newSingleThreadExecutor();
|
||||
try {
|
||||
synchronized (engine.generationHistoryRuntimeRouterLock) {
|
||||
reader.start();
|
||||
assertTrue(started.await(5, TimeUnit.SECONDS));
|
||||
awaitBlocked(reader);
|
||||
engine.attachGenerationHistoryRuntimeRouter(router);
|
||||
assertNull(reader.submit(() -> engine.openGenerationHistoryCoordinateScope(-17, 32))
|
||||
.get(5, TimeUnit.SECONDS));
|
||||
assertTrue(reader.submit(engine::getGenerationHistoryRuntimeRouter).get(5, TimeUnit.SECONDS).isEmpty());
|
||||
}
|
||||
assertSame(scope, read.get(5, TimeUnit.SECONDS));
|
||||
} finally {
|
||||
reader.join(5_000L);
|
||||
assertTrue("Pending router read did not finish", !reader.isAlive());
|
||||
reader.shutdownNow();
|
||||
assertTrue(reader.awaitTermination(5, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -713,27 +699,19 @@ public class IrisEngineGenerationRuntimeScopeTest {
|
||||
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
|
||||
GenerationHistoryRuntimeRouter router = historyRouter(engine);
|
||||
engine.attachGenerationHistoryRuntimeRouter(router);
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
FutureTask<GenerationHistoryRuntimeRouter.CoordinateScope> read = new FutureTask<>(() -> {
|
||||
started.countDown();
|
||||
return engine.openGenerationHistoryCoordinateScope(-17, 32);
|
||||
});
|
||||
Thread reader = new Thread(read, "Detached history read");
|
||||
|
||||
engine.detachGenerationHistoryRuntimeRouter(router);
|
||||
ExecutorService reader = Executors.newSingleThreadExecutor();
|
||||
try {
|
||||
synchronized (engine.generationHistoryRuntimeRouterLock) {
|
||||
engine.detachGenerationHistoryRuntimeRouter(router);
|
||||
reader.start();
|
||||
assertTrue(started.await(5, TimeUnit.SECONDS));
|
||||
awaitBlocked(reader);
|
||||
ExecutionException failure = assertThrows(ExecutionException.class, () -> reader.submit(
|
||||
() -> engine.openGenerationHistoryCoordinateScope(-17, 32)).get(5, TimeUnit.SECONDS));
|
||||
assertTrue(failure.getCause() instanceof IllegalStateException);
|
||||
assertEquals("Iris generation-history runtime router is detached.", failure.getCause().getMessage());
|
||||
}
|
||||
ExecutionException failure = assertThrows(ExecutionException.class, () -> read.get(5, TimeUnit.SECONDS));
|
||||
assertTrue(failure.getCause() instanceof IllegalStateException);
|
||||
assertEquals("Iris generation-history runtime router is detached.", failure.getCause().getMessage());
|
||||
assertTrue(engine.getGenerationHistoryRuntimeRouter().isEmpty());
|
||||
} finally {
|
||||
reader.join(5_000L);
|
||||
assertTrue("Detached router read did not finish", !reader.isAlive());
|
||||
reader.shutdownNow();
|
||||
assertTrue(reader.awaitTermination(5, TimeUnit.SECONDS));
|
||||
}
|
||||
verify(router, never()).openCoordinateScope(anyInt(), anyInt());
|
||||
}
|
||||
@@ -909,12 +887,6 @@ public class IrisEngineGenerationRuntimeScopeTest {
|
||||
return router;
|
||||
}
|
||||
|
||||
private static void awaitBlocked(Thread thread) {
|
||||
Await.reached("the reader to block on the engine lifecycle lock", Duration.ofSeconds(5L),
|
||||
() -> thread.getState() == Thread.State.BLOCKED || !thread.isAlive());
|
||||
assertEquals(Thread.State.BLOCKED, thread.getState());
|
||||
}
|
||||
|
||||
private static IrisEngine engine(
|
||||
GenerationRuntime generationRuntime,
|
||||
EngineEffects effects,
|
||||
|
||||
+3
@@ -132,6 +132,9 @@ public class IrisCarveModifierBiomeBlendTest {
|
||||
Field field = IrisCarveModifier.class.getDeclaredField("rng");
|
||||
field.setAccessible(true);
|
||||
field.set(modifier, rng);
|
||||
Field seed = IrisCarveModifier.class.getDeclaredField("caveBlendSeed");
|
||||
seed.setAccessible(true);
|
||||
seed.setLong(modifier, SEED);
|
||||
return modifier;
|
||||
}
|
||||
|
||||
|
||||
+3
@@ -56,6 +56,9 @@ public class IrisCarveModifierBoundarySupportTest {
|
||||
Field rng = IrisCarveModifier.class.getDeclaredField("rng");
|
||||
rng.setAccessible(true);
|
||||
rng.set(modifier, new RNG(71L));
|
||||
Field seed = IrisCarveModifier.class.getDeclaredField("caveBlendSeed");
|
||||
seed.setAccessible(true);
|
||||
seed.setLong(modifier, 71L);
|
||||
|
||||
IrisMatterSupport.ensureRegistered();
|
||||
Matter floorMatter = new IrisMatter(16, 16, 16);
|
||||
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package art.arcane.iris.generation.stage;
|
||||
|
||||
import art.arcane.volmlib.util.math.BlockPosition;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.SplittableRandom;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class IrisCarveModifierCaveBlendRollTest {
|
||||
private static final int SAMPLES = 1_000_000;
|
||||
|
||||
@Test
|
||||
public void statelessBlendRandomMatchesParallelRngForOneMillionPoints() {
|
||||
SplittableRandom random = new SplittableRandom(0x5eed_cafe_f00dL);
|
||||
long[] seeds = {0L, 1L, -1L, 1337L, Long.MIN_VALUE, Long.MAX_VALUE, 0x5DEECE66DL, random.nextLong()};
|
||||
for (int sample = 0; sample < SAMPLES; sample++) {
|
||||
long seed = sample < seeds.length * 64 ? seeds[sample % seeds.length] : random.nextLong();
|
||||
int x = coordinate(random, sample);
|
||||
int y = sample % 7 == 0 ? random.nextInt() : random.nextInt(-2048, 2048);
|
||||
int z = coordinate(random, sample + 3);
|
||||
assertEquals(seed + " @ " + x + "," + y + "," + z,
|
||||
new RNG(seed).nextParallelRNG(BlockPosition.toLong(x, y, z)).nextInt(),
|
||||
IrisCarveModifier.caveBlendRandom(seed, x, y, z));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void statelessBlendRandomMatchesParallelRngAtCoordinateExtremes() {
|
||||
int[] edges = {Integer.MIN_VALUE, Integer.MIN_VALUE + 1, -33_554_433, -33_554_432, -1, 0, 1,
|
||||
33_554_431, 33_554_432, Integer.MAX_VALUE - 1, Integer.MAX_VALUE};
|
||||
long[] seeds = {0L, -1L, 1337L, Long.MIN_VALUE, Long.MAX_VALUE};
|
||||
for (long seed : seeds) {
|
||||
for (int x : edges) {
|
||||
for (int y : edges) {
|
||||
for (int z : edges) {
|
||||
assertEquals(new RNG(seed).nextParallelRNG(BlockPosition.toLong(x, y, z)).nextInt(),
|
||||
IrisCarveModifier.caveBlendRandom(seed, x, y, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int coordinate(SplittableRandom random, int sample) {
|
||||
return switch (sample % 4) {
|
||||
case 0 -> random.nextInt();
|
||||
case 1 -> random.nextInt(-30_000_000, 30_000_000);
|
||||
default -> random.nextInt(-65_536, 65_536);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package art.arcane.iris.generation.stage;
|
||||
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.volmlib.util.matter.MatterCavern;
|
||||
import org.junit.Test;
|
||||
|
||||
@@ -49,6 +50,7 @@ public class IrisCarveScratchTest {
|
||||
scratch.boundaryMasks[0].add(13);
|
||||
scratch.walls.put(1, 12, 2, cavern);
|
||||
scratch.customBiomeCache.put("cave", null);
|
||||
scratch.caveBiomeCache.put(42L, new IrisBiome());
|
||||
scratch.customCaveBiomePresent = true;
|
||||
|
||||
scratch.reset();
|
||||
@@ -56,6 +58,7 @@ public class IrisCarveScratchTest {
|
||||
assertTrue(scratch.columnMasks[0].isEmpty());
|
||||
assertTrue(scratch.boundaryMasks[0].isEmpty());
|
||||
assertTrue(scratch.customBiomeCache.isEmpty());
|
||||
assertTrue(scratch.caveBiomeCache.isEmpty());
|
||||
assertFalse(scratch.customCaveBiomePresent);
|
||||
int[] wallCount = new int[1];
|
||||
scratch.walls.forEach((x, y, z, value) -> wallCount[0]++);
|
||||
|
||||
@@ -19,11 +19,10 @@ import art.arcane.iris.generation.decoration.IrisDecorator;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.hunk.Hunk;
|
||||
import art.arcane.iris.generation.block.B;
|
||||
import art.arcane.iris.generation.chunk.ChunkDataHunkHolder;
|
||||
import art.arcane.iris.generation.chunk.SectionBuffer;
|
||||
import art.arcane.iris.generation.chunk.ColumnExtentListeningHunk;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import org.bukkit.generator.ChunkGenerator;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
@@ -165,10 +164,7 @@ public class IrisNaturalCaveScanTest {
|
||||
fullOutput.listen((x, y, z, state) -> fullWrites.add(x + ":" + y + ":" + z + ":" + state.key())), null);
|
||||
|
||||
Fixture bounded = new Fixture(true);
|
||||
ChunkGenerator.ChunkData chunkData = mock(ChunkGenerator.ChunkData.class);
|
||||
doReturn(0).when(chunkData).getMinHeight();
|
||||
doReturn(32).when(chunkData).getMaxHeight();
|
||||
ChunkDataHunkHolder holder = new ChunkDataHunkHolder(chunkData);
|
||||
SectionBuffer holder = new SectionBuffer(32);
|
||||
bounded.sparseOutput(holder);
|
||||
List<String> boundedWrites = new ArrayList<>();
|
||||
bounded.modifier.decorateNaturalCaves(-32, 48, new ColumnExtentListeningHunk<>(holder,
|
||||
|
||||
+3
-11
@@ -1,7 +1,7 @@
|
||||
package art.arcane.iris.generation.stage;
|
||||
|
||||
import art.arcane.iris.generation.block.B;
|
||||
import art.arcane.iris.generation.chunk.ChunkDataHunkHolder;
|
||||
import art.arcane.iris.generation.chunk.SectionBuffer;
|
||||
import art.arcane.iris.generation.chunk.ColumnExtentListeningHunk;
|
||||
import art.arcane.iris.generation.concurrent.BurstExecutor;
|
||||
import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||
@@ -15,7 +15,6 @@ import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.hunk.Hunk;
|
||||
import art.arcane.volmlib.util.hunk.storage.ArrayHunk;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.generator.ChunkGenerator;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
||||
@@ -65,8 +64,8 @@ public class IrisPerfectionModifierColumnTest {
|
||||
NativeBlockState[] palette = {null, fixture.air, fixture.water, fixture.stone,
|
||||
fixture.decorant, fixture.half, fixture.spike(true, false), fixture.spike(false, true)};
|
||||
for (int trial = 0; trial < 8; trial++) {
|
||||
ChunkDataHunkHolder bounded = holder(64);
|
||||
ChunkDataHunkHolder full = holder(64);
|
||||
SectionBuffer bounded = new SectionBuffer(64);
|
||||
SectionBuffer full = new SectionBuffer(64);
|
||||
for (int x = 0; x < 16; x++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
int top = random.nextInt(64);
|
||||
@@ -91,13 +90,6 @@ public class IrisPerfectionModifierColumnTest {
|
||||
}
|
||||
}
|
||||
|
||||
private static ChunkDataHunkHolder holder(int height) {
|
||||
ChunkGenerator.ChunkData chunk = mock(ChunkGenerator.ChunkData.class);
|
||||
when(chunk.getMinHeight()).thenReturn(0);
|
||||
when(chunk.getMaxHeight()).thenReturn(height);
|
||||
return new ChunkDataHunkHolder(chunk);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unrelatedTallColumnsAreScannedOnceDuringRepeatedCleanup() {
|
||||
Fixture fixture = new Fixture();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package art.arcane.iris.generation.subterrain;
|
||||
|
||||
import art.arcane.iris.pack.value.IrisRange;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonParseException;
|
||||
import org.junit.Test;
|
||||
@@ -10,6 +11,11 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.HashSet;
|
||||
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.assertFalse;
|
||||
@@ -441,6 +447,74 @@ public class SubterrainPlannerTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void alternatingChunkSamplingResolvesEachCandidateCellOnce() {
|
||||
IrisSubterrainFeature feature = feature(IrisSubterrainFamily.CENOTE).setAllowedRegions(new KList<>("test-region"));
|
||||
AtomicInteger regionQueries = new AtomicInteger();
|
||||
SubterrainPlanner planner = new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 7123, -64, 97,
|
||||
(x, z) -> {
|
||||
regionQueries.incrementAndGet();
|
||||
return "test-region";
|
||||
}));
|
||||
for (int round = 0; round < 500; round++) {
|
||||
planner.sample(7, 10, 7);
|
||||
planner.sample(23, 10, 7);
|
||||
planner.sample(7, 10, 23);
|
||||
planner.sample(-9, 10, -9);
|
||||
}
|
||||
int firstRound = regionQueries.get();
|
||||
assertTrue("candidate cells resolved " + firstRound + " times", firstRound <= 16);
|
||||
for (int round = 0; round < 500; round++) {
|
||||
planner.plansForBounds(-1, -1, 16, 16);
|
||||
planner.plansForBounds(15, 15, 32, 32);
|
||||
}
|
||||
assertEquals(firstRound, regionQueries.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void concurrentCrossChunkSamplingMatchesAFreshPlanner() throws Exception {
|
||||
List<IrisSubterrainFeature> features = List.of(compactFeature(IrisSubterrainFamily.CENOTE).setSpacing(96),
|
||||
compactFeature(IrisSubterrainFamily.TRAVERTINE_TERRACES).setSpacing(160));
|
||||
SubterrainPlanner shared = new SubterrainPlanner(new SubterrainPlanner.Options(features, 4242, -64, 97, (x, z) -> "test-region"));
|
||||
SubterrainPlanner reference = new SubterrainPlanner(new SubterrainPlanner.Options(features, 4242, -64, 97, (x, z) -> "test-region"));
|
||||
int threads = 8;
|
||||
ExecutorService pool = Executors.newFixedThreadPool(threads);
|
||||
try {
|
||||
List<Future<Integer>> results = new ArrayList<>();
|
||||
for (int thread = 0; thread < threads; thread++) {
|
||||
int offset = thread;
|
||||
results.add(pool.submit(() -> {
|
||||
int occupied = 0;
|
||||
for (int step = 0; step < 6000; step++) {
|
||||
int x = Math.floorMod(step * 37 + offset * 101, 2400) - 1200;
|
||||
int z = Math.floorMod(step * 53 + offset * 211, 2400) - 1200;
|
||||
int y = Math.floorMod(step * 11 + offset, 150) - 60;
|
||||
int chunkX = Math.floorDiv(x, 16) * 16;
|
||||
int chunkZ = Math.floorDiv(z, 16) * 16;
|
||||
SubterrainCell expected;
|
||||
synchronized (reference) {
|
||||
expected = reference.sample(reference.plansForBounds(chunkX - 1, chunkZ - 1, chunkX + 16, chunkZ + 16), x, y, z);
|
||||
}
|
||||
SubterrainCell actual = shared.sample(x, y, z);
|
||||
assertEquals(expected, actual);
|
||||
if (actual.occupied()) {
|
||||
occupied++;
|
||||
}
|
||||
}
|
||||
return occupied;
|
||||
}));
|
||||
}
|
||||
int occupied = 0;
|
||||
for (Future<Integer> result : results) {
|
||||
occupied += result.get(60, TimeUnit.SECONDS);
|
||||
}
|
||||
assertTrue(occupied > 0);
|
||||
} finally {
|
||||
pool.shutdownNow();
|
||||
assertTrue(pool.awaitTermination(10, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void immutableSnapshotsAndSeedsControlPlans() {
|
||||
IrisSubterrainFeature feature = feature(IrisSubterrainFamily.CENOTE);
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
package art.arcane.iris.pack.loading;
|
||||
|
||||
import art.arcane.iris.generation.biome.IrisBiomeCustom;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.testsupport.DurabilityMode;
|
||||
import art.arcane.iris.world.history.GenerationRegistryContractFactory;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
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.assertNotEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class IrisDataCustomBiomeKeyTest {
|
||||
@ClassRule
|
||||
public static final DurabilityMode DURABILITY = DurabilityMode.relaxed();
|
||||
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
public void repeatedConcurrentLookupsMatchTheContentAddressedKeyPerDimension() throws Exception {
|
||||
Path pack = temporaryFolder.newFolder("custom-keys").toPath().resolve("iris/pack");
|
||||
writePack(pack);
|
||||
IrisData data = IrisData.openDatapackCompiler(pack.toFile());
|
||||
ExecutorService pool = Executors.newFixedThreadPool(8);
|
||||
try {
|
||||
IrisDimension overworld = data.getDimensionLoader().load("overworld");
|
||||
IrisDimension second = data.getDimensionLoader().load("second");
|
||||
List<IrisBiomeCustom> customs = data.getBiomeLoader().load("test").getCustomDerivitives();
|
||||
assertEquals(2, customs.size());
|
||||
List<String> expected = new ArrayList<>();
|
||||
for (IrisDimension dimension : List.of(overworld, second)) {
|
||||
for (IrisBiomeCustom custom : customs) {
|
||||
expected.add(GenerationRegistryContractFactory.customBiomeResourceKey(
|
||||
dimension.getLoadKey(), custom, data.getContentGate()));
|
||||
}
|
||||
}
|
||||
assertEquals(4, expected.stream().distinct().count());
|
||||
List<Future<List<String>>> results = new ArrayList<>();
|
||||
for (int thread = 0; thread < 8; thread++) {
|
||||
results.add(pool.submit(() -> {
|
||||
List<String> seen = new ArrayList<>();
|
||||
for (int round = 0; round < 200; round++) {
|
||||
seen.clear();
|
||||
for (IrisDimension dimension : List.of(overworld, second)) {
|
||||
for (IrisBiomeCustom custom : customs) {
|
||||
seen.add(data.customBiomeResourceKey(dimension, custom));
|
||||
}
|
||||
}
|
||||
}
|
||||
return List.copyOf(seen);
|
||||
}));
|
||||
}
|
||||
for (Future<List<String>> result : results) {
|
||||
assertEquals(expected, result.get(60, TimeUnit.SECONDS));
|
||||
}
|
||||
IrisBiomeCustom equalCopy = new IrisBiomeCustom().setId(customs.getFirst().getId());
|
||||
assertEquals(GenerationRegistryContractFactory.customBiomeResourceKey(
|
||||
overworld.getLoadKey(), equalCopy, data.getContentGate()),
|
||||
data.customBiomeResourceKey(overworld, equalCopy));
|
||||
assertNotEquals(data.customBiomeResourceKey(overworld, customs.getFirst()),
|
||||
data.customBiomeResourceKey(second, customs.getFirst()));
|
||||
} finally {
|
||||
pool.shutdownNow();
|
||||
assertTrue(pool.awaitTermination(10, TimeUnit.SECONDS));
|
||||
data.close();
|
||||
}
|
||||
}
|
||||
|
||||
private static void writePack(Path root) throws Exception {
|
||||
Files.createDirectories(root.resolve("dimensions"));
|
||||
Files.createDirectories(root.resolve("biomes"));
|
||||
for (String dimension : List.of("overworld", "second")) {
|
||||
Files.writeString(root.resolve("dimensions/" + dimension + ".json"), """
|
||||
{
|
||||
"name": "Test Dimension",
|
||||
"environment": "NORMAL",
|
||||
"logicalHeight": 256,
|
||||
"dimensionHeight": {
|
||||
"min": -64,
|
||||
"max": 320
|
||||
}
|
||||
}
|
||||
""", StandardCharsets.UTF_8);
|
||||
}
|
||||
Files.writeString(root.resolve("biomes/test.json"), """
|
||||
{
|
||||
"name": "Test Biome",
|
||||
"derivative": "minecraft:plains",
|
||||
"vanillaDerivative": "minecraft:plains",
|
||||
"customDerivitives": [
|
||||
{
|
||||
"id": "first_custom"
|
||||
},
|
||||
{
|
||||
"id": "second_custom",
|
||||
"temperature": 1.5
|
||||
}
|
||||
]
|
||||
}
|
||||
""", StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
@@ -99,7 +99,7 @@ public class PregenPerformanceProfileTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pregenerationWantsTwoBurstWorkersPerCore() {
|
||||
public void pregenerationWantsTwoBurstWorkersPerPerformanceCore() {
|
||||
assertEquals(32, PregenPerformanceProfile.pregenBurstParallelism(16));
|
||||
assertEquals(4, PregenPerformanceProfile.pregenBurstParallelism(1));
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user