mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 14:00:26 +00:00
Fixing a handfull of issues!
This commit is contained in:
1 parent
1132242c58
commit
c3359e0a16
30 files changed
+1324
-282
No files matched your search
@@ -316,6 +316,11 @@ public class IrisCaveCarver3D {
|
||||
for (int lz = 0; lz < 16; lz++) {
|
||||
int z = z0 + lz;
|
||||
int index = PowerOfTwoCoordinates.packLocal16(lx, lz);
|
||||
clampedWeights[index] = clampColumnWeight(columnWeights[index]);
|
||||
if (clampedWeights[index] <= resolvedMinWeight) {
|
||||
columnMaxY[index] = minY - 1;
|
||||
continue;
|
||||
}
|
||||
int columnSurfaceY;
|
||||
if (precomputedSurfaceHeights != null && precomputedSurfaceHeights.length > index) {
|
||||
columnSurfaceY = precomputedSurfaceHeights[index];
|
||||
@@ -340,7 +345,6 @@ public class IrisCaveCarver3D {
|
||||
columnThreshold[index] = (thresholdDensity == null
|
||||
? constantThreshold
|
||||
: thresholdDensity.fitDouble(thresholdMin, thresholdMax, x, z)) - thresholdBias;
|
||||
clampedWeights[index] = clampColumnWeight(columnWeights[index]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,6 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
||||
private static final int FIELD_SIZE = CHUNK_SIZE + (BLEND_RADIUS * 2);
|
||||
private static final double MIN_WEIGHT = 0.08D;
|
||||
private static final double THRESHOLD_PENALTY = 0.24D;
|
||||
private static final int MAX_BLENDED_PROFILE_PASSES = 2;
|
||||
private static final int MAX_POOLED_WEIGHT_BUFFERS = 64;
|
||||
/** Lattice columns kept for neighbouring chunks: about 3 KB each on land, roughly 50 MB when full. */
|
||||
private static final int DENSITY_GRID_COLUMNS = 16_384;
|
||||
@@ -348,9 +347,9 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
||||
columnWeightedProfiles.add(new WeightedProfile(profile, weights, averageWeight, null, columnWeightedProfiles.size()));
|
||||
}
|
||||
|
||||
List<WeightedProfile> blendedProfiles = limitAndMergeBlendedProfiles(columnWeightedProfiles, MAX_BLENDED_PROFILE_PASSES, CHUNK_AREA);
|
||||
columnWeightedProfiles.sort(MantleCarvingComponent::compareByCarveOrder);
|
||||
List<WeightedProfile> resolvedProfiles = resolveDimensionCarvingProfiles(chunkX, chunkZ, resolverState, blendScratch);
|
||||
resolvedProfiles.addAll(blendedProfiles);
|
||||
resolvedProfiles.addAll(columnWeightedProfiles);
|
||||
return resolvedProfiles;
|
||||
}
|
||||
|
||||
@@ -619,67 +618,6 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
||||
return surfaceHeights;
|
||||
}
|
||||
|
||||
private static List<WeightedProfile> limitAndMergeBlendedProfiles(List<WeightedProfile> blendedProfiles, int maxProfiles) {
|
||||
return limitAndMergeBlendedProfiles(blendedProfiles, maxProfiles, CHUNK_AREA);
|
||||
}
|
||||
|
||||
private static List<WeightedProfile> limitAndMergeBlendedProfiles(List<WeightedProfile> blendedProfiles, int maxProfiles, int areaSize) {
|
||||
if (blendedProfiles == null || blendedProfiles.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
int clampedLimit = Math.max(1, maxProfiles);
|
||||
List<WeightedProfile> rankedProfiles = new ArrayList<>(blendedProfiles);
|
||||
rankedProfiles.sort(MantleCarvingComponent::compareBySelectionRank);
|
||||
List<WeightedProfile> keptProfiles = new ArrayList<>();
|
||||
int keptCount = Math.min(clampedLimit, rankedProfiles.size());
|
||||
for (int index = 0; index < keptCount; index++) {
|
||||
keptProfiles.add(rankedProfiles.get(index));
|
||||
}
|
||||
|
||||
if (rankedProfiles.size() > keptCount) {
|
||||
for (int columnIndex = 0; columnIndex < areaSize; columnIndex++) {
|
||||
int dominantIndex = 0;
|
||||
double dominantWeight = Double.NEGATIVE_INFINITY;
|
||||
for (int keptIndex = 0; keptIndex < keptProfiles.size(); keptIndex++) {
|
||||
double keptWeight = keptProfiles.get(keptIndex).columnWeights[columnIndex];
|
||||
if (keptWeight > dominantWeight) {
|
||||
dominantWeight = keptWeight;
|
||||
dominantIndex = keptIndex;
|
||||
}
|
||||
}
|
||||
|
||||
double droppedWeight = 0D;
|
||||
for (int droppedIndex = keptCount; droppedIndex < rankedProfiles.size(); droppedIndex++) {
|
||||
droppedWeight += rankedProfiles.get(droppedIndex).columnWeights[columnIndex];
|
||||
}
|
||||
if (droppedWeight <= 0D) {
|
||||
continue;
|
||||
}
|
||||
|
||||
WeightedProfile dominantProfile = keptProfiles.get(dominantIndex);
|
||||
double mergedWeight = dominantProfile.columnWeights[columnIndex] + droppedWeight;
|
||||
dominantProfile.columnWeights[columnIndex] = clampWeight(mergedWeight);
|
||||
}
|
||||
}
|
||||
|
||||
List<WeightedProfile> mergedProfiles = new ArrayList<>();
|
||||
for (WeightedProfile keptProfile : keptProfiles) {
|
||||
double averageWeight = computeAverageWeight(keptProfile.columnWeights, areaSize);
|
||||
mergedProfiles.add(new WeightedProfile(keptProfile.profile, keptProfile.columnWeights, averageWeight, keptProfile.worldYRange, keptProfile.sequence));
|
||||
}
|
||||
mergedProfiles.sort(MantleCarvingComponent::compareByCarveOrder);
|
||||
return mergedProfiles;
|
||||
}
|
||||
|
||||
private static int compareBySelectionRank(WeightedProfile a, WeightedProfile b) {
|
||||
int weightOrder = Double.compare(b.averageWeight, a.averageWeight);
|
||||
if (weightOrder != 0) {
|
||||
return weightOrder;
|
||||
}
|
||||
return Integer.compare(a.sequence, b.sequence);
|
||||
}
|
||||
|
||||
private static int compareByCarveOrder(WeightedProfile a, WeightedProfile b) {
|
||||
int weightOrder = Double.compare(a.averageWeight, b.averageWeight);
|
||||
if (weightOrder != 0) {
|
||||
@@ -689,10 +627,6 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
||||
}
|
||||
|
||||
private static double computeAverageWeight(double[] weights) {
|
||||
return computeAverageWeight(weights, CHUNK_AREA);
|
||||
}
|
||||
|
||||
private static double computeAverageWeight(double[] weights, int areaSize) {
|
||||
if (weights == null || weights.length == 0) {
|
||||
return 0D;
|
||||
}
|
||||
@@ -700,7 +634,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
||||
for (double weight : weights) {
|
||||
sum += weight;
|
||||
}
|
||||
return sum / Math.max(1, areaSize);
|
||||
return sum / CHUNK_AREA;
|
||||
}
|
||||
|
||||
private static double clampWeight(double value) {
|
||||
@@ -730,10 +664,6 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
||||
this.worldYRange = worldYRange;
|
||||
this.sequence = sequence;
|
||||
}
|
||||
|
||||
private double averageWeight() {
|
||||
return averageWeight;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class BlendScratch {
|
||||
|
||||
@@ -930,7 +930,7 @@ public class IrisEngine implements Engine {
|
||||
queryMaximumX,
|
||||
queryMaximumZ);
|
||||
for (NativeStructureVolume volume : resolved) {
|
||||
if (!volume.intersectsRect(minimumX, minimumZ, maximumX, maximumZ)) {
|
||||
if (!volume.intersectsInfluenceRect(minimumX, minimumZ, maximumX, maximumZ)) {
|
||||
continue;
|
||||
}
|
||||
if (matches == null) {
|
||||
|
||||
+2
-2
@@ -115,7 +115,7 @@ public final class NativeStructureVolumeIndex {
|
||||
for (int chunkX = fromChunkX; chunkX <= toChunkX; chunkX++) {
|
||||
for (int chunkZ = fromChunkZ; chunkZ <= toChunkZ; chunkZ++) {
|
||||
for (NativeStructureVolume volume : chunkVolumes(engine, runtimeId, chunkX, chunkZ)) {
|
||||
if (!volume.intersectsRect(minX, minZ, maxX, maxZ)) {
|
||||
if (!volume.intersectsInfluenceRect(minX, minZ, maxX, maxZ)) {
|
||||
continue;
|
||||
}
|
||||
if (matches == null) {
|
||||
@@ -150,7 +150,7 @@ public final class NativeStructureVolumeIndex {
|
||||
for (int originX = chunkX - ORIGIN_REACH_CHUNKS; originX <= chunkX + ORIGIN_REACH_CHUNKS; originX++) {
|
||||
for (int originZ = chunkZ - ORIGIN_REACH_CHUNKS; originZ <= chunkZ + ORIGIN_REACH_CHUNKS; originZ++) {
|
||||
for (NativeStructureVolume volume : scopedOriginVolumes(engine, originX, originZ, pinnedRuntimeId)) {
|
||||
if (!volume.intersectsRect(minX, minZ, maxX, maxZ)) {
|
||||
if (!volume.intersectsInfluenceRect(minX, minZ, maxX, maxZ)) {
|
||||
continue;
|
||||
}
|
||||
if (volumes == null) {
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ public final class NativeStructureVolumeMemo {
|
||||
for (int chunkX = fromChunkX; chunkX <= toChunkX; chunkX++) {
|
||||
for (int chunkZ = fromChunkZ; chunkZ <= toChunkZ; chunkZ++) {
|
||||
for (NativeStructureVolume volume : chunkVolumes(engine, hooks, chunkX, chunkZ)) {
|
||||
if (!volume.intersectsRect(minX, minZ, maxX, maxZ)) {
|
||||
if (!volume.intersectsInfluenceRect(minX, minZ, maxX, maxZ)) {
|
||||
continue;
|
||||
}
|
||||
if (matches == null) {
|
||||
|
||||
@@ -49,6 +49,7 @@ import art.arcane.volmlib.util.math.BlockPosition;
|
||||
import art.arcane.volmlib.util.math.Position2;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.matter.MatterMarker;
|
||||
import it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.Objects;
|
||||
@@ -1135,10 +1136,14 @@ final class IrisObjectPlacementRunner {
|
||||
int envelopeMinY = projection != null ? Integer.MIN_VALUE : worldY + bounds.minY() - warpMargin;
|
||||
int envelopeMaxY = projection != null ? Integer.MAX_VALUE : worldY + bounds.maxY() + warpMargin;
|
||||
boolean envelopeMeetsPiece = false;
|
||||
boolean includesSurfaceCuts = false;
|
||||
for (NativeStructureVolume volume : volumes) {
|
||||
if (volume.intersects(minX, envelopeMinY, minZ, maxX, envelopeMaxY, maxZ)) {
|
||||
envelopeMeetsPiece = true;
|
||||
break;
|
||||
}
|
||||
if (volume.surfaceAdjustment() != null && volume.intersectsInfluenceRect(minX, minZ, maxX, maxZ)) {
|
||||
envelopeMeetsPiece = true;
|
||||
includesSurfaceCuts = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1148,6 +1153,7 @@ final class IrisObjectPlacementRunner {
|
||||
|
||||
self.readLock.lock();
|
||||
try {
|
||||
Long2LongOpenHashMap surfaceCuts = null;
|
||||
VectorMap<NativeBlockState>.Cursor cursor = self.blocks.cursor();
|
||||
while (cursor.next()) {
|
||||
NativeBlockState state = cursor.value();
|
||||
@@ -1187,6 +1193,28 @@ final class IrisObjectPlacementRunner {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (!includesSurfaceCuts) {
|
||||
continue;
|
||||
}
|
||||
if (surfaceCuts == null) {
|
||||
surfaceCuts = new Long2LongOpenHashMap();
|
||||
surfaceCuts.defaultReturnValue(Long.MIN_VALUE);
|
||||
}
|
||||
long column = ((long) xx << 32) ^ (zz & 0xffffffffL);
|
||||
long cut = surfaceCuts.get(column);
|
||||
if (cut == Long.MIN_VALUE) {
|
||||
int surfaceY = Engine.hostHeight(engine, xx, zz, true) + engine.getMinHeight();
|
||||
int targetY = NativeStructureVolume.surfaceCutY(volumes, xx, zz, surfaceY);
|
||||
cut = ((long) surfaceY << 32) ^ (targetY & 0xffffffffL);
|
||||
surfaceCuts.put(column, cut);
|
||||
}
|
||||
int surfaceY = (int) (cut >> 32);
|
||||
int targetY = (int) cut;
|
||||
int margin = projection == null ? warpMargin : 0;
|
||||
if (targetY < surfaceY && worldBlockY > (long) targetY - margin
|
||||
&& worldBlockY <= (long) surfaceY + 1 + margin) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
self.readLock.unlock();
|
||||
|
||||
@@ -124,6 +124,7 @@ final class IrisObjectTransforms {
|
||||
interpolation = IrisObjectPlacementScaleInterpolator.NONE;
|
||||
}
|
||||
boolean savedOrigin = origin == ScaleOrigin.SAVED;
|
||||
boolean fractional = scale != Math.rint(scale);
|
||||
List<ScaledVoxel> voxels;
|
||||
IrisObject output;
|
||||
TreeGrounding grounding;
|
||||
@@ -148,26 +149,33 @@ final class IrisObjectTransforms {
|
||||
sourcePosition.getBlockZ() + sourceCenter.getZ());
|
||||
IrisBlockVector minimum = scaledMinimum(position, scale);
|
||||
IrisBlockVector maximum = scaledMaximum(position, scale);
|
||||
IrisVector corner = fractional ? new IrisVector(position.getX() * scale,
|
||||
position.getY() * scale, position.getZ() * scale) : minimum;
|
||||
if (!savedOrigin) {
|
||||
minimum.subtract(targetCenter);
|
||||
maximum.subtract(targetCenter);
|
||||
if (fractional) {
|
||||
corner.subtract(targetCenter);
|
||||
}
|
||||
}
|
||||
TileData tile = self.states.get(sourcePosition);
|
||||
voxels.add(new ScaledVoxel(sourcePosition.clone(), minimum, maximum, entry.getValue(),
|
||||
voxels.add(new ScaledVoxel(sourcePosition.clone(), minimum, maximum, corner, entry.getValue(),
|
||||
tile == null ? null : tile.clone(), grounding != null && grounding.isCanopy(
|
||||
sourcePosition.getBlockX(), sourcePosition.getBlockY(), sourcePosition.getBlockZ())));
|
||||
}
|
||||
} finally {
|
||||
self.readLock.unlock();
|
||||
}
|
||||
if (scale != Math.rint(scale)) {
|
||||
Set<TreeBlockCanvas.Vec> canopy = grounding == null ? null : new HashSet<>();
|
||||
if (fractional) {
|
||||
voxels.sort(Comparator.comparingInt((ScaledVoxel voxel) -> voxel.source().getBlockX())
|
||||
.thenComparingInt(voxel -> voxel.source().getBlockY())
|
||||
.thenComparingInt(voxel -> voxel.source().getBlockZ()));
|
||||
}
|
||||
Set<TreeBlockCanvas.Vec> canopy = grounding == null ? null : new HashSet<>();
|
||||
for (ScaledVoxel voxel : voxels) {
|
||||
writeScaledVoxel(output, voxel, canopy);
|
||||
writeResampledVoxels(output, voxels, scale, canopy);
|
||||
} else {
|
||||
for (ScaledVoxel voxel : voxels) {
|
||||
writeScaledVoxel(output, voxel, canopy);
|
||||
}
|
||||
}
|
||||
|
||||
VectorMap<NativeBlockState> scaledBlocks = output.blocks;
|
||||
@@ -206,7 +214,7 @@ final class IrisObjectTransforms {
|
||||
for (int x = minimum.getBlockX(); x <= maximum.getBlockX(); x++) {
|
||||
for (int z = minimum.getBlockZ(); z <= maximum.getBlockZ(); z++) {
|
||||
NativeBlockState state = output.blocks.get(new IrisBlockVector(x, y, z));
|
||||
if (state != null) {
|
||||
if (state != null && !canopy.contains(new TreeBlockCanvas.Vec(x, y, z))) {
|
||||
roots.add(new TreeGrounding.Root(x, y, z, state));
|
||||
}
|
||||
}
|
||||
@@ -225,23 +233,53 @@ final class IrisObjectTransforms {
|
||||
position.setZ(z);
|
||||
for (int y = minimum.getBlockY(); y <= maximum.getBlockY(); y++) {
|
||||
position.setY(y);
|
||||
output.blocks.put(position, voxel.block());
|
||||
if (canopy != null) {
|
||||
TreeBlockCanvas.Vec cell = new TreeBlockCanvas.Vec(x, y, z);
|
||||
if (voxel.canopy()) {
|
||||
canopy.add(cell);
|
||||
} else {
|
||||
canopy.remove(cell);
|
||||
writeScaledBlock(output, position, voxel, canopy);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeResampledVoxels(IrisObject output, List<ScaledVoxel> voxels, double scale,
|
||||
Set<TreeBlockCanvas.Vec> canopy) {
|
||||
VectorMap<ScaledVoxel> samples = new VectorMap<>();
|
||||
IrisBlockVector position = new IrisBlockVector();
|
||||
for (ScaledVoxel voxel : voxels) {
|
||||
for (int x = voxel.minimum().getBlockX(); x <= voxel.maximum().getBlockX(); x++) {
|
||||
position.setX(x);
|
||||
for (int z = voxel.minimum().getBlockZ(); z <= voxel.maximum().getBlockZ(); z++) {
|
||||
position.setZ(z);
|
||||
for (int y = voxel.minimum().getBlockY(); y <= voxel.maximum().getBlockY(); y++) {
|
||||
position.setY(y);
|
||||
ScaledVoxel previous = samples.get(position);
|
||||
if (previous == null || voxel.overlap(position, scale) >= previous.overlap(position, scale)) {
|
||||
samples.put(position, voxel);
|
||||
}
|
||||
}
|
||||
if (voxel.tile() == null) {
|
||||
output.states.remove(position);
|
||||
} else {
|
||||
output.states.put(position, voxel.tile().clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (Map.Entry<IrisBlockVector, ScaledVoxel> entry : samples) {
|
||||
writeScaledBlock(output, entry.getKey(), entry.getValue(), canopy);
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeScaledBlock(IrisObject output, IrisBlockVector position, ScaledVoxel voxel,
|
||||
Set<TreeBlockCanvas.Vec> canopy) {
|
||||
output.blocks.put(position, voxel.block());
|
||||
if (canopy != null) {
|
||||
TreeBlockCanvas.Vec cell = new TreeBlockCanvas.Vec(position.getBlockX(),
|
||||
position.getBlockY(), position.getBlockZ());
|
||||
if (voxel.canopy()) {
|
||||
canopy.add(cell);
|
||||
} else {
|
||||
canopy.remove(cell);
|
||||
}
|
||||
}
|
||||
if (voxel.tile() == null) {
|
||||
output.states.remove(position);
|
||||
} else {
|
||||
output.states.put(position, voxel.tile().clone());
|
||||
}
|
||||
}
|
||||
|
||||
private static IrisObject createCenteredScaledObject(IrisObject source, double scale) {
|
||||
@@ -541,7 +579,16 @@ final class IrisObjectTransforms {
|
||||
}
|
||||
|
||||
private record ScaledVoxel(IrisBlockVector source, IrisBlockVector minimum, IrisBlockVector maximum,
|
||||
NativeBlockState block, TileData tile, boolean canopy) {
|
||||
IrisVector corner, NativeBlockState block, TileData tile, boolean canopy) {
|
||||
private double overlap(IrisBlockVector position, double scale) {
|
||||
return overlap(corner.getX(), position.getX(), scale)
|
||||
* overlap(corner.getY(), position.getY(), scale)
|
||||
* overlap(corner.getZ(), position.getZ(), scale);
|
||||
}
|
||||
|
||||
private static double overlap(double source, double target, double scale) {
|
||||
return Math.max(0D, Math.min(source + scale, target + 1D) - Math.max(source, target));
|
||||
}
|
||||
}
|
||||
|
||||
private enum ScaleOrigin {
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
package art.arcane.iris.generation.chunk;
|
||||
|
||||
import art.arcane.iris.platform.bukkit.nms.INMS;
|
||||
import art.arcane.iris.platform.bukkit.nms.INMSBinding;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.generator.ChunkGenerator;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class ChunkDataHunkHolderTest {
|
||||
@@ -90,6 +99,53 @@ public class ChunkDataHunkHolderTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fallbackAppliesExplicitCaveAirBetweenSolidRunsWithNegativeMinimumHeight() {
|
||||
ChunkGenerator.ChunkData chunk = mock(ChunkGenerator.ChunkData.class);
|
||||
when(chunk.getMinHeight()).thenReturn(-256);
|
||||
when(chunk.getMaxHeight()).thenReturn(512);
|
||||
ChunkDataHunkHolder holder = new ChunkDataHunkHolder(chunk);
|
||||
BlockData dirt = mock(BlockData.class);
|
||||
BlockData coal = mock(BlockData.class);
|
||||
BlockData caveAir = mock(BlockData.class);
|
||||
NativeBlockState dirtState = state(dirt);
|
||||
NativeBlockState coalState = state(coal);
|
||||
NativeBlockState caveAirState = state(caveAir);
|
||||
BlockData[] applied = new BlockData[768];
|
||||
Arrays.fill(applied, dirt);
|
||||
for (int y = 0; y <= 75; y++) {
|
||||
holder.setRaw(14, y + 256, 6, y < 33 ? dirtState : y <= 54 ? caveAirState : coalState);
|
||||
}
|
||||
doAnswer(call -> {
|
||||
int y = call.getArgument(1);
|
||||
applied[y + 256] = call.getArgument(3);
|
||||
return null;
|
||||
}).when(chunk).setBlock(anyInt(), anyInt(), anyInt(), any(BlockData.class));
|
||||
doAnswer(call -> {
|
||||
int from = call.getArgument(1);
|
||||
int to = call.getArgument(4);
|
||||
BlockData block = call.getArgument(6);
|
||||
Arrays.fill(applied, from + 256, to + 256, block);
|
||||
return null;
|
||||
}).when(chunk).setRegion(anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), any(BlockData.class));
|
||||
|
||||
try (MockedStatic<INMS> nms = mockStatic(INMS.class)) {
|
||||
nms.when(INMS::get).thenReturn(mock(INMSBinding.class));
|
||||
holder.apply();
|
||||
}
|
||||
|
||||
for (int y = -256; y < 512; y++) {
|
||||
BlockData expected = y >= 33 && y <= 54 ? caveAir : y >= 55 && y <= 75 ? coal : dirt;
|
||||
assertSame("column at " + y, expected, applied[y + 256]);
|
||||
}
|
||||
}
|
||||
|
||||
private static NativeBlockState state(BlockData data) {
|
||||
NativeBlockState state = mock(NativeBlockState.class);
|
||||
when(state.nativeHandle()).thenReturn(data);
|
||||
return state;
|
||||
}
|
||||
|
||||
private static ChunkDataHunkHolder holder(int height) {
|
||||
ChunkGenerator.ChunkData chunk = mock(ChunkGenerator.ChunkData.class);
|
||||
when(chunk.getMinHeight()).thenReturn(0);
|
||||
|
||||
+47
@@ -231,6 +231,53 @@ public class IrisCaveCarver3DNearParityTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sparseProfilesSampleOnlyActiveColumnsAndDiscardPreviousPassColumns() {
|
||||
for (int sampleStep : new int[]{1, 2, 3}) {
|
||||
for (boolean adaptive : new boolean[]{false, true}) {
|
||||
Engine engine = createEngine(128, 110);
|
||||
AtomicInteger heightSamples = new AtomicInteger();
|
||||
doAnswer(invocation -> {
|
||||
heightSamples.incrementAndGet();
|
||||
return 110;
|
||||
}).when(engine).getHeight(anyInt(), anyInt(), anyBoolean());
|
||||
IrisCaveProfile profile = createProfile(false, false)
|
||||
.setSampleStep(sampleStep).setAdaptiveSampling(adaptive)
|
||||
.setAllowFluid(false).setAllowSurfaceBreak(false)
|
||||
.setDensityThreshold(new IrisStyledRange(2D, 2D, new IrisGeneratorStyle(NoiseStyle.FLAT)));
|
||||
IrisCaveCarver3D carver = new IrisCaveCarver3D(engine, profile);
|
||||
double[] weights = new double[256];
|
||||
weights[0] = 1D;
|
||||
weights[255] = 1D;
|
||||
WriterCapture first = createWriterCapture(128);
|
||||
|
||||
carver.carve(first.writer, 0, 0, weights, 0.08D, 0.24D);
|
||||
|
||||
assertEquals(2, heightSamples.get());
|
||||
assertFalse(first.carvedCells.isEmpty());
|
||||
for (String cell : first.carvedCells) {
|
||||
int x = coordinate(cell, 0);
|
||||
int z = coordinate(cell, 2);
|
||||
assertTrue(x == 0 && z == 0 || x == 15 && z == 15);
|
||||
}
|
||||
|
||||
Arrays.fill(weights, 0D);
|
||||
weights[(7 << 4) | 3] = 1D;
|
||||
heightSamples.set(0);
|
||||
WriterCapture second = createWriterCapture(128);
|
||||
|
||||
carver.carve(second.writer, 0, 0, weights, 0.08D, 0.24D);
|
||||
|
||||
assertEquals(1, heightSamples.get());
|
||||
assertFalse(second.carvedCells.isEmpty());
|
||||
for (String cell : second.carvedCells) {
|
||||
assertEquals(7, coordinate(cell, 0));
|
||||
assertEquals(3, coordinate(cell, 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verticalDensityPreservesTwoBlockModuleTunnelsWithoutBleedingAcrossBands() throws Exception {
|
||||
for (int step : new int[]{2, 4, 8}) {
|
||||
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
package art.arcane.iris.generation.mantle;
|
||||
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.cave.IrisCaveProfile;
|
||||
import art.arcane.iris.generation.context.ChunkContext;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.EngineMetrics;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||
import art.arcane.iris.world.IrisWorld;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.stream.ProceduralStream;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.HashMap;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyDouble;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class MantleCarvingComponentJunctionTest {
|
||||
@Test
|
||||
public void twoCaveProfilesBlendAcrossBothSidesOfAChunkBoundary() throws Exception {
|
||||
Fixture fixture = fixture(false);
|
||||
fixture.generate(false);
|
||||
|
||||
assertTrue(fixture.weight(-1, fixture.west(), 15, 12) > 0D);
|
||||
assertTrue(fixture.weight(-1, fixture.east(), 15, 12) > 0D);
|
||||
assertTrue(fixture.weight(0, fixture.west(), 0, 12) > 0D);
|
||||
assertTrue(fixture.weight(0, fixture.east(), 0, 12) > 0D);
|
||||
assertEquals(fixture.weight(-1, fixture.west(), 15, 12),
|
||||
fixture.weight(0, fixture.east(), 0, 12), 0D);
|
||||
assertEquals(fixture.weight(-1, fixture.east(), 15, 12),
|
||||
fixture.weight(0, fixture.west(), 0, 12), 0D);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void thirdProfileOutsideTheBlendRadiusDoesNotChangeBoundaryWeights() throws Exception {
|
||||
Fixture control = fixture(false);
|
||||
Fixture junction = fixture(true);
|
||||
control.generate(false);
|
||||
junction.generate(false);
|
||||
|
||||
assertEquals("A third biome outside the local blend radius must not remove the east cave from the west chunk",
|
||||
control.weight(-1, control.east(), 15, 12),
|
||||
junction.weight(-1, junction.east(), 15, 12), 0D);
|
||||
assertEquals("A third biome outside the local blend radius must not remove the west cave from the east chunk",
|
||||
control.weight(0, control.west(), 0, 12),
|
||||
junction.weight(0, junction.west(), 0, 12), 0D);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void threeWayJunctionRetainsEachProfileOnlyInsideItsLocalBlend() throws Exception {
|
||||
Fixture fixture = fixture(true);
|
||||
fixture.generate(false);
|
||||
|
||||
for (int chunkX : new int[]{-1, 0}) {
|
||||
int boundaryX = chunkX == -1 ? 15 : 0;
|
||||
assertEquals(3, fixture.captured().get(chunkX).size());
|
||||
assertTrue(fixture.weight(chunkX, fixture.west(), boundaryX, 6) > 0D);
|
||||
assertTrue(fixture.weight(chunkX, fixture.east(), boundaryX, 6) > 0D);
|
||||
assertTrue(fixture.weight(chunkX, fixture.north(), boundaryX, 6) > 0D);
|
||||
assertEquals(0D, fixture.weight(chunkX, fixture.north(), boundaryX, 12), 0D);
|
||||
assertEquals(0D, fixture.weight(chunkX, fixture.west(), boundaryX, 0), 0D);
|
||||
assertEquals(0D, fixture.weight(chunkX, fixture.east(), boundaryX, 0), 0D);
|
||||
assertEquals(1D, fixture.weight(chunkX, fixture.north(), boundaryX, 0), 0D);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void junctionWeightsRemainStableAcrossGenerationOrder() throws Exception {
|
||||
Fixture forward = fixture(true);
|
||||
Fixture reverse = fixture(true);
|
||||
forward.generate(false);
|
||||
reverse.generate(true);
|
||||
|
||||
for (int chunkX : new int[]{-1, 0}) {
|
||||
for (int x = 0; x < 16; x++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
assertEquals(forward.weight(chunkX, forward.west(), x, z),
|
||||
reverse.weight(chunkX, reverse.west(), x, z), 0D);
|
||||
assertEquals(forward.weight(chunkX, forward.east(), x, z),
|
||||
reverse.weight(chunkX, reverse.east(), x, z), 0D);
|
||||
assertEquals(forward.weight(chunkX, forward.north(), x, z),
|
||||
reverse.weight(chunkX, reverse.north(), x, z), 0D);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Fixture fixture(boolean thirdProfile) throws ReflectiveOperationException {
|
||||
Engine engine = mock(Engine.class);
|
||||
EngineMantle engineMantle = mock(EngineMantle.class);
|
||||
when(engineMantle.getEngine()).thenReturn(engine);
|
||||
MantleCarvingComponent component = new MantleCarvingComponent(engineMantle);
|
||||
when(engine.getDimension()).thenReturn(new IrisDimension());
|
||||
when(engine.getWorld()).thenReturn(IrisWorld.builder().minHeight(0).maxHeight(128).build());
|
||||
when(engine.getMetrics()).thenReturn(new EngineMetrics(16));
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
when(complex.allowsMantleChunkWrite(anyInt(), anyInt())).thenReturn(true);
|
||||
ProceduralStream<Double> heights = mock(ProceduralStream.class);
|
||||
when(heights.getDouble(anyDouble(), anyDouble())).thenReturn(100D);
|
||||
when(complex.getNaturalHeightStream()).thenReturn(heights);
|
||||
ProceduralStream<IrisRegion> regions = mock(ProceduralStream.class);
|
||||
ProceduralStream<IrisBiome> surfaces = mock(ProceduralStream.class);
|
||||
ProceduralStream<IrisBiome> caves = mock(ProceduralStream.class);
|
||||
ProceduralStream<NativeBlockState> fluids = mock(ProceduralStream.class);
|
||||
when(complex.getRegionStream()).thenReturn(regions);
|
||||
when(complex.getNaturalTrueBiomeStream()).thenReturn(surfaces);
|
||||
when(complex.getCaveBiomeStream()).thenReturn(caves);
|
||||
when(complex.getFluidStream()).thenReturn(fluids);
|
||||
IrisCaveProfile west = new IrisCaveProfile().setEnabled(true);
|
||||
IrisCaveProfile east = new IrisCaveProfile().setEnabled(true);
|
||||
IrisCaveProfile north = new IrisCaveProfile().setEnabled(true);
|
||||
IrisBiome westBiome = new IrisBiome().setCaveProfile(west);
|
||||
IrisBiome eastBiome = new IrisBiome().setCaveProfile(east);
|
||||
IrisBiome northBiome = new IrisBiome().setCaveProfile(north);
|
||||
when(caves.get(anyDouble(), anyDouble())).thenAnswer(call -> {
|
||||
double x = call.getArgument(0);
|
||||
double z = call.getArgument(1);
|
||||
return thirdProfile && z < 7D ? northBiome : x < 0D ? westBiome : eastBiome;
|
||||
});
|
||||
ChunkContext context = mock(ChunkContext.class);
|
||||
when(context.getComplex()).thenReturn(complex);
|
||||
MantleWriter writer = mock(MantleWriter.class);
|
||||
Mantle<Matter> storage = mock(Mantle.class);
|
||||
when(storage.getWorldHeight()).thenReturn(128);
|
||||
when(writer.getMantle()).thenReturn(storage);
|
||||
Map<Integer, Map<IrisCaveProfile, double[]>> captured = new HashMap<>();
|
||||
Map<IrisCaveProfile, IrisCaveCarver3D> carvers = new IdentityHashMap<>();
|
||||
for (IrisCaveProfile profile : new IrisCaveProfile[]{west, east, north}) {
|
||||
IrisCaveCarver3D carver = mock(IrisCaveCarver3D.class);
|
||||
doAnswer(call -> {
|
||||
int chunkX = call.getArgument(1);
|
||||
double[] weights = call.getArgument(3);
|
||||
captured.computeIfAbsent(chunkX, ignored -> new IdentityHashMap<>()).put(profile, weights.clone());
|
||||
return 0;
|
||||
}).when(carver).carve(any(), anyInt(), anyInt(), any(), anyDouble(), anyDouble(),
|
||||
any(), any(), any(), any(), any());
|
||||
carvers.put(profile, carver);
|
||||
}
|
||||
Field profileCarvers = MantleCarvingComponent.class.getDeclaredField("profileCarvers");
|
||||
profileCarvers.setAccessible(true);
|
||||
profileCarvers.set(component, carvers);
|
||||
return new Fixture(component, context, writer, west, east, north, captured);
|
||||
}
|
||||
|
||||
private record Fixture(MantleCarvingComponent component, ChunkContext context, MantleWriter writer,
|
||||
IrisCaveProfile west, IrisCaveProfile east, IrisCaveProfile north,
|
||||
Map<Integer, Map<IrisCaveProfile, double[]>> captured) {
|
||||
private void generate(boolean reverse) {
|
||||
component.generateLayer(writer, reverse ? 0 : -1, 0, context);
|
||||
component.generateLayer(writer, reverse ? -1 : 0, 0, context);
|
||||
}
|
||||
|
||||
private double weight(int chunkX, IrisCaveProfile profile, int x, int z) {
|
||||
double[] weights = captured.get(chunkX).get(profile);
|
||||
return weights == null ? 0D : weights[(x << 4) | z];
|
||||
}
|
||||
}
|
||||
}
|
||||
-162
@@ -1,162 +0,0 @@
|
||||
package art.arcane.iris.generation.mantle;
|
||||
|
||||
import art.arcane.iris.generation.cave.IrisCaveProfile;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class MantleCarvingComponentTop2BlendTest {
|
||||
private static Constructor<?> weightedProfileConstructor;
|
||||
private static Method limitMethod;
|
||||
private static Field profileField;
|
||||
private static Field columnWeightsField;
|
||||
|
||||
@BeforeClass
|
||||
public static void setup() throws Exception {
|
||||
Class<?> weightedProfileClass = Class.forName("art.arcane.iris.generation.mantle.MantleCarvingComponent$WeightedProfile");
|
||||
weightedProfileConstructor = weightedProfileClass.getDeclaredConstructor(IrisCaveProfile.class, double[].class, double.class, Class.forName("art.arcane.iris.pack.value.IrisRange"), int.class);
|
||||
weightedProfileConstructor.setAccessible(true);
|
||||
limitMethod = MantleCarvingComponent.class.getDeclaredMethod("limitAndMergeBlendedProfiles", List.class, int.class, int.class);
|
||||
limitMethod.setAccessible(true);
|
||||
profileField = weightedProfileClass.getDeclaredField("profile");
|
||||
profileField.setAccessible(true);
|
||||
columnWeightsField = weightedProfileClass.getDeclaredField("columnWeights");
|
||||
columnWeightsField.setAccessible(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void topTwoProfilesAreKeptAndDroppedWeightsAreMergedIntoDominantColumns() throws Exception {
|
||||
WeightedInput input = createWeightedProfiles();
|
||||
List<?> limited = invokeLimit(input.weightedProfiles(), 2);
|
||||
assertEquals(2, limited.size());
|
||||
|
||||
Map<IrisCaveProfile, double[]> byProfile = extractWeightsByProfile(limited);
|
||||
IrisCaveProfile first = input.profiles().first();
|
||||
IrisCaveProfile second = input.profiles().second();
|
||||
|
||||
assertEquals(1.0D, byProfile.get(first)[1], 0D);
|
||||
assertEquals(1.0D, byProfile.get(second)[0], 0D);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void topTwoMergeIsDeterministicAcrossRuns() throws Exception {
|
||||
WeightedInput firstInput = createWeightedProfiles();
|
||||
WeightedInput secondInput = createWeightedProfiles();
|
||||
List<?> first = invokeLimit(firstInput.weightedProfiles(), 2);
|
||||
List<?> second = invokeLimit(secondInput.weightedProfiles(), 2);
|
||||
|
||||
Map<IrisCaveProfile, double[]> firstByProfile = extractWeightsByProfile(first);
|
||||
Map<IrisCaveProfile, double[]> secondByProfile = extractWeightsByProfile(second);
|
||||
|
||||
assertEquals(firstByProfile.get(firstInput.profiles().first())[0], secondByProfile.get(secondInput.profiles().first())[0], 0D);
|
||||
assertEquals(firstByProfile.get(firstInput.profiles().first())[1], secondByProfile.get(secondInput.profiles().first())[1], 0D);
|
||||
assertEquals(firstByProfile.get(firstInput.profiles().second())[0], secondByProfile.get(secondInput.profiles().second())[0], 0D);
|
||||
assertEquals(firstByProfile.get(firstInput.profiles().second())[1], secondByProfile.get(secondInput.profiles().second())[1], 0D);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void topTwoMergePreservesIndependentWeightsAtHighColumnIndexes() throws Exception {
|
||||
WeightedInput input = createWeightedProfiles();
|
||||
List<?> limited = invokeLimit(input.weightedProfiles(), 2);
|
||||
|
||||
Map<IrisCaveProfile, double[]> byProfile = extractWeightsByProfile(limited);
|
||||
IrisCaveProfile first = input.profiles().first();
|
||||
IrisCaveProfile second = input.profiles().second();
|
||||
|
||||
assertEquals(1.0D, byProfile.get(first)[255], 0D);
|
||||
assertEquals(1.0D, byProfile.get(second)[254], 0D);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void topTwoTieBreaksBySequenceNotIdentity() throws Exception {
|
||||
IrisCaveProfile first = new IrisCaveProfile().setEnabled(true).setBaseWeight(1.0D);
|
||||
IrisCaveProfile second = new IrisCaveProfile().setEnabled(true).setBaseWeight(1.0D);
|
||||
IrisCaveProfile third = new IrisCaveProfile().setEnabled(true).setBaseWeight(1.0D);
|
||||
|
||||
double[] weightsA = new double[256];
|
||||
double[] weightsB = new double[256];
|
||||
double[] weightsC = new double[256];
|
||||
weightsA[0] = 0.5D;
|
||||
weightsB[0] = 0.5D;
|
||||
weightsC[0] = 0.5D;
|
||||
|
||||
List<Object> weighted = new ArrayList<>();
|
||||
weighted.add(weightedProfileConstructor.newInstance(first, weightsA, average(weightsA), null, 0));
|
||||
weighted.add(weightedProfileConstructor.newInstance(second, weightsB, average(weightsB), null, 1));
|
||||
weighted.add(weightedProfileConstructor.newInstance(third, weightsC, average(weightsC), null, 2));
|
||||
|
||||
List<?> limited = invokeLimit(weighted, 2);
|
||||
assertEquals(2, limited.size());
|
||||
|
||||
Map<IrisCaveProfile, double[]> byProfile = extractWeightsByProfile(limited);
|
||||
assertEquals(true, byProfile.containsKey(first));
|
||||
assertEquals(true, byProfile.containsKey(second));
|
||||
assertEquals(false, byProfile.containsKey(third));
|
||||
}
|
||||
|
||||
private WeightedInput createWeightedProfiles() throws Exception {
|
||||
IrisCaveProfile first = new IrisCaveProfile().setEnabled(true).setBaseWeight(1.31D);
|
||||
IrisCaveProfile second = new IrisCaveProfile().setEnabled(true).setBaseWeight(1.17D);
|
||||
IrisCaveProfile third = new IrisCaveProfile().setEnabled(true).setBaseWeight(0.93D);
|
||||
Profiles profiles = new Profiles(first, second, third);
|
||||
|
||||
double[] firstWeights = new double[256];
|
||||
firstWeights[0] = 0.2D;
|
||||
firstWeights[1] = 0.8D;
|
||||
firstWeights[255] = 0.6D;
|
||||
|
||||
double[] secondWeights = new double[256];
|
||||
secondWeights[0] = 0.7D;
|
||||
secondWeights[1] = 0.1D;
|
||||
secondWeights[254] = 0.7D;
|
||||
|
||||
double[] thirdWeights = new double[256];
|
||||
thirdWeights[0] = 0.3D;
|
||||
thirdWeights[1] = 0.4D;
|
||||
thirdWeights[254] = 0.3D;
|
||||
thirdWeights[255] = 0.4D;
|
||||
|
||||
List<Object> weighted = new ArrayList<>();
|
||||
weighted.add(weightedProfileConstructor.newInstance(first, firstWeights, average(firstWeights), null, 0));
|
||||
weighted.add(weightedProfileConstructor.newInstance(second, secondWeights, average(secondWeights), null, 1));
|
||||
weighted.add(weightedProfileConstructor.newInstance(third, thirdWeights, average(thirdWeights), null, 2));
|
||||
return new WeightedInput(weighted, profiles);
|
||||
}
|
||||
|
||||
private List<?> invokeLimit(List<Object> weightedProfiles, int limit) throws Exception {
|
||||
return (List<?>) limitMethod.invoke(null, weightedProfiles, limit, 256);
|
||||
}
|
||||
|
||||
private Map<IrisCaveProfile, double[]> extractWeightsByProfile(List<?> weightedProfiles) throws Exception {
|
||||
Map<IrisCaveProfile, double[]> byProfile = new IdentityHashMap<>();
|
||||
for (Object weightedProfile : weightedProfiles) {
|
||||
IrisCaveProfile profile = (IrisCaveProfile) profileField.get(weightedProfile);
|
||||
double[] weights = (double[]) columnWeightsField.get(weightedProfile);
|
||||
byProfile.put(profile, weights);
|
||||
}
|
||||
return byProfile;
|
||||
}
|
||||
|
||||
private double average(double[] weights) {
|
||||
double total = 0D;
|
||||
for (double weight : weights) {
|
||||
total += weight;
|
||||
}
|
||||
return total / weights.length;
|
||||
}
|
||||
|
||||
private record Profiles(IrisCaveProfile first, IrisCaveProfile second, IrisCaveProfile third) {
|
||||
}
|
||||
|
||||
private record WeightedInput(List<Object> weightedProfiles, Profiles profiles) {
|
||||
}
|
||||
}
|
||||
+81
-1
@@ -13,6 +13,7 @@ import art.arcane.iris.structure.object.ObjectPlaceMode;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.mantle.EngineMantle;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.iris.testsupport.PlatformBinding;
|
||||
import art.arcane.volmlib.util.math.Vector3i;
|
||||
@@ -30,7 +31,10 @@ import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class NativeStructureObjectVetoTest {
|
||||
@@ -52,6 +56,9 @@ public class NativeStructureObjectVetoTest {
|
||||
when(complex.getHeightStream()).thenReturn(heightStream);
|
||||
engine = mock(Engine.class);
|
||||
when(engine.getHeight()).thenReturn(256);
|
||||
when(engine.getHeight(anyInt(), anyInt())).thenReturn(SURFACE_Y);
|
||||
when(engine.getHeight(anyInt(), anyInt(), eq(true))).thenAnswer(invocation ->
|
||||
engine.getHeight(invocation.getArgument(0), invocation.getArgument(1)));
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
when(engine.getDimension()).thenReturn(mock(IrisDimension.class));
|
||||
volumes();
|
||||
@@ -67,6 +74,79 @@ public class NativeStructureObjectVetoTest {
|
||||
assertFalse(placer.written().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flattenSkirtRejectsATreeWhoseGroundWillBeLoweredOutsideThePiece() {
|
||||
RecordingPlacer placer = new RecordingPlacer(engine);
|
||||
volumes(new NativeStructureVolume("minecraft:village_plains", 1, 64, -1, 11, 65, 7,
|
||||
new NativeStructureVolume.SurfaceAdjustment(List.of(
|
||||
new NativeStructureVolume.SurfaceAnchor(1, 11, -1, 7, 64, 2)), 24, 64)));
|
||||
|
||||
assertEquals(-1, place(placer));
|
||||
assertTrue(placer.written().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flattenSkirtKeepsATreeWhereTheGradeDoesNotLowerItsGround() {
|
||||
RecordingPlacer placer = new RecordingPlacer(engine);
|
||||
volumes(new NativeStructureVolume("minecraft:village_plains", 1, 96, -1, 11, 97, 7,
|
||||
new NativeStructureVolume.SurfaceAdjustment(List.of(
|
||||
new NativeStructureVolume.SurfaceAnchor(1, 11, -1, 7, 96, 2)), 24, 64)));
|
||||
|
||||
assertTrue(place(placer) >= 0);
|
||||
assertFalse(placer.written().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flattenCutKeepsObjectsBelowTheRemainingCaveRoof() {
|
||||
when(engine.getHeight(anyInt(), anyInt())).thenReturn(140);
|
||||
RecordingPlacer placer = new RecordingPlacer(engine);
|
||||
volumes(new NativeStructureVolume("minecraft:village_plains", 1, 120, -1, 11, 121, 7,
|
||||
new NativeStructureVolume.SurfaceAdjustment(List.of(
|
||||
new NativeStructureVolume.SurfaceAnchor(1, 11, -1, 7, 120, 2)), 24, 64)));
|
||||
|
||||
assertTrue(place(placer) >= 0);
|
||||
assertFalse(placer.written().isEmpty());
|
||||
verify(engine, times(1)).getHeight(0, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flattenCutKeepsObjectsAboveTheRemovedSurfaceBand() {
|
||||
when(engine.getHeight(anyInt(), anyInt())).thenReturn(40);
|
||||
RecordingPlacer placer = new RecordingPlacer(engine);
|
||||
volumes(new NativeStructureVolume("minecraft:village_plains", 1, 20, -1, 11, 21, 7,
|
||||
new NativeStructureVolume.SurfaceAdjustment(List.of(
|
||||
new NativeStructureVolume.SurfaceAnchor(1, 11, -1, 7, 20, 2)), 24, 64)));
|
||||
|
||||
assertTrue(place(placer) >= 0);
|
||||
assertFalse(placer.written().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flattenCutUsesHostGroundBelowAnUpperTerrainLayer() {
|
||||
EngineMantle mantle = mock(EngineMantle.class);
|
||||
when(engine.getMantle()).thenReturn(mantle);
|
||||
when(mantle.trueHeight(anyInt(), anyInt())).thenReturn(40);
|
||||
RecordingPlacer placer = new RecordingPlacer(engine);
|
||||
volumes(new NativeStructureVolume("minecraft:village_plains", 1, 20, -1, 11, 21, 7,
|
||||
new NativeStructureVolume.SurfaceAdjustment(List.of(
|
||||
new NativeStructureVolume.SurfaceAnchor(1, 11, -1, 7, 20, 2)), 24, 64)));
|
||||
|
||||
assertTrue(place(placer) >= 0);
|
||||
assertFalse(placer.written().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flattenCutConvertsTheHostGroundToNegativeWorldCoordinates() {
|
||||
when(engine.getMinHeight()).thenReturn(-256);
|
||||
RecordingPlacer placer = new RecordingPlacer(engine);
|
||||
volumes(new NativeStructureVolume("minecraft:village_plains", 1, -190, -1, 11, -189, 7,
|
||||
new NativeStructureVolume.SurfaceAdjustment(List.of(
|
||||
new NativeStructureVolume.SurfaceAnchor(1, 11, -1, 7, -190, 2)), 24, 64)));
|
||||
|
||||
assertEquals(-1, place(placer));
|
||||
assertTrue(placer.written().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canopyBlockInsideAPieceRejectsTheWholeObject() {
|
||||
int canopyY = plantedTopY();
|
||||
@@ -250,7 +330,7 @@ public class NativeStructureObjectVetoTest {
|
||||
}
|
||||
|
||||
private NativeStructureVolume volume(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
|
||||
return new NativeStructureVolume("minecraft:village_plains", minX, minY, minZ, maxX, maxY, maxZ);
|
||||
return new NativeStructureVolume("minecraft:village_plains", minX, minY, minZ, maxX, maxY, maxZ, null);
|
||||
}
|
||||
|
||||
private static NativeBlockState state(String key, boolean solid) {
|
||||
|
||||
+1
-1
@@ -105,7 +105,7 @@ public class NativeStructureVolumeMemoTest {
|
||||
}
|
||||
|
||||
private static NativeStructureVolume volume(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
|
||||
return new NativeStructureVolume("minecraft:village_swamp", minX, minY, minZ, maxX, maxY, maxZ);
|
||||
return new NativeStructureVolume("minecraft:village_swamp", minX, minY, minZ, maxX, maxY, maxZ, null);
|
||||
}
|
||||
|
||||
private static final class RecordingHooks implements EnginePlatformHooks {
|
||||
|
||||
+46
-1
@@ -4,13 +4,16 @@ import art.arcane.volmlib.nativelib.terrain.structure.NativeStructureVolume;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class NativeStructureVolumeTest {
|
||||
private static final NativeStructureVolume PIECE =
|
||||
new NativeStructureVolume("minecraft:village_plains", 10, 64, 20, 25, 78, 40);
|
||||
new NativeStructureVolume("minecraft:village_plains", 10, 64, 20, 25, 78, 40, null);
|
||||
|
||||
@Test
|
||||
public void rectQueriesIgnoreTheVerticalAxis() {
|
||||
@@ -53,4 +56,46 @@ public class NativeStructureVolumeTest {
|
||||
assertEquals(PIECE.maxY(), normalized.maxY());
|
||||
assertEquals(PIECE.maxZ(), normalized.maxZ());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void terrainInfluenceDoesNotChangePhysicalIntersection() {
|
||||
NativeStructureVolume volume = adjustedPoint(0, 90, 0, 24);
|
||||
assertTrue(volume.intersectsInfluenceRect(-1, 0, -1, 0));
|
||||
assertFalse(volume.intersects(-1, 40, 0, -1, 160, 0));
|
||||
assertFalse(volume.intersectsRect(-1, 0, -1, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void oppositeGradesBlendWithoutCuttingTheSurface() {
|
||||
List<NativeStructureVolume> volumes = List.of(adjustedPoint(0, 90, 0, 20), adjustedPoint(20, 110, 0, 20));
|
||||
assertEquals(100, NativeStructureVolume.surfaceCutY(volumes, 10, 0, 100));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void halfBlockCutsRoundTheirSignedDepthLikeTheFitter() {
|
||||
assertEquals(99, NativeStructureVolume.surfaceCutY(List.of(adjustedPoint(0, 99, 0, 24)), 12, 0, 100));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void realVillageJunctionsLowerTheReportedPineGround() {
|
||||
int[][] junctions = {{-2591,144,-7062},{-2591,144,-7061},{-2593,141,-7055},{-2592,141,-7055},
|
||||
{-2591,163,-7070},{-2591,163,-7071},{-2608,149,-7075},{-2609,149,-7075},
|
||||
{-2591,136,-7051},{-2591,137,-7051},{-2602,126,-7047},{-2591,136,-7050},
|
||||
{-2591,137,-7050},{-2602,126,-7046},{-2602,122,-7042},{-2602,122,-7041},
|
||||
{-2624,124,-7067},{-2624,124,-7068}};
|
||||
List<NativeStructureVolume.SurfaceAnchor> anchors = new ArrayList<>();
|
||||
for (int[] junction : junctions) {
|
||||
anchors.add(new NativeStructureVolume.SurfaceAnchor(junction[0], junction[0], junction[2], junction[2], junction[1], 1));
|
||||
}
|
||||
NativeStructureVolume volume = new NativeStructureVolume("minecraft:village_plains", -2600, 144, -7070, -2590, 145, -7062,
|
||||
new NativeStructureVolume.SurfaceAdjustment(anchors, 24, 64));
|
||||
assertEquals(151, NativeStructureVolume.surfaceCutY(List.of(volume), -2601, -7063, 162));
|
||||
assertEquals(162, NativeStructureVolume.surfaceCutY(List.of(volume), -2700, -7063, 162));
|
||||
}
|
||||
|
||||
private static NativeStructureVolume adjustedPoint(int x, int groundY, int z, int padding) {
|
||||
return new NativeStructureVolume("minecraft:village_plains", x, groundY, z, x, groundY, z,
|
||||
new NativeStructureVolume.SurfaceAdjustment(List.of(
|
||||
new NativeStructureVolume.SurfaceAnchor(x, x, z, z, groundY, 1)), padding, 64));
|
||||
}
|
||||
}
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
package art.arcane.iris.structure.object;
|
||||
|
||||
import art.arcane.iris.generation.block.TileData;
|
||||
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.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.math.Vector3i;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class IrisObjectFractionalScalingTest {
|
||||
@Test
|
||||
public void fractionalReductionRetainsBothLayersWhenTwoOutputVoxelsRemain() {
|
||||
NativeBlockState stem = state("minecraft:mushroom_stem");
|
||||
NativeBlockState cap = state("minecraft:red_mushroom_block");
|
||||
IrisObject source = layers(stem, cap);
|
||||
for (boolean savedOrigin : new boolean[]{false, true}) {
|
||||
for (double factor : new double[]{0.6D, 0.75D, 0.9D}) {
|
||||
IrisObject scaled = scale(source, factor, savedOrigin);
|
||||
|
||||
assertEquals(2, scaled.getH());
|
||||
assertEquals(2, scaled.getBlocks().size());
|
||||
assertSame(stem, scaled.getBlocks().get(scaled.getSigned(0, 0, 0)));
|
||||
assertSame(cap, scaled.getBlocks().get(scaled.getSigned(0, 1, 0)));
|
||||
}
|
||||
}
|
||||
assertSame(stem, source.getBlocks().get(source.getSigned(0, 0, 0)));
|
||||
assertSame(cap, source.getBlocks().get(source.getSigned(0, 1, 0)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fractionalEnlargementUsesTheDominantMaterialInSharedOutputVoxels() {
|
||||
NativeBlockState lower = state("minecraft:stone");
|
||||
NativeBlockState upper = state("minecraft:dirt");
|
||||
IrisObject source = layers(lower, upper);
|
||||
for (boolean savedOrigin : new boolean[]{false, true}) {
|
||||
IrisObject scaled = scale(source, 1.75D, savedOrigin);
|
||||
|
||||
assertEquals(4, scaled.getH());
|
||||
for (int y = 0; y < 4; y++) {
|
||||
assertSame(y < 2 ? lower : upper, scaled.getBlocks().get(scaled.getSigned(0, y, 0)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fractionalReductionResolvesMaterialCoverageAcrossAllThreeAxes() {
|
||||
IrisObject source = new IrisObject(2, 2, 2);
|
||||
for (int x = 0; x < 2; x++) {
|
||||
for (int y = 0; y < 2; y++) {
|
||||
for (int z = 0; z < 2; z++) {
|
||||
source.setUnsigned(x, y, z, state("test:voxel_" + x + y + z));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IrisObject scaled = source.scaled(0.75D, IrisObjectPlacementScaleInterpolator.NONE);
|
||||
|
||||
assertEquals(8, scaled.getBlocks().size());
|
||||
for (int x = 0; x < 2; x++) {
|
||||
for (int y = 0; y < 2; y++) {
|
||||
for (int z = 0; z < 2; z++) {
|
||||
assertSame(source.getBlocks().get(source.getSigned(x, y, z)),
|
||||
scaled.getBlocks().get(scaled.getSigned(x, y, z)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fractionalSavedOriginPreservesNegativeCoordinatesAndTheirTiles() {
|
||||
NativeBlockState lower = state("minecraft:chest");
|
||||
NativeBlockState upper = state("minecraft:barrel");
|
||||
IrisObject source = new IrisObject(1, 4, 1);
|
||||
source.setCenter(new Vector3i(0, 3, 0));
|
||||
source.setUnsigned(0, 1, 0, lower);
|
||||
source.setUnsignedTile(0, 1, 0, tile("minecraft:chest", "lower"));
|
||||
source.setUnsigned(0, 2, 0, upper);
|
||||
source.setUnsignedTile(0, 2, 0, tile("minecraft:barrel", "upper"));
|
||||
|
||||
IrisObject scaled = source.scaledAroundOrigin(0.75D, IrisObjectPlacementScaleInterpolator.NONE);
|
||||
|
||||
assertEquals(new Vector3i(0, 3, 0), scaled.getCenter());
|
||||
assertEquals(-3, scaled.getAABB().min().getY());
|
||||
assertEquals(0, scaled.getAABB().max().getY());
|
||||
assertSame(lower, scaled.getBlocks().get(new IrisBlockVector(0, -2, 0)));
|
||||
assertSame(upper, scaled.getBlocks().get(new IrisBlockVector(0, -1, 0)));
|
||||
assertEquals("lower", scaled.getStates().get(new IrisBlockVector(0, -2, 0)).getProperties().get("name"));
|
||||
assertEquals("upper", scaled.getStates().get(new IrisBlockVector(0, -1, 0)).getProperties().get("name"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sameMaterialTilesFollowTheirDominantSourceVoxel() {
|
||||
NativeBlockState chest = state("minecraft:chest");
|
||||
IrisObject source = layers(chest, chest);
|
||||
TileData lower = tile("minecraft:chest", "lower");
|
||||
TileData upper = tile("minecraft:chest", "upper");
|
||||
source.setUnsignedTile(0, 0, 0, lower);
|
||||
source.setUnsignedTile(0, 1, 0, upper);
|
||||
|
||||
IrisObject scaled = source.scaled(0.75D, IrisObjectPlacementScaleInterpolator.NONE);
|
||||
|
||||
TileData scaledLower = scaled.getStates().get(scaled.getSigned(0, 0, 0));
|
||||
TileData scaledUpper = scaled.getStates().get(scaled.getSigned(0, 1, 0));
|
||||
assertEquals(lower, scaledLower);
|
||||
assertEquals(upper, scaledUpper);
|
||||
assertNotSame(lower, scaledLower);
|
||||
assertNotSame(lower.getProperties(), scaledLower.getProperties());
|
||||
assertNotSame(upper, scaledUpper);
|
||||
assertNotSame(scaledLower.getProperties(), scaledUpper.getProperties());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sameMaterialCanopyRolesAndRootContactsFollowTheDominantSourceVoxel() {
|
||||
NativeBlockState wood = state("minecraft:spruce_wood");
|
||||
IrisObject source = layers(wood, wood);
|
||||
TreeGrounding grounding = TreeGrounding.create(List.of(new TreeGrounding.Root(0, 0, 0, wood)),
|
||||
8, Set.of(new TreeBlockCanvas.Vec(0, 1, 0)));
|
||||
source.setTreeGrounding(grounding);
|
||||
for (boolean savedOrigin : new boolean[]{false, true}) {
|
||||
IrisObject scaled = scale(source, 0.75D, savedOrigin);
|
||||
int bottom = -scaled.getCenter().getY();
|
||||
TreeGrounding result = scaled.getTreeGrounding();
|
||||
|
||||
assertEquals(List.of(new TreeGrounding.Root(0, bottom, 0, wood)), result.roots());
|
||||
assertFalse(result.isCanopy(0, bottom, 0));
|
||||
assertTrue(result.isCanopy(0, bottom + 1, 0));
|
||||
assertEquals(6, result.maxExtension());
|
||||
}
|
||||
assertSame(grounding, source.getTreeGrounding());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canopyWinningAnUnresolvableRootCollisionDoesNotBecomeAGroundContact() {
|
||||
NativeBlockState wood = state("minecraft:spruce_wood");
|
||||
IrisObject source = layers(wood, wood);
|
||||
source.setTreeGrounding(TreeGrounding.create(List.of(new TreeGrounding.Root(0, 0, 0, wood)),
|
||||
8, Set.of(new TreeBlockCanvas.Vec(0, 1, 0))));
|
||||
|
||||
IrisObject scaled = source.scaled(0.5D, IrisObjectPlacementScaleInterpolator.NONE);
|
||||
|
||||
assertEquals(1, scaled.getBlocks().size());
|
||||
assertTrue(scaled.getTreeGrounding().roots().isEmpty());
|
||||
assertTrue(scaled.getTreeGrounding().isCanopy(0, 0, 0));
|
||||
}
|
||||
|
||||
private static IrisObject layers(NativeBlockState lower, NativeBlockState upper) {
|
||||
IrisObject source = new IrisObject(1, 2, 1);
|
||||
source.setCenter(new Vector3i(0, 0, 0));
|
||||
source.setUnsigned(0, 0, 0, lower);
|
||||
source.setUnsigned(0, 1, 0, upper);
|
||||
return source;
|
||||
}
|
||||
|
||||
private static IrisObject scale(IrisObject source, double scale, boolean savedOrigin) {
|
||||
return savedOrigin ? source.scaledAroundOrigin(scale, IrisObjectPlacementScaleInterpolator.NONE)
|
||||
: source.scaled(scale, IrisObjectPlacementScaleInterpolator.NONE);
|
||||
}
|
||||
|
||||
private static TileData tile(String material, String name) {
|
||||
KMap<String, Object> properties = new KMap<>();
|
||||
properties.put("name", name);
|
||||
return new TileData(material, properties);
|
||||
}
|
||||
|
||||
private static NativeBlockState state(String key) {
|
||||
NativeBlockState state = mock(NativeBlockState.class);
|
||||
when(state.key()).thenReturn(key);
|
||||
when(state.materialKey()).thenReturn(key);
|
||||
return state;
|
||||
}
|
||||
}
|
||||
+85
@@ -620,6 +620,91 @@ public class IrisObjectPlacementRunnerRegressionTest {
|
||||
assertSame(machine, placer.get(0, ANCHOR_Y + 9, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void organicMushroomStiltsUseTheStemAndStopAtTheCaveFloor() {
|
||||
NativeBlockState stem = state("minecraft:mushroom_stem", true);
|
||||
NativeBlockState cap = state("minecraft:red_mushroom_block", true);
|
||||
IrisObject mushroom = new IrisObject(1, 2, 1);
|
||||
mushroom.setUnsigned(0, 0, 0, stem);
|
||||
mushroom.setUnsigned(0, 1, 0, cap);
|
||||
RecordingPlacer placer = new RecordingPlacer(null);
|
||||
placer.terrain(0, 0, 0, 30, 120, 140);
|
||||
placer.set(0, 30, 0, solid);
|
||||
placer.writes().clear();
|
||||
IrisObjectPlacement placement = placement().setMode(ObjectPlaceMode.ORGANIC_STILT)
|
||||
.setStiltSettings(new IrisStiltSettings().setOrganicJitter(0).setOrganicScratch(0));
|
||||
|
||||
mushroom.place(0, 60, 0, placer, placement, new RNG(2L), data);
|
||||
|
||||
assertEquals(1, placer.writesOf(cap).size());
|
||||
for (int y = 31; y < 60; y++) {
|
||||
assertSame(stem, placer.get(0, y, 0));
|
||||
}
|
||||
assertSame(solid, placer.get(0, 30, 0));
|
||||
assertTrue(placer.writes().stream().allMatch(write -> write.y() >= 31));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultOrganicStiltsDoNotReachAFloorMoreThanOneHundredBlocksBelow() {
|
||||
NativeBlockState stem = state("minecraft:mushroom_stem", true);
|
||||
IrisObject mushroom = new IrisObject(1, 1, 1);
|
||||
mushroom.setUnsigned(0, 0, 0, stem);
|
||||
RecordingPlacer placer = new RecordingPlacer(null);
|
||||
placer.set(0, 30, 0, solid);
|
||||
placer.writes().clear();
|
||||
IrisObjectPlacement placement = placement().setMode(ObjectPlaceMode.ORGANIC_STILT)
|
||||
.setStiltSettings(new IrisStiltSettings().setOrganicJitter(0).setOrganicScratch(0));
|
||||
|
||||
mushroom.place(0, 160, 0, placer, placement, new RNG(2L), data);
|
||||
|
||||
assertEquals(9, placer.world.values().stream().filter(block -> block == stem).count());
|
||||
assertTrue(placer.writesOf(stem).stream().allMatch(write -> write.y() >= 152));
|
||||
assertNull(placer.get(0, 151, 0));
|
||||
assertSame(solid, placer.get(0, 30, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shrinkingTwoBlockMushroomsPreservesTheStemUsedByOrganicStilts() {
|
||||
NativeBlockState stem = state("minecraft:mushroom_stem", true);
|
||||
NativeBlockState cap = state("minecraft:red_mushroom_block", true);
|
||||
IrisObject mushroom = new IrisObject(1, 2, 1);
|
||||
mushroom.setUnsigned(0, 0, 0, stem);
|
||||
mushroom.setUnsigned(0, 1, 0, cap);
|
||||
IrisObject scaled = mushroom.scaled(0.75, IrisObjectPlacementScaleInterpolator.NONE);
|
||||
RecordingPlacer placer = new RecordingPlacer(null);
|
||||
placer.set(0, ANCHOR_Y, 0, solid);
|
||||
placer.writes().clear();
|
||||
IrisObjectPlacement placement = placement().setMode(ObjectPlaceMode.ORGANIC_STILT)
|
||||
.setStiltSettings(new IrisStiltSettings().setOrganicJitter(0).setOrganicScratch(0));
|
||||
|
||||
scaled.place(0, ANCHOR_Y + 10, 0, placer, placement, new RNG(2L), data);
|
||||
|
||||
assertSame(stem, placer.get(0, ANCHOR_Y + 1, 0));
|
||||
assertEquals(1, placer.writesOf(cap).size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void centerHeightSurfaceSupportAllowsAnImportedRootAboveALowerAdjacentColumn() {
|
||||
NativeBlockState wood = state("minecraft:spruce_wood", true);
|
||||
IrisObject tree = new IrisObject(7, 27, 7);
|
||||
tree.getBlocks().put(new IrisBlockVector(0, -13, 0), wood);
|
||||
tree.getBlocks().put(new IrisBlockVector(1, -13, 0), wood);
|
||||
tree.getBlocks().put(new IrisBlockVector(1, -13, -1), wood);
|
||||
RecordingPlacer placer = new RecordingPlacer(null);
|
||||
placer.terrain(0, 0, 0, 167);
|
||||
placer.terrain(1, 0, 0, 163);
|
||||
placer.terrain(1, -1, 0, 163);
|
||||
IrisObjectPlacement placement = new IrisObjectPlacement()
|
||||
.setRotation(IrisObjectRotation.of(0, 0, 0));
|
||||
|
||||
assertTrue(tree.place(0, -1, 0, placer, placement, new RNG(2L), data) >= 0);
|
||||
|
||||
assertSame(wood, placer.get(1, 167, 0));
|
||||
assertNull(placer.get(1, 166, 0));
|
||||
assertFalse(placer.isSurfaceSolid(1, 166, 0));
|
||||
assertEquals(163, placer.highest(1, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stiltPaletteCannotIntroduceExcludedMaterialsOrContainers() {
|
||||
NativeBlockState calcite = state("minecraft:calcite", true);
|
||||
|
||||
@@ -10,6 +10,8 @@ import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.testsupport.PlatformBinding;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.nativelib.terrain.structure.NativeStructureVolume;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
@@ -84,6 +86,30 @@ public class TreeGroundingPlacementTest {
|
||||
assertFalse(terrain.written.containsKey(new Position(2, 79, 0)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nativeFlattenInfluenceDoesNotBlockRootExtensionsBelowItsPiece() {
|
||||
Terrain terrain = new Terrain();
|
||||
terrain.heights.put(new Column(1, 0), 77);
|
||||
Engine engine = data.getEngine();
|
||||
when(terrain.placer.getEngine()).thenReturn(engine);
|
||||
when(engine.getDimension()).thenReturn(new IrisDimension());
|
||||
when(engine.getHeight(anyInt(), anyInt(), anyBoolean())).thenAnswer(invocation ->
|
||||
terrain.surface(invocation.getArgument(0), invocation.getArgument(1)));
|
||||
NativeStructureVolume volume = new NativeStructureVolume("minecraft:village_plains",
|
||||
10, 100, 0, 12, 110, 2,
|
||||
new NativeStructureVolume.SurfaceAdjustment(List.of(
|
||||
new NativeStructureVolume.SurfaceAnchor(10, 12, 0, 2, 100, 4)), 16, 16));
|
||||
when(engine.getNativeStructureVolumes(anyInt(), anyInt(), anyInt(), anyInt()))
|
||||
.thenReturn(new KList<>(List.of(volume)));
|
||||
|
||||
assertTrue(place(tree(true), terrain, placement()) >= 0);
|
||||
|
||||
for (int y = 77; y <= 81; y++) {
|
||||
assertSame(wood, terrain.written.get(new Position(1, y, 0)));
|
||||
}
|
||||
assertFalse(terrain.written.containsKey(new Position(1, 76, 0)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cliffRejectionWritesNoPartOfTheTreeOrListener() {
|
||||
Terrain terrain = new Terrain();
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package art.arcane.iris.world;
|
||||
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.platform.generation.BukkitChunkGenerator;
|
||||
import art.arcane.iris.world.lifecycle.WorldLifecycleCaller;
|
||||
import art.arcane.iris.world.lifecycle.WorldLifecycleRequest;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.WorldCreator;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.mockito.MockedConstruction;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.Mockito.mockConstruction;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
|
||||
public class IrisWorldCreatorSeedTest {
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
public void explicitSeedsReachTheGeneratorAndLifecycleRequestWithoutDefaultSubstitution() throws Exception {
|
||||
for (long seed : new long[]{0L, 78264193L, -734829104958217364L, Long.MIN_VALUE, Long.MAX_VALUE}) {
|
||||
assertCreatedSeed(new IrisWorldCreator().seed(seed), seed);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void omittedSeedUsesTheCreationDefault() throws Exception {
|
||||
assertCreatedSeed(new IrisWorldCreator(), 1337L);
|
||||
}
|
||||
|
||||
private void assertCreatedSeed(IrisWorldCreator creator, long expectedSeed) throws Exception {
|
||||
NamespacedKey key = new NamespacedKey("iris", "seed-selection");
|
||||
File root = temporaryFolder.newFolder();
|
||||
IrisDimension dimension = new IrisDimension();
|
||||
dimension.setLoadKey("cohesion");
|
||||
WorldCreator bukkitCreator = WorldCreatorCompat.ofKey(key);
|
||||
List<IrisWorld> worlds = new ArrayList<>();
|
||||
try (MockedStatic<IrisWorldStorage> storage = mockStatic(IrisWorldStorage.class);
|
||||
MockedStatic<WorldCreatorCompat> compatibility = mockStatic(WorldCreatorCompat.class);
|
||||
MockedConstruction<BukkitChunkGenerator> generators = mockConstruction(BukkitChunkGenerator.class,
|
||||
(generator, context) -> worlds.add((IrisWorld) context.arguments().getFirst()))) {
|
||||
storage.when(() -> IrisWorldStorage.keyFromName("seed-selection")).thenReturn(key);
|
||||
storage.when(() -> IrisWorldStorage.dimensionRoot(key)).thenReturn(root);
|
||||
compatibility.when(() -> WorldCreatorCompat.ofKey(key)).thenReturn(bukkitCreator);
|
||||
compatibility.when(() -> WorldCreatorCompat.keyOf(bukkitCreator)).thenReturn(key);
|
||||
|
||||
WorldCreator created = creator.name("seed-selection").dimension(dimension).create();
|
||||
WorldLifecycleRequest request = WorldLifecycleRequest.fromCreator(
|
||||
created, false, false, WorldLifecycleCaller.CREATE);
|
||||
|
||||
assertEquals(1, generators.constructed().size());
|
||||
assertEquals(expectedSeed, worlds.getFirst().getRawWorldSeed());
|
||||
assertEquals(expectedSeed, created.seed());
|
||||
assertEquals(expectedSeed, request.seed());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user