mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 22:10:18 +00:00
Added cave cell kernel
- Decide whole 8x8 adaptive cells as solid or open from lattice corner bounds with an ulp guard - Evaluate only mixed cells per column with the original bilinear arithmetic - Share the row kernel between serial and parallel adaptive classification - Count solid, open and mixed cells and exact rows, printed by the generation probe - Pin carve write sequences for eight carver configurations with golden hashes
This commit is contained in:
1 parent
e2430500cf
commit
7c4a5e1937
6 files changed
+998
-4
No files matched your search
@@ -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);
|
||||
}
|
||||
}
|
||||
+370
@@ -0,0 +1,370 @@
|
||||
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 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 enum Scenario {
|
||||
WARP_MODULES,
|
||||
WARP_TERRAIN,
|
||||
STYLED_BLEND,
|
||||
FLUID_BOUNDARY,
|
||||
NO_WARP_MODULES,
|
||||
VERTICAL_STEP,
|
||||
LATTICE,
|
||||
FLOOR_FLUID_LAYERED;
|
||||
|
||||
IrisCaveProfile[] profiles() {
|
||||
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 -> 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 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user