mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 22:10:18 +00:00
Merged wave1 into parallel waits
- Terrain bake and object bake from turbo/wave1
This commit is contained in:
commit
cb008b6428
46 files changed
+4854
-913
No files matched your search
@@ -0,0 +1,81 @@
|
||||
package art.arcane.iris.generation.mantle;
|
||||
|
||||
import art.arcane.iris.testsupport.KeyedBlockState;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class MutationLogTest {
|
||||
@Test
|
||||
public void logKeepsPositionsValuesAndFlagsInOrder() {
|
||||
NativeBlockState state = new KeyedBlockState("minecraft:stone");
|
||||
MutationLog log = MutationLog.of(List.of(
|
||||
new ObjectDestinationTransaction.SetMutation(new ObjectDestinationTransaction.DataKey(3, 70, -2, String.class), "marker", true),
|
||||
new ObjectDestinationTransaction.CustomBlockMutation(new ObjectDestinationTransaction.DataKey(-17, 4, 40, NativeBlockState.class), state, false)));
|
||||
for (int index = 0; index < 100; index++) {
|
||||
log.add(index, index + 1, -index, "grow" + index, (index & 1) == 0, (index & 2) == 0);
|
||||
}
|
||||
|
||||
assertEquals(102, log.size());
|
||||
assertEquals(3, log.x(0));
|
||||
assertEquals(70, log.y(0));
|
||||
assertEquals(-2, log.z(0));
|
||||
assertEquals("marker", log.value(0));
|
||||
assertFalse(log.custom(0));
|
||||
assertTrue(log.canopy(0));
|
||||
assertSame(String.class, log.type(0));
|
||||
assertSame(state, log.value(1));
|
||||
assertTrue(log.custom(1));
|
||||
assertFalse(log.canopy(1));
|
||||
assertSame(NativeBlockState.class, log.type(1));
|
||||
assertEquals(99, log.x(101));
|
||||
assertEquals("grow99", log.value(101));
|
||||
assertFalse(log.custom(101));
|
||||
assertTrue(log.canopy(98));
|
||||
assertFalse(log.canopy(100));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mismatchedMutationTypesAreRejected() {
|
||||
assertThrows(IllegalArgumentException.class, () -> MutationLog.of(List.of(
|
||||
new ObjectDestinationTransaction.SetMutation(new ObjectDestinationTransaction.DataKey(0, 4, 0, Integer.class), "text", false))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void planSlicesMatchThePlanBuiltFromTheSameMutations() {
|
||||
MutationLog log = new MutationLog();
|
||||
List<ObjectDestinationTransaction.Mutation> tail = new ArrayList<>();
|
||||
for (int index = 0; index < 64; index++) {
|
||||
int x = (index * 7) - 30;
|
||||
int y = 4 + index;
|
||||
int z = (index * 5) - 20;
|
||||
String value = "value" + (index % 9);
|
||||
boolean canopy = index % 4 == 0;
|
||||
log.add(x, y, z, value, false, canopy);
|
||||
if (index >= 10) {
|
||||
tail.add(new ObjectDestinationTransaction.SetMutation(new ObjectDestinationTransaction.DataKey(x, y, z, String.class), value, canopy));
|
||||
}
|
||||
}
|
||||
ObjectSourcePlan sliced = new ObjectSourcePlan(log, 10, List.of());
|
||||
ObjectSourcePlan expected = new ObjectSourcePlan(tail);
|
||||
|
||||
assertEquals(expected.destinationCount(), sliced.destinationCount());
|
||||
int total = 0;
|
||||
for (int chunkX = -3; chunkX <= 30; chunkX++) {
|
||||
for (int chunkZ = -3; chunkZ <= 30; chunkZ++) {
|
||||
List<ObjectDestinationTransaction.Mutation> mutations = sliced.mutationsFor(chunkX, chunkZ);
|
||||
assertEquals(expected.mutationsFor(chunkX, chunkZ), mutations);
|
||||
total += mutations.size();
|
||||
}
|
||||
}
|
||||
assertEquals(54, total);
|
||||
}
|
||||
}
|
||||
@@ -163,6 +163,44 @@ public class ObjectSourcePlanCacheTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tryAcquireReportsBusySourcesWithoutConsumingThem() throws Exception {
|
||||
ObjectSourcePlanCache cache = new ObjectSourcePlanCache(1024L * 1024L);
|
||||
ObjectSourcePlan plan = planTouching(new int[]{0, 0}, new int[]{1, 0});
|
||||
CountDownLatch entered = new CountDownLatch(1);
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
AtomicInteger builds = new AtomicInteger();
|
||||
ExecutorService executor = Executors.newFixedThreadPool(1);
|
||||
try {
|
||||
Future<ObjectSourcePlan> owner = executor.submit(() -> cache.acquire(0, 0, 0, 0, 7, () -> {
|
||||
builds.incrementAndGet();
|
||||
entered.countDown();
|
||||
await(release);
|
||||
return plan;
|
||||
}));
|
||||
assertTrue(entered.await(5L, TimeUnit.SECONDS));
|
||||
|
||||
assertSame(ObjectSourcePlanCache.BUSY, cache.tryAcquire(0, 0, 1, 0, 7, () -> {
|
||||
builds.incrementAndGet();
|
||||
return plan;
|
||||
}));
|
||||
ObjectSourcePlan unrelated = planTouching(new int[]{5, 5});
|
||||
assertSame(unrelated, cache.tryAcquire(5, 5, 5, 5, 7, () -> unrelated));
|
||||
release.countDown();
|
||||
assertSame(plan, owner.get(5L, TimeUnit.SECONDS));
|
||||
|
||||
assertSame(plan, cache.tryAcquire(0, 0, 1, 0, 7, () -> {
|
||||
builds.incrementAndGet();
|
||||
return plan;
|
||||
}));
|
||||
assertEquals(1, builds.get());
|
||||
assertEquals(2L, cache.stats().drained());
|
||||
} finally {
|
||||
release.countDown();
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void concurrentDestinationsShareOneSourceBuild() throws Exception {
|
||||
ObjectSourcePlanCache cache = new ObjectSourcePlanCache(1024L * 1024L);
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
package art.arcane.iris.generation.noise;
|
||||
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.SplittableRandom;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class GeneratorProgramParityTest {
|
||||
private static final NoiseStyle[] STYLES = {
|
||||
NoiseStyle.SIMPLEX, NoiseStyle.PERLIN, NoiseStyle.IRIS, NoiseStyle.IRIS_HALF, NoiseStyle.FRACTAL_SMOKE,
|
||||
NoiseStyle.FRACTAL_WATER, NoiseStyle.CLOVER, NoiseStyle.CELLULAR, NoiseStyle.NOWHERE,
|
||||
NoiseStyle.CELLULAR_IRIS_DOUBLE, NoiseStyle.SIMPLEX_VASCULAR, NoiseStyle.FLAT, NoiseStyle.STATIC,
|
||||
NoiseStyle.GLOB_IRIS_THICK, NoiseStyle.FRACTAL_BILLOW_PERLIN, NoiseStyle.PERLIN_IRIS
|
||||
};
|
||||
|
||||
@Test
|
||||
public void programHeightsMatchScalarHeightsBitForBit() {
|
||||
SplittableRandom random = new SplittableRandom(0x5EED1E55L);
|
||||
TerrainBatchScratch scratch = new TerrainBatchScratch();
|
||||
for (int generatorIndex = 0; generatorIndex < 160; generatorIndex++) {
|
||||
long shapeSeed = random.nextLong();
|
||||
IrisGenerator scalar = generator(new SplittableRandom(shapeSeed));
|
||||
IrisGenerator batch = generator(new SplittableRandom(shapeSeed));
|
||||
long superSeed = random.nextLong();
|
||||
GeneratorProgram program = batch.compileProgram(superSeed);
|
||||
int count = 1 + random.nextInt(600);
|
||||
double[] xs = new double[count];
|
||||
double[] zs = new double[count];
|
||||
double[] out = new double[count];
|
||||
fillCoordinates(random, xs, zs, count);
|
||||
program.heights(scratch, xs, zs, count, out);
|
||||
for (int index = 0; index < count; index++) {
|
||||
assertEquals("generator " + generatorIndex + " point " + xs[index] + "," + zs[index],
|
||||
Double.doubleToRawLongBits(scalar.getHeight(xs[index], zs[index], superSeed)),
|
||||
Double.doubleToRawLongBits(out[index]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void singlePointProgramHeightsMatchScalarHeightsBitForBit() {
|
||||
SplittableRandom random = new SplittableRandom(0xFEEDL);
|
||||
TerrainBatchScratch scratch = new TerrainBatchScratch();
|
||||
for (int generatorIndex = 0; generatorIndex < 160; generatorIndex++) {
|
||||
long shapeSeed = random.nextLong();
|
||||
IrisGenerator scalar = generator(new SplittableRandom(shapeSeed));
|
||||
IrisGenerator compiled = generator(new SplittableRandom(shapeSeed));
|
||||
long superSeed = random.nextLong();
|
||||
GeneratorProgram program = compiled.compileProgram(superSeed);
|
||||
for (int sample = 0; sample < 400; sample++) {
|
||||
double x = coordinate(random);
|
||||
double z = random.nextInt(5) == 0 ? x : coordinate(random);
|
||||
assertEquals("generator " + generatorIndex + " point " + x + "," + z,
|
||||
Double.doubleToRawLongBits(scalar.getHeight(x, z, superSeed)),
|
||||
Double.doubleToRawLongBits(program.height(scratch, x, z)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void chunkGridsMatchScalarHeightsBitForBit() {
|
||||
SplittableRandom random = new SplittableRandom(0xC0FFEEL);
|
||||
TerrainBatchScratch scratch = new TerrainBatchScratch();
|
||||
double[] xs = new double[256];
|
||||
double[] zs = new double[256];
|
||||
double[] out = new double[256];
|
||||
for (int generatorIndex = 0; generatorIndex < 60; generatorIndex++) {
|
||||
long shapeSeed = random.nextLong();
|
||||
IrisGenerator scalar = generator(new SplittableRandom(shapeSeed));
|
||||
IrisGenerator batch = generator(new SplittableRandom(shapeSeed));
|
||||
long superSeed = random.nextLong();
|
||||
GeneratorProgram program = batch.compileProgram(superSeed);
|
||||
for (int chunk = 0; chunk < 4; chunk++) {
|
||||
int originX = random.nextInt(-200_000, 200_000) << 4;
|
||||
int originZ = random.nextInt(-200_000, 200_000) << 4;
|
||||
for (int index = 0; index < 256; index++) {
|
||||
xs[index] = originX + (index & 15);
|
||||
zs[index] = originZ + (index >> 4);
|
||||
}
|
||||
program.heights(scratch, xs, zs, 256, out);
|
||||
for (int index = 0; index < 256; index++) {
|
||||
assertEquals(Double.doubleToRawLongBits(scalar.getHeight(xs[index], zs[index], superSeed)),
|
||||
Double.doubleToRawLongBits(out[index]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void fillCoordinates(SplittableRandom random, double[] xs, double[] zs, int count) {
|
||||
for (int index = 0; index < count; index++) {
|
||||
xs[index] = coordinate(random);
|
||||
zs[index] = random.nextInt(5) == 0 ? xs[index] : coordinate(random);
|
||||
}
|
||||
}
|
||||
|
||||
private static double coordinate(SplittableRandom random) {
|
||||
return switch (random.nextInt(6)) {
|
||||
case 0 -> random.nextInt(-4096, 4096);
|
||||
case 1 -> random.nextInt(-30_000_000, 30_000_000);
|
||||
case 2 -> random.nextInt(-512, 512) + random.nextDouble();
|
||||
case 3 -> random.nextInt(-64, 64) * 6;
|
||||
case 4 -> random.nextDouble(-30_000_000D, 30_000_000D);
|
||||
default -> random.nextInt(-16, 16) * 0.25D;
|
||||
};
|
||||
}
|
||||
|
||||
private static IrisGenerator generator(SplittableRandom random) {
|
||||
int layers = 1 + random.nextInt(3);
|
||||
KList<IrisNoiseGenerator> composite = new KList<>();
|
||||
for (int layer = 0; layer < layers; layer++) {
|
||||
composite.add(layer(random, 0));
|
||||
}
|
||||
double[] details = {0D, 0.25D, 0.5D, 1D};
|
||||
IrisGenerator generator = new IrisGenerator().setComposite(composite)
|
||||
.setSurfaceDetail(details[random.nextInt(details.length)])
|
||||
.setMultiplicitive(random.nextInt(4) == 0)
|
||||
.setSeed(random.nextLong(-5000L, 5000L))
|
||||
.setZoom(0.2D + random.nextDouble() * 3D)
|
||||
.setOffsetX(random.nextInt(-40, 40) * 0.25D)
|
||||
.setOffsetZ(random.nextInt(-40, 40) * 0.25D)
|
||||
.setOpacity(0.1D + random.nextDouble());
|
||||
if (random.nextInt(3) == 0) {
|
||||
generator.setCliffHeightMin(random.nextDouble() * 4D).setCliffHeightMax(4D + random.nextDouble() * 12D)
|
||||
.setCliffHeightGenerator(layer(random, 2));
|
||||
}
|
||||
if (random.nextInt(5) == 0) {
|
||||
generator.setCellFractureHeight(random.nextDouble()).setCellFractureZoom(0.5D + random.nextDouble());
|
||||
}
|
||||
return generator;
|
||||
}
|
||||
|
||||
private static IrisNoiseGenerator layer(SplittableRandom random, int depth) {
|
||||
IrisNoiseGenerator layer = new IrisNoiseGenerator()
|
||||
.setStyle(STYLES[random.nextInt(STYLES.length)].style())
|
||||
.setSeed(random.nextLong(-100_000L, 100_000L))
|
||||
.setZoom(0.3D + random.nextDouble() * 4D)
|
||||
.setOpacity(0.05D + random.nextDouble())
|
||||
.setOffsetX(random.nextInt(-20, 20) * 0.5D)
|
||||
.setOffsetZ(random.nextInt(-20, 20) * 0.5D)
|
||||
.setOffsetY(random.nextInt(4) == 0 ? random.nextDouble() * 0.1D : 0D)
|
||||
.setNegative(random.nextInt(4) == 0)
|
||||
.setExponent(random.nextInt(4) == 0 ? 0.5D + random.nextDouble() : 1D)
|
||||
.setParametric(random.nextInt(6) == 0)
|
||||
.setBezier(random.nextInt(6) == 0)
|
||||
.setSinCentered(random.nextInt(8) == 0)
|
||||
.setOctaves(random.nextInt(5) == 0 ? 2 : 1)
|
||||
.setEnabled(depth == 0 || random.nextInt(8) != 0);
|
||||
if (depth < 4 && random.nextInt(3) != 0) {
|
||||
int children = random.nextInt(4) == 0 ? 2 + random.nextInt(2) : 1;
|
||||
KList<IrisNoiseGenerator> fracture = new KList<>();
|
||||
for (int child = 0; child < children; child++) {
|
||||
fracture.add(layer(random, depth + 1));
|
||||
}
|
||||
layer.setFracture(fracture);
|
||||
}
|
||||
return layer;
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,6 @@ package art.arcane.iris.generation.noise;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
@@ -34,7 +31,7 @@ public class IrisGeneratorBatchTest {
|
||||
assertTrue(batch.supportsBatch());
|
||||
double[] sampled = new double[XS.length];
|
||||
for (long seed : seeds) {
|
||||
batch.getHeights(XS, ZS, seed, sampled, sampled.length);
|
||||
batch.compileProgram(seed).heights(new TerrainBatchScratch(), XS, ZS, sampled.length, sampled);
|
||||
for (int index = 0; index < sampled.length; index++) {
|
||||
assertEquals(style + " detail=" + detail + " seed=" + seed + " index=" + index,
|
||||
Double.doubleToRawLongBits(scalar.getHeight(XS[index], ZS[index], seed)),
|
||||
@@ -54,7 +51,7 @@ public class IrisGeneratorBatchTest {
|
||||
IrisGenerator batch = generator(style, detail, false).setCliffHeightMax(0D).setCellFractureHeight(0D);
|
||||
double[] values = new double[XS.length];
|
||||
for (long seed : new long[]{-1337L, Long.MAX_VALUE - 37L}) {
|
||||
batch.getHeights(XS, ZS, seed, values, XS.length);
|
||||
batch.compileProgram(seed).heights(new TerrainBatchScratch(), XS, ZS, XS.length, values);
|
||||
for (int index = 0; index < XS.length; index++) {
|
||||
assertEquals(Double.doubleToRawLongBits(scalar.getHeight(XS[index], ZS[index], seed)),
|
||||
Double.doubleToRawLongBits(values[index]));
|
||||
@@ -65,13 +62,15 @@ public class IrisGeneratorBatchTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolvesLayerContextOncePerBatch() {
|
||||
public void resolvesLayerContextOncePerCompile() {
|
||||
IrisNoiseGenerator layer = spy(new IrisNoiseGenerator().setStyle(NoiseStyle.SIMPLEX.style()));
|
||||
IrisGenerator generator = new IrisGenerator().setComposite(new KList<>(layer));
|
||||
double[] sampled = new double[XS.length];
|
||||
generator.getHeights(XS, ZS, 99L, sampled, XS.length);
|
||||
GeneratorProgram program = generator.compileProgram(99L);
|
||||
verify(layer, times(1)).getGenerator(anyLong(), isNull(), isNull());
|
||||
clearInvocations(layer);
|
||||
program.heights(new TerrainBatchScratch(), XS, ZS, XS.length, sampled);
|
||||
verify(layer, times(0)).getGenerator(anyLong(), isNull(), isNull());
|
||||
for (int index = 0; index < XS.length; index++) {
|
||||
generator.getHeight(XS[index], ZS[index], 99L);
|
||||
}
|
||||
@@ -79,24 +78,15 @@ public class IrisGeneratorBatchTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void customGeneratorFallbackKeepsCoordinateOrder() {
|
||||
List<Double> sampled = new ArrayList<>();
|
||||
public void customGeneratorsCannotCompile() {
|
||||
IrisGenerator generator = new IrisGenerator() {
|
||||
@Override
|
||||
public double getHeight(double x, double z, long seed) {
|
||||
sampled.add(x);
|
||||
return sampled.size();
|
||||
return x;
|
||||
}
|
||||
};
|
||||
assertFalse(generator.supportsBatch());
|
||||
double[] values = new double[XS.length];
|
||||
generator.getHeights(XS, ZS, 0L, values, values.length);
|
||||
for (int index = 0; index < values.length; index++) {
|
||||
assertEquals(Double.valueOf(XS[index]), sampled.get(index));
|
||||
assertEquals(index + 1D, values[index], 0D);
|
||||
}
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> generator.getHeights(new double[4097], new double[4097], 0L, new double[4097], 4097));
|
||||
assertThrows(IllegalStateException.class, () -> generator.compileProgram(0L));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
package art.arcane.iris.generation.runtime;
|
||||
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.generation.bake.BakePlane;
|
||||
import art.arcane.iris.generation.bake.GeneratorEvaluations;
|
||||
import art.arcane.iris.generation.block.B;
|
||||
import art.arcane.iris.generation.image.IrisImageMapApplication;
|
||||
import art.arcane.iris.generation.image.IrisImageMapRuntime;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
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.testsupport.PlatformLeakGuard;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
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 org.mockito.MockedStatic;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.SplittableRandom;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Answers.CALLS_REAL_METHODS;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyDouble;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class IrisComplexBakeParityTest {
|
||||
@ClassRule public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
|
||||
@Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
private IrisSettings previousSettings;
|
||||
|
||||
@Before
|
||||
public void prepareRuntime() {
|
||||
previousSettings = IrisSettings.settings;
|
||||
IrisSettings.settings = new IrisSettings();
|
||||
IrisServices.register(PreservationRegistry.class, mock(PreservationRegistry.class));
|
||||
IrisPlatform platform = mock(IrisPlatform.class);
|
||||
when(platform.platformName()).thenReturn("bukkit");
|
||||
IrisPlatforms.bind(platform);
|
||||
}
|
||||
|
||||
@After
|
||||
public void releaseRuntime() {
|
||||
IrisPlatforms.unbind();
|
||||
IrisServices.remove(PreservationRegistry.class);
|
||||
IrisSettings.settings = previousSettings;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void batchedChunkBaseHeightsEqualScalarHeightsBitForBit() throws Exception {
|
||||
try (Statics ignored = new Statics(); Fixture batched = new Fixture(pack()); Fixture scalar = new Fixture(pack())) {
|
||||
SplittableRandom random = new SplittableRandom(0xBA5EL);
|
||||
for (int chunk = 0; chunk < 48; chunk++) {
|
||||
int chunkX = random.nextInt(-400, 400);
|
||||
int chunkZ = random.nextInt(-400, 400);
|
||||
double[] baked = (double[]) batched.complex.bakePlane(BakePlane.BASE_HEIGHT, chunkX, chunkZ);
|
||||
for (int index = 0; index < 256; index++) {
|
||||
int x = (chunkX << 4) + (index & 15);
|
||||
int z = (chunkZ << 4) + (index >> 4);
|
||||
assertEquals("chunk " + chunkX + "," + chunkZ + " column " + index,
|
||||
Double.doubleToRawLongBits(scalar.complex.getBaseTerrainHeightStream().getDouble(x, z)),
|
||||
Double.doubleToRawLongBits(baked[index]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void batchedChunksEvaluateExactlyTheScalarGeneratorSet() throws Exception {
|
||||
try (Statics ignored = new Statics(); Fixture batched = new Fixture(pack()); Fixture scalar = new Fixture(pack())) {
|
||||
SplittableRandom random = new SplittableRandom(0xC0DEL);
|
||||
long totalBatched = 0L;
|
||||
for (int chunk = 0; chunk < 32; chunk++) {
|
||||
int chunkX = random.nextInt(-400, 400);
|
||||
int chunkZ = random.nextInt(-400, 400);
|
||||
GeneratorEvaluations.Snapshot before = GeneratorEvaluations.snapshot();
|
||||
batched.complex.bakePlane(BakePlane.BASE_HEIGHT, chunkX, chunkZ);
|
||||
GeneratorEvaluations.Snapshot afterBatch = GeneratorEvaluations.snapshot();
|
||||
for (int index = 0; index < 256; index++) {
|
||||
scalar.complex.getBaseTerrainHeightStream().getDouble((chunkX << 4) + (index & 15), (chunkZ << 4) + (index >> 4));
|
||||
}
|
||||
GeneratorEvaluations.Snapshot afterScalar = GeneratorEvaluations.snapshot();
|
||||
long batchedEvaluations = afterBatch.minus(before).bakedEvaluations();
|
||||
assertEquals(0L, afterBatch.minus(before).scalarEvaluations());
|
||||
assertEquals("chunk " + chunkX + "," + chunkZ, batchedEvaluations,
|
||||
afterScalar.minus(afterBatch).scalarEvaluations());
|
||||
totalBatched += batchedEvaluations;
|
||||
}
|
||||
assertTrue(totalBatched > 0L);
|
||||
}
|
||||
}
|
||||
|
||||
private Path pack() throws Exception {
|
||||
Path pack = temporaryFolder.newFolder().toPath();
|
||||
Files.createDirectories(pack.resolve("dimensions"));
|
||||
Files.createDirectories(pack.resolve("regions"));
|
||||
Files.createDirectories(pack.resolve("biomes"));
|
||||
Files.createDirectories(pack.resolve("generators"));
|
||||
Files.writeString(pack.resolve("dimensions/main.json"), """
|
||||
{"regions":["alpha","beta"],"landChance":0.55,"regionZoom":0.2,"biomeZoom":0.15,
|
||||
"carvingEnabled":false,"hydrology":{"rivers":{"enabled":false}}}
|
||||
""");
|
||||
Files.writeString(pack.resolve("regions/alpha.json"),
|
||||
"{\"landBiomes\":[\"peaks\",\"meadow\",\"dunes\"],\"seaBiomes\":[\"sea\"],\"rarity\":1}");
|
||||
Files.writeString(pack.resolve("regions/beta.json"),
|
||||
"{\"landBiomes\":[\"meadow\",\"spires\"],\"seaBiomes\":[\"sea\"],\"rarity\":2}");
|
||||
generator(pack, "mountain", "BILINEAR_STARCAST_9", 52, 0.5D, """
|
||||
[{"style":{"style":"FRACTAL_SMOKE","zoom":1},"seed":3356}]""", "");
|
||||
generator(pack, "magnet", "BILINEAR_STARCAST_9", 52, 0.5D, """
|
||||
[{"style":{"style":"FRACTAL_SMOKE","zoom":0.6},"seed":911}]""", "");
|
||||
generator(pack, "plain", "BILINEAR_STARCAST_9", 12, 0.5D, """
|
||||
[{"style":{"style":"IRIS_DOUBLE","zoom":0.87,"fracture":{"style":"NOWHERE","zoom":0.195,"multiplier":18}},"seed":10056,"bezier":true}]""", "");
|
||||
generator(pack, "cliffs", "BILINEAR_STARCAST_9", 12, 0.5D, """
|
||||
[{"style":{"style":"GLOB","zoom":0.4,"exponent":0.7,"fracture":{"style":"NOWHERE","zoom":0.1,"multiplier":5}},"negative":true,"seed":10056}]""",
|
||||
",\"cliffHeightMax\":80,\"cliffHeightMin\":35,\"cliffHeightGenerator\":{\"seed\":2348,\"style\":{\"style\":\"CELLULAR_HEIGHT\"},\"zoom\":0.4}");
|
||||
generator(pack, "dunes", "BILINEAR_STARCAST_9", 23, 1D, """
|
||||
[{"style":{"style":"SIMPLEX","zoom":2},"seed":77,"fracture":[{"style":{"style":"PERLIN"},"seed":5,"opacity":2.5}]}]""", "");
|
||||
generator(pack, "spikes", "STARCAST_12", 42, 0D, """
|
||||
[{"style":{"style":"CELLULAR","zoom":0.5},"seed":4}]""", "");
|
||||
biome(pack, "peaks", "[{\"generator\":\"mountain\",\"min\":40,\"max\":120},{\"generator\":\"spikes\",\"min\":0,\"max\":20}]",
|
||||
",\"children\":[\"meadow\"],\"childShrinkFactor\":1.5");
|
||||
biome(pack, "meadow", "[{\"generator\":\"plain\",\"min\":2,\"max\":14},{\"generator\":\"cliffs\",\"min\":0,\"max\":9}]", "");
|
||||
biome(pack, "dunes", "[{\"generator\":\"dunes\",\"min\":3,\"max\":30},{\"generator\":\"magnet\",\"min\":-4,\"max\":6}]", "");
|
||||
biome(pack, "spires", "[{\"generator\":\"spikes\",\"min\":10,\"max\":60},{\"generator\":\"plain\",\"min\":1,\"max\":4}]", "");
|
||||
biome(pack, "sea", "[{\"generator\":\"mountain\",\"min\":-32,\"max\":-10}]", "");
|
||||
return pack;
|
||||
}
|
||||
|
||||
private static void generator(Path pack, String key, String function, double scale, double detail,
|
||||
String composite, String extra) throws Exception {
|
||||
Files.writeString(pack.resolve("generators/" + key + ".json"), "{\"surfaceDetail\":" + detail
|
||||
+ ",\"interpolator\":{\"function\":\"" + function + "\",\"horizontalScale\":" + scale + "}"
|
||||
+ ",\"seed\":" + Math.abs(key.hashCode()) + ",\"composite\":" + composite.strip() + extra + "}");
|
||||
}
|
||||
|
||||
private static void biome(Path pack, String key, String generators, String extra) throws Exception {
|
||||
Files.writeString(pack.resolve("biomes/" + key + ".json"), "{\"generators\":" + generators + extra + "}");
|
||||
}
|
||||
|
||||
private static final class Statics implements AutoCloseable {
|
||||
private final MockedStatic<B> blocks = mockStatic(B.class);
|
||||
private final MockedStatic<IrisImageMapRuntime> imageRuntimes = mockStatic(IrisImageMapRuntime.class);
|
||||
|
||||
private Statics() {
|
||||
NativeBlockState block = mock(NativeBlockState.class);
|
||||
when(block.isFluid()).thenReturn(true);
|
||||
blocks.when(() -> B.getState(anyString())).thenReturn(block);
|
||||
blocks.when(() -> B.getStateOrNull(anyString(), eq(false))).thenReturn(block);
|
||||
IrisImageMapRuntime images = mock(IrisImageMapRuntime.class);
|
||||
when(images.has(IrisImageMapApplication.BIOME)).thenReturn(false);
|
||||
when(images.sampleTerrainHeight(anyDouble(), anyDouble(), anyDouble()))
|
||||
.thenAnswer(invocation -> invocation.<Double>getArgument(2));
|
||||
imageRuntimes.when(() -> IrisImageMapRuntime.compile(any(Engine.class))).thenReturn(images);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
imageRuntimes.close();
|
||||
blocks.close();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Fixture implements AutoCloseable {
|
||||
private final Engine engine = mock(Engine.class, CALLS_REAL_METHODS);
|
||||
private final EnginePlatformHooks hooks = mock(EnginePlatformHooks.class);
|
||||
private final IrisData data;
|
||||
private final IrisComplex complex;
|
||||
|
||||
private Fixture(Path pack) {
|
||||
data = IrisData.openRuntime(pack.toFile());
|
||||
IrisDimension dimension = data.getDimensionLoader().load("main");
|
||||
doReturn(data).when(engine).getData();
|
||||
doReturn(dimension).when(engine).getDimension();
|
||||
doReturn(384).when(engine).getHeight();
|
||||
doReturn(-64).when(engine).getMinHeight();
|
||||
doReturn(320).when(engine).getMaxHeight();
|
||||
when(hooks.isMainThread()).thenReturn(false);
|
||||
doReturn(hooks).when(engine).getPlatformHooks();
|
||||
doReturn(new SeedManager(1337L)).when(engine).getSeedManager();
|
||||
complex = new IrisComplex(engine);
|
||||
doReturn(complex).when(engine).getComplex();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
complex.close();
|
||||
data.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-8
@@ -44,6 +44,7 @@ import static org.mockito.ArgumentMatchers.anyDouble;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.atMost;
|
||||
import static org.mockito.Mockito.clearInvocations;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@@ -89,16 +90,10 @@ public class IrisComplexHeightStreamTest {
|
||||
try (Fixture fixture = new Fixture(pack())) {
|
||||
IrisBiome mapped = new IrisBiome();
|
||||
when(fixture.images.sampleBiome(anyDouble(), anyDouble())).thenReturn(mapped);
|
||||
MeteredCache bridge = (MeteredCache) fixture.complex.getBridgeStream();
|
||||
MeteredCache regions = (MeteredCache) fixture.complex.getRegionStream();
|
||||
MeteredCache heights = (MeteredCache) fixture.complex.getNaturalHeightStream();
|
||||
assertSame(mapped, fixture.complex.getBaseBiomeStream().get(-16, 32));
|
||||
assertSame(mapped, fixture.complex.getBaseBiomeStream().get(-16.75, 10, 32.5));
|
||||
assertSame(mapped, fixture.complex.getNaturalTrueBiomeStream().get(-16, 32));
|
||||
assertSame(mapped, fixture.complex.getNaturalTrueBiomeStream().get(-16.75, 10, 32.5));
|
||||
assertEquals(0L, bridge.getSize());
|
||||
assertEquals(0L, regions.getSize());
|
||||
assertEquals(0L, heights.getSize());
|
||||
assertNull(fixture.complex.getSharedCornerBounds());
|
||||
|
||||
when(fixture.images.sampleBiome(anyDouble(), anyDouble())).thenReturn(null);
|
||||
@@ -116,7 +111,7 @@ public class IrisComplexHeightStreamTest {
|
||||
int[] rounded = new int[256];
|
||||
clearInvocations(fixture.hooks);
|
||||
cache.fillRounded(rounded);
|
||||
verify(fixture.hooks, times(1)).isMainThread();
|
||||
verify(fixture.hooks, atMost(2)).isMainThread();
|
||||
assertEquals(256, samples.get());
|
||||
for (int index = 0; index < 256; index++) {
|
||||
int x = index & 15;
|
||||
@@ -127,7 +122,7 @@ public class IrisComplexHeightStreamTest {
|
||||
}
|
||||
clearInvocations(fixture.hooks);
|
||||
cache.fill();
|
||||
verify(fixture.hooks, times(1)).isMainThread();
|
||||
verify(fixture.hooks, atMost(2)).isMainThread();
|
||||
assertEquals(256, samples.get());
|
||||
}
|
||||
}
|
||||
|
||||
+51
-9
@@ -2,6 +2,8 @@ package art.arcane.iris.generation.runtime;
|
||||
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.world.pregen.PregenPerformanceProfile;
|
||||
import art.arcane.iris.generation.bake.ChunkBakeCache;
|
||||
import art.arcane.iris.generation.bake.PlaneSource;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyPlanner;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyPlannerSettings;
|
||||
@@ -48,17 +50,14 @@ public class IrisComplexHydrologyNoiseCacheTest {
|
||||
try (Fixture fixture = new Fixture(1_024)) {
|
||||
ProceduralStream<Double> wrapper = fixture.complex.getRawHeightStream();
|
||||
assertEquals(13D, fixture.height.getDouble(18, 5), 0D);
|
||||
assertEquals(13D, fixture.rawHeight.getDouble(18, 5), 0D);
|
||||
|
||||
PregenPerformanceProfile.apply(fixture.engine);
|
||||
|
||||
assertEquals(4_096L * 256L, fixture.height.getMaxSize());
|
||||
assertEquals(4_096L * 256L, fixture.rawHeight.getMaxSize());
|
||||
assertTrue(fixture.bakes.capacity() >= 4_096);
|
||||
assertEquals(1_024L * 256L, fixture.biomes.getMaxSize());
|
||||
assertSame(wrapper, fixture.complex.getRawHeightStream());
|
||||
assertEquals(47D, wrapper.getDouble(18, 5), 0D);
|
||||
assertEquals(13D, fixture.height.getDouble(18, 5), 0D);
|
||||
assertEquals(13D, fixture.rawHeight.getDouble(18, 5), 0D);
|
||||
} finally {
|
||||
PregenPerformanceProfile.restore();
|
||||
IrisSettings.settings = previousSettings;
|
||||
@@ -75,7 +74,7 @@ public class IrisComplexHydrologyNoiseCacheTest {
|
||||
fixture.complex.ensureTerrainNoiseCacheSize(4_096);
|
||||
|
||||
fixture.assertCapacities(32_768);
|
||||
assertEquals(4_096L * 256L, fixture.rawHeight.getMaxSize());
|
||||
assertTrue(fixture.bakes.capacity() >= 4_096);
|
||||
assertEquals(reservedBytes, budget.reservedBytes());
|
||||
fixture.complex.close();
|
||||
assertEquals(0L, budget.reservedBytes());
|
||||
@@ -88,7 +87,7 @@ public class IrisComplexHydrologyNoiseCacheTest {
|
||||
fixture.complex.ensureTerrainNoiseCacheSize(4_096);
|
||||
|
||||
fixture.assertCapacities(65_536);
|
||||
assertEquals(65_536L * 256L, fixture.rawHeight.getMaxSize());
|
||||
assertTrue(fixture.bakes.capacity() >= 65_536);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,11 +228,54 @@ public class IrisComplexHydrologyNoiseCacheTest {
|
||||
HydrologyNoiseCacheBudget budget = new HydrologyNoiseCacheBudget(Long.MAX_VALUE);
|
||||
setField(fixture.complex, "detached", true);
|
||||
fixture.complex.expandHydrologyNoiseCaches(budget);
|
||||
fixture.complex.reserveHydrologyBake(budget);
|
||||
fixture.assertCapacities(1_024);
|
||||
assertEquals(1_024, fixture.bakes.capacity());
|
||||
setField(fixture.complex, "detached", false);
|
||||
fixture.complex.setHydrologyRuntime(null);
|
||||
fixture.complex.expandHydrologyNoiseCaches(budget);
|
||||
fixture.complex.reserveHydrologyBake(budget);
|
||||
fixture.assertCapacities(1_024);
|
||||
assertEquals(1_024, fixture.bakes.capacity());
|
||||
assertEquals(0L, budget.reservedBytes());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void plannerReservationGrowsTheBakeToTheHydrologyTarget() throws Exception {
|
||||
try (Fixture fixture = new Fixture(1_024)) {
|
||||
HydrologyNoiseCacheBudget budget = new HydrologyNoiseCacheBudget(Long.MAX_VALUE);
|
||||
fixture.complex.reserveHydrologyBake(budget);
|
||||
fixture.complex.reserveHydrologyBake(budget);
|
||||
assertEquals(32_768, fixture.bakes.capacity());
|
||||
assertEquals(2L * (32_768L - 1_024L) * 4_096L, budget.reservedBytes());
|
||||
fixture.complex.ensureTerrainNoiseCacheSize(4_096);
|
||||
assertEquals(32_768, fixture.bakes.capacity());
|
||||
fixture.complex.close();
|
||||
assertEquals(0L, budget.reservedBytes());
|
||||
fixture.complex.reserveHydrologyBake(budget);
|
||||
assertEquals(0L, budget.reservedBytes());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void plannerReservationTakesOnlyTheGrantedShareOfTheBudget() throws Exception {
|
||||
try (Fixture fixture = new Fixture(1_024)) {
|
||||
HydrologyNoiseCacheBudget budget = new HydrologyNoiseCacheBudget(2L * 80L * 4_096L + 4_095L);
|
||||
fixture.complex.reserveHydrologyBake(budget);
|
||||
assertEquals(1_104, fixture.bakes.capacity());
|
||||
assertEquals(2L * 80L * 4_096L, budget.reservedBytes());
|
||||
fixture.complex.close();
|
||||
assertEquals(0L, budget.reservedBytes());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void plannerReservationLeavesLargerBakesUnchanged() throws Exception {
|
||||
try (Fixture fixture = new Fixture(65_536)) {
|
||||
HydrologyNoiseCacheBudget budget = new HydrologyNoiseCacheBudget(Long.MAX_VALUE);
|
||||
fixture.complex.reserveHydrologyBake(budget);
|
||||
assertEquals(65_536, fixture.bakes.capacity());
|
||||
assertEquals(0L, budget.reservedBytes());
|
||||
}
|
||||
}
|
||||
@@ -297,7 +339,7 @@ public class IrisComplexHydrologyNoiseCacheTest {
|
||||
private final CachedStream2D<IrisRegion> regions;
|
||||
private final CachedStream2D<InferredType> bridge;
|
||||
private final CachedDoubleStream2D height;
|
||||
private final CachedDoubleStream2D rawHeight;
|
||||
private final ChunkBakeCache bakes;
|
||||
|
||||
private Fixture(int capacity) throws Exception {
|
||||
services.when(() -> IrisServices.get(PreservationRegistry.class)).thenReturn(mock(PreservationRegistry.class));
|
||||
@@ -305,10 +347,10 @@ public class IrisComplexHydrologyNoiseCacheTest {
|
||||
regions = objectCache(capacity);
|
||||
bridge = objectCache(capacity);
|
||||
height = heightCache(capacity);
|
||||
rawHeight = heightCache(capacity);
|
||||
bakes = new ChunkBakeCache(mock(PlaneSource.class), capacity);
|
||||
complex.setNaturalHeightStream(height);
|
||||
complex.setHeightStream(ProceduralStream.ofDouble((x, z) -> 47D));
|
||||
setField(complex, "cachedHeightStream", rawHeight);
|
||||
setField(complex, "bakeCache", bakes);
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
complex.setBaseBiomeStream(biomes);
|
||||
complex.setRegionStream(regions);
|
||||
|
||||
+2
-2
@@ -83,7 +83,7 @@ public class IrisComplexSelfSelectionTest {
|
||||
Field field = IrisComplex.class.getDeclaredField("childSelectionPlans");
|
||||
field.setAccessible(true);
|
||||
field.set(complex, plans);
|
||||
implode = IrisComplex.class.getDeclaredMethod("implode", IrisBiome.class, Double.class, Double.class, int.class);
|
||||
implode = IrisComplex.class.getDeclaredMethod("implode", IrisBiome.class, double.class, double.class, int.class);
|
||||
implode.setAccessible(true);
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ public class IrisComplexSelfSelectionTest {
|
||||
}
|
||||
|
||||
@Override
|
||||
public CNG getChildrenGenerator(RNG rng, int signature, double scale) {
|
||||
public CNG getChildrenGenerator(long seed, double scale) {
|
||||
return noise;
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -97,11 +97,11 @@ public class IrisCarveModifierInferenceIsolationTest {
|
||||
doReturn(0D).when(region).getShoreHeight(anyDouble(), anyDouble());
|
||||
Method fixBiomeType = IrisComplex.class.getDeclaredMethod(
|
||||
"fixBiomeType",
|
||||
Double.class,
|
||||
double.class,
|
||||
IrisBiome.class,
|
||||
IrisRegion.class,
|
||||
Double.class,
|
||||
Double.class,
|
||||
double.class,
|
||||
double.class,
|
||||
double.class
|
||||
);
|
||||
fixBiomeType.setAccessible(true);
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package art.arcane.iris.generation.stream;
|
||||
|
||||
import art.arcane.volmlib.util.math.Rarity;
|
||||
import art.arcane.volmlib.util.stream.ProceduralStream;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.SplittableRandom;
|
||||
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
|
||||
public class RarityStreamTest {
|
||||
@Test
|
||||
public void picksMatchRarityStreamSelectionForEveryNoiseValue() {
|
||||
SplittableRandom random = new SplittableRandom(4242L);
|
||||
for (int trial = 0; trial < 200; trial++) {
|
||||
List<Option> options = new ArrayList<>();
|
||||
int count = 1 + random.nextInt(12);
|
||||
for (int index = 0; index < count; index++) {
|
||||
options.add(new Option(random.nextInt(-2, 40)));
|
||||
}
|
||||
double[] value = new double[1];
|
||||
ProceduralStream<Double> noise = ProceduralStream.ofDouble((x, z) -> value[0]);
|
||||
ProceduralStream<Option> expected = noise.selectRarity(options);
|
||||
RarityStream<Option> candidate = new RarityStream<>(noise, options);
|
||||
for (int sample = 0; sample < 2_000; sample++) {
|
||||
value[0] = switch (random.nextInt(5)) {
|
||||
case 0 -> random.nextDouble();
|
||||
case 1 -> random.nextDouble(-0.5D, 1.5D);
|
||||
case 2 -> random.nextInt(0, 64) / 63D;
|
||||
case 3 -> sample % 2 == 0 ? Double.NaN : -0D;
|
||||
default -> Math.nextUp(random.nextInt(0, 16) / 15D);
|
||||
};
|
||||
assertSame(expected.get(sample, -sample), candidate.get(sample, -sample));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void emptyOptionsSelectNothing() {
|
||||
assertNull(new RarityStream<Option>(ProceduralStream.ofDouble((x, z) -> 0.5D), List.of()).get(0D, 0D));
|
||||
}
|
||||
|
||||
private record Option(int rarity) implements Rarity {
|
||||
@Override
|
||||
public int getRarity() {
|
||||
return rarity;
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -791,7 +791,7 @@ public class IrisObjectPlacementRunnerRegressionTest {
|
||||
|
||||
object.place(0, ANCHOR_Y, 0, placer, placement, new RNG(2L), data);
|
||||
|
||||
assertEquals(1, placer.blockReads);
|
||||
assertEquals(enabled || authored ? 1 : 0, placer.blockReads);
|
||||
assertEquals(1, placer.writes().size());
|
||||
assertSame(submerged && (enabled || authored) ? wet : dry,
|
||||
placer.writes().get(0).state());
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package art.arcane.iris.structure.object;
|
||||
|
||||
import art.arcane.iris.generation.geometry.IrisBlockVector;
|
||||
import art.arcane.iris.testsupport.KeyedBlockState;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertSame;
|
||||
|
||||
public class ObjectPlacementBakeTest {
|
||||
@Test
|
||||
public void spinsResolvingToTheSameKernelShareOneBake() {
|
||||
IrisObject object = object();
|
||||
IrisObjectRotation rotation = new IrisObjectRotation();
|
||||
|
||||
ObjectPlacementBake first = ObjectPlacementBake.of(object, new SpinKernel(rotation, 0, 10, 0), false, null);
|
||||
ObjectPlacementBake second = ObjectPlacementBake.of(object, new SpinKernel(rotation, 3, 80, 9), false, null);
|
||||
ObjectPlacementBake turned = ObjectPlacementBake.of(object, new SpinKernel(rotation, 0, 100, 0), false, null);
|
||||
|
||||
assertSame(first, second);
|
||||
assertNotSame(first, turned);
|
||||
assertEquals(3, first.count());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bakedOffsetsMatchTheKernelTransformInCursorOrder() {
|
||||
IrisObject object = object();
|
||||
IrisObjectRotation rotation = IrisObjectRotation.of(0D, 90D, 0D);
|
||||
SpinKernel spin = new SpinKernel(rotation, 0, 0, 0);
|
||||
IrisBlockVector translate = new IrisBlockVector(1.5D, -2D, 0.25D);
|
||||
|
||||
ObjectPlacementBake bake = ObjectPlacementBake.of(object, spin, true, translate);
|
||||
double[] exact = bake.exact(object, spin);
|
||||
|
||||
int index = 0;
|
||||
for (IrisBlockVector source : object.getBlocks().keys()) {
|
||||
IrisBlockVector expected = source.clone();
|
||||
spin.rotate(expected);
|
||||
expected.setY(-expected.getBlockY());
|
||||
expected.add(translate);
|
||||
assertEquals((int) Math.round(expected.getX()), bake.offsetX(index));
|
||||
assertEquals((int) Math.round(expected.getY()), bake.offsetY(index));
|
||||
assertEquals((int) Math.round(expected.getZ()), bake.offsetZ(index));
|
||||
assertEquals(expected.getBlockY(), bake.layerY(index));
|
||||
assertEquals(expected.getX(), exact[index * 3], 0D);
|
||||
assertEquals(expected.getY(), exact[index * 3 + 1], 0D);
|
||||
assertEquals(expected.getZ(), exact[index * 3 + 2], 0D);
|
||||
index++;
|
||||
}
|
||||
assertEquals(bake.count(), index);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void editedObjectsRebake() {
|
||||
IrisObject object = object();
|
||||
SpinKernel spin = new SpinKernel(new IrisObjectRotation(), 0, 0, 0);
|
||||
ObjectPlacementBake before = ObjectPlacementBake.of(object, spin, false, null);
|
||||
|
||||
object.getBlocks().put(new IrisBlockVector(1, 1, 1), new KeyedBlockState("minecraft:dirt"));
|
||||
ObjectPlacementBake after = ObjectPlacementBake.of(object, spin, false, null);
|
||||
|
||||
assertNotSame(before, after);
|
||||
assertEquals(4, after.count());
|
||||
assertSame(after, ObjectPlacementBake.of(object, spin, false, null));
|
||||
}
|
||||
|
||||
private static IrisObject object() {
|
||||
IrisObject object = new IrisObject(5, 5, 5);
|
||||
KeyedBlockState stone = new KeyedBlockState("minecraft:stone");
|
||||
object.getBlocks().put(new IrisBlockVector(-2, 0, 1), stone);
|
||||
object.getBlocks().put(new IrisBlockVector(0, -1, 2), stone);
|
||||
object.getBlocks().put(new IrisBlockVector(2, 2, -2), new KeyedBlockState("minecraft:oak_log[axis=y]"));
|
||||
return object;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,761 @@
|
||||
package art.arcane.iris.structure.object;
|
||||
|
||||
import art.arcane.iris.generation.block.BlockDataMergeSupport;
|
||||
import art.arcane.iris.generation.block.IrisBlockData;
|
||||
import art.arcane.iris.generation.block.TileData;
|
||||
import art.arcane.iris.generation.cave.CarvingMode;
|
||||
import art.arcane.iris.generation.decoration.IrisStiltSettings;
|
||||
import art.arcane.iris.generation.decoration.IrisVacuumSettings;
|
||||
import art.arcane.iris.generation.decoration.tree.TreeBlockCanvas;
|
||||
import art.arcane.iris.generation.decoration.tree.TreeGrounding;
|
||||
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.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.generation.terrain.IrisMaterialPalette;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.pack.loading.ResourceLoader;
|
||||
import art.arcane.iris.pack.validation.ContentGate;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
import art.arcane.iris.testsupport.PlatformLeakGuard;
|
||||
import art.arcane.iris.world.entity.IrisMarker;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.math.BlockPosition;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.matter.MatterMarker;
|
||||
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.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
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.anyDouble;
|
||||
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 ObjectPlacementGoldenTest {
|
||||
@ClassRule
|
||||
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
|
||||
|
||||
private static final int OBJECTS = 200;
|
||||
private static final int SEEDS = 5;
|
||||
private static final long GOLDEN = 0x408f9d4113015897L;
|
||||
private static final long MARKER_GOLDEN = 0x60b4bbf1144b239cL;
|
||||
|
||||
private static final List<String> PALETTE = List.of(
|
||||
"minecraft:stone",
|
||||
"minecraft:dirt",
|
||||
"minecraft:grass_block[snowy=false]",
|
||||
"minecraft:oak_log[axis=y]",
|
||||
"minecraft:oak_log[axis=x]",
|
||||
"minecraft:oak_leaves[distance=3,persistent=false,waterlogged=false]",
|
||||
"minecraft:oak_leaves[distance=1,persistent=true,waterlogged=false]",
|
||||
"minecraft:oak_leaves[distance=7,persistent=false,waterlogged=true]",
|
||||
"minecraft:oak_stairs[facing=north,half=bottom,waterlogged=false]",
|
||||
"minecraft:oak_stairs[facing=east,half=top,waterlogged=true]",
|
||||
"minecraft:vine[east=false,north=true,south=false,up=false,west=false]",
|
||||
"minecraft:air",
|
||||
"minecraft:cave_air",
|
||||
"minecraft:chest[facing=south,waterlogged=false]",
|
||||
"minecraft:lantern[hanging=false,waterlogged=false]",
|
||||
"minecraft:cobblestone",
|
||||
"minecraft:glass",
|
||||
"minecraft:oak_slab[type=bottom,waterlogged=false]");
|
||||
|
||||
private final Map<String, GoldenState> states = new ConcurrentHashMap<>();
|
||||
private IrisData data;
|
||||
private IrisData objectData;
|
||||
private Engine engine;
|
||||
private IrisObjectRotation.StateRotator previousRotator;
|
||||
private BlockDataMergeSupport.StateMerger previousMerger;
|
||||
|
||||
@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);
|
||||
previousRotator = IrisObjectRotation.bindPlatformRotator(ObjectPlacementGoldenTest::rotateState);
|
||||
previousMerger = BlockDataMergeSupport.bindPlatformMerger(this::mergeStates);
|
||||
|
||||
ProceduralStream<NativeBlockState> rock = mock(ProceduralStream.class, withSettings().stubOnly());
|
||||
when(rock.get(anyDouble(), anyDouble())).thenAnswer(invocation -> state("minecraft:stone"));
|
||||
IrisComplex complex = mock(IrisComplex.class, withSettings().stubOnly());
|
||||
when(complex.getRockStream()).thenReturn(rock);
|
||||
IrisDimension dimension = mock(IrisDimension.class, withSettings().stubOnly());
|
||||
engine = mock(Engine.class, withSettings().stubOnly());
|
||||
when(engine.getHeight()).thenReturn(256);
|
||||
when(engine.getMinHeight()).thenReturn(0);
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
when(engine.getDimension()).thenReturn(dimension);
|
||||
ContentGate gate = mock(ContentGate.class, withSettings().stubOnly());
|
||||
when(gate.ready()).thenReturn(false);
|
||||
data = mock(IrisData.class, withSettings().stubOnly());
|
||||
when(data.getEngine()).thenReturn(engine);
|
||||
when(data.getContentGate()).thenReturn(gate);
|
||||
objectData = mock(IrisData.class, withSettings().stubOnly());
|
||||
when(objectData.getContentGate()).thenReturn(gate);
|
||||
}
|
||||
|
||||
@After
|
||||
public void unbindPlatform() {
|
||||
IrisObjectRotation.restorePlatformRotator(previousRotator);
|
||||
BlockDataMergeSupport.restorePlatformMerger(previousMerger);
|
||||
IrisPlatforms.unbind();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void placementsMatchTheRecordedBlockStream() throws IOException {
|
||||
Random random = new Random(20261021L);
|
||||
List<IrisObjectRotation> rotations = rotations();
|
||||
long combined = 17L;
|
||||
StringBuilder dump = new StringBuilder();
|
||||
for (int index = 0; index < OBJECTS; index++) {
|
||||
IrisObject object = randomObject(random, index);
|
||||
object.setLoader(objectData);
|
||||
IrisObjectPlacement base = randomPlacement(random);
|
||||
for (int rotationIndex = 0; rotationIndex < rotations.size(); rotationIndex++) {
|
||||
IrisObjectPlacement placement = base.toPlacement("golden");
|
||||
placement.setRotation(rotations.get(rotationIndex));
|
||||
boolean explicitY = (index + rotationIndex) % 3 == 0;
|
||||
for (int seed = 0; seed < SEEDS; seed++) {
|
||||
RNG rng = new RNG(index * 1_000_003L + rotationIndex * 7919L + seed);
|
||||
GoldenPlacer placer = new GoldenPlacer(engine, (index & 1) == 0);
|
||||
List<String> listened = new ArrayList<>();
|
||||
int x = 400 + index * 3 + seed;
|
||||
int z = -900 + rotationIndex * 5 - seed;
|
||||
int y = explicitY ? 90 + seed : -1;
|
||||
int result;
|
||||
if (placement.getMode() == ObjectPlaceMode.ORGANIC_STILT && explicitY && seed % 2 == 0) {
|
||||
result = object.placeOnFloor(x, 70, z, placer, placement, rng,
|
||||
(position, state) -> listened.add(position.getX() + "," + position.getY() + "," + position.getZ() + "=" + state.key()),
|
||||
data);
|
||||
} else {
|
||||
result = object.place(x, y, z, placer, placement, rng,
|
||||
(position, state) -> listened.add(position.getX() + "," + position.getY() + "," + position.getZ() + "=" + state.key()),
|
||||
null, data);
|
||||
}
|
||||
long hash = placer.hash;
|
||||
hash = mix(hash, result);
|
||||
hash = mix(hash, rng.nextLong());
|
||||
for (String entry : listened) {
|
||||
hash = mix(hash, entry.hashCode());
|
||||
}
|
||||
hash = mix(hash, listened.size());
|
||||
combined = mix(combined, hash);
|
||||
dump.append(index).append(' ').append(rotationIndex).append(' ').append(seed).append(' ')
|
||||
.append(Long.toHexString(hash)).append(' ').append(placer.writes).append('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
String dumpPath = System.getenv("IRIS_OBJECT_GOLDEN_DUMP");
|
||||
if (dumpPath != null && !dumpPath.isBlank()) {
|
||||
Files.writeString(Path.of(dumpPath), dump.toString());
|
||||
}
|
||||
assertEquals(Long.toHexString(GOLDEN), Long.toHexString(combined));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void markedPlacementsMatchTheRecordedBlockStream() {
|
||||
ResourceLoader<IrisMarker> markers = mock(ResourceLoader.class, withSettings().stubOnly());
|
||||
IrisMarker anywhere = new IrisMarker();
|
||||
anywhere.setEmptyAbove(false);
|
||||
when(markers.load("anywhere")).thenReturn(anywhere);
|
||||
when(markers.load("open")).thenReturn(new IrisMarker());
|
||||
when(objectData.getMarkerLoader()).thenReturn(markers);
|
||||
Random random = new Random(77L);
|
||||
List<IrisObjectRotation> rotations = rotations();
|
||||
long combined = 23L;
|
||||
int markerWrites = 0;
|
||||
for (int index = 0; index < 40; index++) {
|
||||
IrisObject object = randomObject(random, index);
|
||||
object.setLoader(objectData);
|
||||
IrisObjectPlacement base = new IrisObjectPlacement();
|
||||
base.setMode(ObjectPlaceMode.STRUCTURE_PIECE);
|
||||
base.setRequireSurfaceSupport(false);
|
||||
IrisObjectMarker loose = new IrisObjectMarker();
|
||||
loose.setMarker("anywhere");
|
||||
loose.setMaximumMarkers(1 + random.nextInt(6));
|
||||
loose.setMark(new KList<>(new IrisBlockData("minecraft:stone"), new IrisBlockData("minecraft:oak_leaves")));
|
||||
IrisObjectMarker exact = new IrisObjectMarker();
|
||||
exact.setMarker("open");
|
||||
exact.setExact(true);
|
||||
exact.setMark(new KList<>(new IrisBlockData("minecraft:oak_log[axis=y]")));
|
||||
base.setMarkers(new KList<>(loose, exact));
|
||||
for (int rotationIndex = 0; rotationIndex < rotations.size(); rotationIndex++) {
|
||||
IrisObjectPlacement placement = base.toPlacement("marked");
|
||||
placement.setRotation(rotations.get(rotationIndex));
|
||||
for (int seed = 0; seed < 3; seed++) {
|
||||
RNG rng = new RNG(index * 31L + rotationIndex * 7L + seed);
|
||||
GoldenPlacer placer = new GoldenPlacer(engine, false);
|
||||
int result = object.place(100 + index, 80 + seed, 200 - index, placer, placement, rng, null, null, data);
|
||||
long hash = mix(placer.hash, result);
|
||||
hash = mix(hash, rng.nextLong());
|
||||
combined = mix(combined, hash);
|
||||
markerWrites += placer.markerWrites;
|
||||
}
|
||||
}
|
||||
}
|
||||
assertTrue(markerWrites > 100);
|
||||
assertEquals(Long.toHexString(MARKER_GOLDEN), Long.toHexString(combined));
|
||||
}
|
||||
|
||||
private static long mix(long hash, long value) {
|
||||
long mixed = (hash ^ value) * 0x9E3779B97F4A7C15L;
|
||||
return mixed ^ (mixed >>> 29);
|
||||
}
|
||||
|
||||
private List<IrisObjectRotation> rotations() {
|
||||
IrisObjectRotation defaultY = new IrisObjectRotation();
|
||||
IrisObjectRotation lockedQuarter = IrisObjectRotation.of(0D, 90D, 0D);
|
||||
IrisObjectRotation flipped = rotation(new IrisAxisRotationClamp(true, true, 180D, 180D, 0D),
|
||||
new IrisAxisRotationClamp(true, false, 0D, 0D, 90D), new IrisAxisRotationClamp());
|
||||
IrisObjectRotation angled = rotation(new IrisAxisRotationClamp(true, false, 0D, 0D, 90D),
|
||||
new IrisAxisRotationClamp(true, false, 0D, 0D, 45D), new IrisAxisRotationClamp(true, false, 0D, 0D, 90D));
|
||||
return List.of(defaultY, lockedQuarter, flipped, angled);
|
||||
}
|
||||
|
||||
private IrisObjectPlacement randomPlacement(Random random) {
|
||||
IrisObjectPlacement placement = new IrisObjectPlacement();
|
||||
List<ObjectPlaceMode> modes = List.of(ObjectPlaceMode.CENTER_HEIGHT, ObjectPlaceMode.CENTER_HEIGHT,
|
||||
ObjectPlaceMode.CENTER_HEIGHT, ObjectPlaceMode.STILT, ObjectPlaceMode.FAST_STILT,
|
||||
ObjectPlaceMode.CENTER_STILT, ObjectPlaceMode.ERODE_STILT, ObjectPlaceMode.ORGANIC_STILT,
|
||||
ObjectPlaceMode.CEILING_HANG, ObjectPlaceMode.MAX_HEIGHT, ObjectPlaceMode.FAST_MAX_HEIGHT,
|
||||
ObjectPlaceMode.MIN_HEIGHT, ObjectPlaceMode.PAINT, ObjectPlaceMode.VACUUM, ObjectPlaceMode.VACUUM_ORGANIC,
|
||||
ObjectPlaceMode.STRUCTURE_PIECE, ObjectPlaceMode.FLOATING, ObjectPlaceMode.FAST_MIN_STILT);
|
||||
placement.setMode(modes.get(random.nextInt(modes.size())));
|
||||
if (random.nextInt(4) == 0) {
|
||||
placement.setTranslate(new IrisObjectTranslate()
|
||||
.setX(random.nextInt(7) - 3)
|
||||
.setY(random.nextInt(5) - 2)
|
||||
.setZ(random.nextInt(7) - 3)
|
||||
.setYRandom(random.nextInt(3) - 1));
|
||||
}
|
||||
if (random.nextInt(6) == 0) {
|
||||
placement.setWarp(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).setMultiplier(1D + random.nextInt(4)));
|
||||
}
|
||||
if (random.nextInt(3) == 0) {
|
||||
placement.setEdit(randomEdits(random));
|
||||
}
|
||||
if (random.nextInt(4) == 0) {
|
||||
placement.setStiltSettings(new IrisStiltSettings());
|
||||
}
|
||||
if (random.nextInt(5) == 0) {
|
||||
placement.setVacuumSettings(new IrisVacuumSettings().setRadius(1 + random.nextInt(4)));
|
||||
}
|
||||
placement.setWaterloggable(random.nextInt(4) == 0);
|
||||
placement.setUnderwater(random.nextInt(8) == 0);
|
||||
placement.setOnwater(random.nextInt(10) == 0);
|
||||
placement.setMeld(random.nextInt(8) == 0);
|
||||
placement.setBore(random.nextInt(10) == 0);
|
||||
placement.setForcePlace(random.nextInt(3) == 0);
|
||||
placement.setBottom(random.nextInt(6) == 0);
|
||||
placement.setSnow(random.nextInt(5) == 0 ? 0.7D : 0D);
|
||||
placement.setRequireSurfaceSupport(random.nextInt(3) != 0);
|
||||
placement.setSurfaceSupportBuffer(random.nextInt(3));
|
||||
placement.setSurfaceSupportDepth(1 + random.nextInt(2));
|
||||
placement.setCarvingSupport(switch (random.nextInt(5)) {
|
||||
case 0 -> CarvingMode.ANYWHERE;
|
||||
case 1 -> CarvingMode.CARVING_ONLY;
|
||||
default -> CarvingMode.SURFACE_ONLY;
|
||||
});
|
||||
return placement;
|
||||
}
|
||||
|
||||
private KList<IrisObjectReplace> randomEdits(Random random) {
|
||||
KList<IrisObjectReplace> edits = new KList<>();
|
||||
int count = 1 + random.nextInt(3);
|
||||
for (int index = 0; index < count; index++) {
|
||||
IrisObjectReplace replace = new IrisObjectReplace();
|
||||
switch (random.nextInt(4)) {
|
||||
case 0 -> {
|
||||
replace.setFind(new KList<>(new IrisBlockData("minecraft:oak_leaves")));
|
||||
replace.setReplace(palette("minecraft:cobblestone"));
|
||||
}
|
||||
case 1 -> {
|
||||
replace.setFind(new KList<>(new IrisBlockData("minecraft:oak_stairs")));
|
||||
replace.setReplace(palette("minecraft:oak_stairs[half=top]"));
|
||||
}
|
||||
case 2 -> {
|
||||
replace.setExact(true);
|
||||
replace.setFind(new KList<>(new IrisBlockData("minecraft:stone")));
|
||||
replace.setReplace(palette("minecraft:glass"));
|
||||
}
|
||||
default -> {
|
||||
replace.setFind(new KList<>(new IrisBlockData("minecraft:grass_block"), new IrisBlockData("minecraft:dirt")));
|
||||
replace.setReplace(palette("minecraft:oak_leaves[distance=2,persistent=false,waterlogged=false]"));
|
||||
}
|
||||
}
|
||||
replace.setChance(switch (random.nextInt(3)) {
|
||||
case 0 -> 1F;
|
||||
case 1 -> 0.5F;
|
||||
default -> 0.15F;
|
||||
});
|
||||
edits.add(replace);
|
||||
}
|
||||
return edits;
|
||||
}
|
||||
|
||||
private static IrisMaterialPalette palette(String key) {
|
||||
IrisMaterialPalette palette = new IrisMaterialPalette();
|
||||
palette.setPalette(new KList<>(new IrisBlockData(key)));
|
||||
return palette;
|
||||
}
|
||||
|
||||
private IrisObject randomObject(Random random, int index) {
|
||||
int width = 1 + random.nextInt(9);
|
||||
int height = 1 + random.nextInt(9);
|
||||
int depth = 1 + random.nextInt(9);
|
||||
IrisObject object = new IrisObject(width, height, depth);
|
||||
int count = 1 + random.nextInt(Math.max(1, (width * height * depth) / 2));
|
||||
List<IrisBlockVector> placed = new ArrayList<>();
|
||||
for (int block = 0; block < count; block++) {
|
||||
int x = random.nextInt(width) - (width / 2);
|
||||
int y = random.nextInt(height) - (height / 2);
|
||||
int z = random.nextInt(depth) - (depth / 2);
|
||||
IrisBlockVector position = new IrisBlockVector(x, y, z);
|
||||
String key = PALETTE.get(random.nextInt(PALETTE.size()));
|
||||
object.getBlocks().put(position, state(key));
|
||||
placed.add(position);
|
||||
if (key.startsWith("minecraft:chest") && random.nextBoolean()) {
|
||||
object.getStates().put(position, new TileData("minecraft:chest", new KMap<>()));
|
||||
}
|
||||
}
|
||||
if (index % 4 == 1 && !placed.isEmpty()) {
|
||||
List<TreeGrounding.Root> roots = new ArrayList<>();
|
||||
Set<TreeBlockCanvas.Vec> canopy = new HashSet<>();
|
||||
for (IrisBlockVector position : placed) {
|
||||
if (random.nextInt(3) == 0) {
|
||||
canopy.add(new TreeBlockCanvas.Vec(position.getBlockX(), position.getBlockY(), position.getBlockZ()));
|
||||
}
|
||||
}
|
||||
IrisBlockVector root = placed.get(0);
|
||||
roots.add(new TreeGrounding.Root(root.getBlockX(), -(height / 2), root.getBlockZ(), state("minecraft:oak_log[axis=y]")));
|
||||
object.setTreeGrounding(TreeGrounding.create(roots, 4, canopy));
|
||||
}
|
||||
return object;
|
||||
}
|
||||
|
||||
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 GoldenState state(String raw) {
|
||||
String key = raw.toLowerCase();
|
||||
if (key.indexOf(':') < 0) {
|
||||
key = "minecraft:" + key;
|
||||
}
|
||||
return states.computeIfAbsent(key, GoldenState::new);
|
||||
}
|
||||
|
||||
private NativeBlockState mergeStates(NativeBlockState base, NativeBlockState update) {
|
||||
GoldenState left = (GoldenState) base;
|
||||
GoldenState right = (GoldenState) update;
|
||||
if (!left.material.equals(right.material)) {
|
||||
return update;
|
||||
}
|
||||
LinkedHashMap<String, String> merged = new LinkedHashMap<>(left.properties);
|
||||
merged.putAll(right.properties);
|
||||
return state(GoldenState.compose(left.material, merged));
|
||||
}
|
||||
|
||||
private static NativeBlockState rotateState(IrisObjectRotation rotation, NativeBlockState state, int spinX, int spinY, int spinZ) {
|
||||
if (state == null || !rotation.canRotate()) {
|
||||
return state;
|
||||
}
|
||||
GoldenState golden = (GoldenState) state;
|
||||
String facing = golden.properties.get("facing");
|
||||
if (facing != null) {
|
||||
IrisBlockVector vector = switch (facing) {
|
||||
case "north" -> new IrisBlockVector(0, 0, -1);
|
||||
case "south" -> new IrisBlockVector(0, 0, 1);
|
||||
case "east" -> new IrisBlockVector(1, 0, 0);
|
||||
case "west" -> new IrisBlockVector(-1, 0, 0);
|
||||
case "up" -> new IrisBlockVector(0, 1, 0);
|
||||
default -> new IrisBlockVector(0, -1, 0);
|
||||
};
|
||||
IrisBlockVector rotated = rotation.rotate(vector, spinX, spinY, spinZ);
|
||||
String face = faceName(rotated);
|
||||
if (face.equals("up") || face.equals("down")) {
|
||||
return golden.material.endsWith("_stairs") ? null : state;
|
||||
}
|
||||
return face.equals(facing) ? state : golden.with("facing", face);
|
||||
}
|
||||
String axis = golden.properties.get("axis");
|
||||
if (axis != null) {
|
||||
IrisBlockVector vector = switch (axis) {
|
||||
case "x" -> new IrisBlockVector(1, 0, 0);
|
||||
case "y" -> new IrisBlockVector(0, 1, 0);
|
||||
default -> new IrisBlockVector(0, 0, 1);
|
||||
};
|
||||
IrisBlockVector rotated = rotation.rotate(vector, spinX, spinY, spinZ);
|
||||
double ax = Math.abs(rotated.getX());
|
||||
double ay = Math.abs(rotated.getY());
|
||||
double az = Math.abs(rotated.getZ());
|
||||
String result = ax >= ay && ax >= az ? "x" : ay >= az ? "y" : "z";
|
||||
return result.equals(axis) ? state : golden.with("axis", result);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
private static String faceName(IrisBlockVector vector) {
|
||||
double ax = Math.abs(vector.getX());
|
||||
double ay = Math.abs(vector.getY());
|
||||
double az = Math.abs(vector.getZ());
|
||||
if (ay >= ax && ay >= az) {
|
||||
return vector.getY() > 0 ? "up" : "down";
|
||||
}
|
||||
if (ax >= az) {
|
||||
return vector.getX() > 0 ? "east" : "west";
|
||||
}
|
||||
return vector.getZ() > 0 ? "south" : "north";
|
||||
}
|
||||
|
||||
private final class GoldenState implements NativeBlockState {
|
||||
private final String key;
|
||||
private final String material;
|
||||
private final LinkedHashMap<String, String> properties;
|
||||
|
||||
private GoldenState(String key) {
|
||||
this.key = key;
|
||||
int bracket = key.indexOf('[');
|
||||
this.material = bracket < 0 ? key : key.substring(0, bracket);
|
||||
this.properties = new LinkedHashMap<>();
|
||||
if (bracket >= 0) {
|
||||
for (String part : key.substring(bracket + 1, key.length() - 1).split(",")) {
|
||||
int split = part.indexOf('=');
|
||||
properties.put(part.substring(0, split), part.substring(split + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String compose(String material, Map<String, String> properties) {
|
||||
if (properties.isEmpty()) {
|
||||
return material;
|
||||
}
|
||||
StringBuilder builder = new StringBuilder(material).append('[');
|
||||
boolean first = true;
|
||||
for (Map.Entry<String, String> entry : properties.entrySet()) {
|
||||
if (!first) {
|
||||
builder.append(',');
|
||||
}
|
||||
builder.append(entry.getKey()).append('=').append(entry.getValue());
|
||||
first = false;
|
||||
}
|
||||
return builder.append(']').toString();
|
||||
}
|
||||
|
||||
private GoldenState with(String name, String value) {
|
||||
LinkedHashMap<String, String> updated = new LinkedHashMap<>(properties);
|
||||
updated.put(name, value);
|
||||
return state(compose(material, updated));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String key() {
|
||||
return key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String namespace() {
|
||||
return "minecraft";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAir() {
|
||||
return material.endsWith("air");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSolid() {
|
||||
return !isAir() && !isFluid() && !isVineBlock() && !material.equals("minecraft:lantern");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOccluding() {
|
||||
return material.equals("minecraft:stone") || material.equals("minecraft:dirt")
|
||||
|| material.equals("minecraft:grass_block") || material.equals("minecraft:oak_log")
|
||||
|| material.equals("minecraft:cobblestone") || material.equals("minecraft:oak_slab");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCustom() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFluid() {
|
||||
return material.equals("minecraft:water");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWater() {
|
||||
return isFluid();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWaterLogged() {
|
||||
return "true".equals(properties.get("waterlogged"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLit() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUpdatable() {
|
||||
return material.endsWith("_stairs");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFoliage() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTreeBlock() {
|
||||
return material.endsWith("_log") || material.endsWith("_leaves");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFoliagePlantable() {
|
||||
return material.equals("minecraft:grass_block") || material.equals("minecraft:dirt");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDecorant() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isStorage() {
|
||||
return material.equals("minecraft:chest");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isStorageChest() {
|
||||
return isStorage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOre() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDeepSlate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isVineBlock() {
|
||||
return material.equals("minecraft:vine");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canPlaceOnto(NativeBlockState onto) {
|
||||
return onto != null && onto.isSolid();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(NativeBlockState state) {
|
||||
if (!(state instanceof GoldenState other) || !other.material.equals(material)) {
|
||||
return false;
|
||||
}
|
||||
for (Map.Entry<String, String> entry : other.properties.entrySet()) {
|
||||
if (!entry.getValue().equals(properties.get(entry.getKey()))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasTileEntity() {
|
||||
return isStorage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativeBlockState withProperty(String name, String value) {
|
||||
return with(name, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object nativeHandle() {
|
||||
return key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
private final class GoldenPlacer implements IObjectPlacer {
|
||||
private final Engine engine;
|
||||
private final boolean preventDecay;
|
||||
private final Map<Long, NativeBlockState> blocks = new HashMap<>();
|
||||
private final Map<Long, Map<Class<?>, Object>> extra = new HashMap<>();
|
||||
private long hash = 31L;
|
||||
private int writes;
|
||||
private int markerWrites;
|
||||
|
||||
private GoldenPlacer(Engine engine, boolean preventDecay) {
|
||||
this.engine = engine;
|
||||
this.preventDecay = preventDecay;
|
||||
}
|
||||
|
||||
private static long key(int x, int y, int z) {
|
||||
return (((long) x & 0x3FFFFFL) << 42) | (((long) y & 0xFFFFFL) << 22) | ((long) z & 0x3FFFFFL);
|
||||
}
|
||||
|
||||
private int surface(int x, int z) {
|
||||
return 64 + Math.floorMod(x * 31 + z * 17, 9) - 4;
|
||||
}
|
||||
|
||||
private void record(String kind, int x, int y, int z, String value) {
|
||||
writes++;
|
||||
hash = mix(hash, kind.hashCode());
|
||||
hash = mix(hash, x);
|
||||
hash = mix(hash, y);
|
||||
hash = mix(hash, z);
|
||||
hash = mix(hash, value.hashCode());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHighest(int x, int z, IrisData data) {
|
||||
return surface(x, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) {
|
||||
return ignoreFluid ? surface(x, z) : Math.max(surface(x, z), 62);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void set(int x, int y, int z, NativeBlockState d) {
|
||||
record("set", x, y, z, d == null ? "null" : d.key());
|
||||
blocks.put(key(x, y, z), d);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativeBlockState get(int x, int y, int z) {
|
||||
NativeBlockState written = blocks.get(key(x, y, z));
|
||||
if (written != null) {
|
||||
return written;
|
||||
}
|
||||
int surface = surface(x, z);
|
||||
if (y <= surface) {
|
||||
return state(y == surface ? "minecraft:grass_block[snowy=false]" : "minecraft:stone");
|
||||
}
|
||||
if (y <= 62) {
|
||||
return state("minecraft:water");
|
||||
}
|
||||
return Math.floorMod(x + y + z, 13) == 0 ? state("minecraft:oak_leaves[distance=1,persistent=true,waterlogged=true]") : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPreventingDecay() {
|
||||
return preventDecay;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCarved(int x, int y, int z) {
|
||||
return Math.floorMod(x * 7 + y * 3 + z * 11, 29) == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSolid(int x, int y, int z) {
|
||||
NativeBlockState state = get(x, y, z);
|
||||
return state != null && state.isSolid();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUnderwater(int x, int z) {
|
||||
return surface(x, z) < 62;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFluidHeight() {
|
||||
return 62;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFluidHeight(int x, int z) {
|
||||
return 62;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDebugSmartBore() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTile(int xx, int yy, int zz, TileData tile) {
|
||||
record("tile", xx, yy, zz, tile == null ? "null" : "tile");
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> void setData(int xx, int yy, int zz, T value) {
|
||||
record("data", xx, yy, zz, value == null ? "null" : value.getClass().getSimpleName() + ":" + value);
|
||||
if (value instanceof MatterMarker) {
|
||||
markerWrites++;
|
||||
}
|
||||
extra.computeIfAbsent(key(xx, yy, zz), ignored -> new HashMap<>()).put(value == null ? Object.class : value.getClass(), value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T getData(int xx, int yy, int zz, Class<T> type) {
|
||||
Map<Class<?>, Object> values = extra.get(key(xx, yy, zz));
|
||||
Object value = values == null ? null : values.get(type);
|
||||
return value == null ? null : type.cast(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean beginTreeCanopy(BlockPosition position) {
|
||||
boolean open = IObjectPlacer.super.beginTreeCanopy(position);
|
||||
record("canopy", position.getX(), position.getY(), position.getZ(), Boolean.toString(open));
|
||||
return open;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Engine getEngine() {
|
||||
return engine;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user