mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-19 23:14:10 +00:00
Merged core stability fixes
- Object verification includes stable deposits and featurette fixes
This commit is contained in:
@@ -474,7 +474,7 @@ final class DecoratorCore {
|
||||
&& (!caveSkipFluid || !B.isFluid(lower) && !B.isFluid(upper));
|
||||
}
|
||||
|
||||
private static int computeStack(IrisDecorator decorator, RNG rng, double realX, double realZ,
|
||||
static int computeStack(IrisDecorator decorator, RNG rng, double realX, double realZ,
|
||||
IrisData irisData, int max) {
|
||||
int stack = decorator.getHeight(rng, realX, realZ, irisData);
|
||||
if (decorator.isScaleStack()) {
|
||||
|
||||
@@ -39,6 +39,10 @@ public class IrisSeaFloorDecorator extends IrisEngineDecorator {
|
||||
@Override
|
||||
public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1,
|
||||
Hunk<PlatformBlockState> data, IrisBiome biome, int height, int max) {
|
||||
int limit = Math.min(max, Math.min(getEngine().getHeight(), data.getHeight()));
|
||||
if (height < 0 || height >= limit) {
|
||||
return;
|
||||
}
|
||||
RNG rng = getRNG(realX, realZ);
|
||||
IrisDecorator decorator = DecoratorCore.pickDecorator(biome, getPart(), partRNG, rng, getData(), realX, realZ);
|
||||
|
||||
@@ -51,34 +55,25 @@ public class IrisSeaFloorDecorator extends IrisEngineDecorator {
|
||||
&& !decorator.getSlopeCondition().isValid(getComplex().getSlopeStream().get(realX, realZ))) {
|
||||
return;
|
||||
}
|
||||
if (height >= 0 && height < getEngine().getHeight()) {
|
||||
data.set(x, height, z, decorator.getBlockData100(biome, rng, realX, height, realZ, getData()));
|
||||
PlatformBlockState block = decorator.getBlockData100(biome, rng, realX, height, realZ, getData());
|
||||
if (block != null) {
|
||||
data.set(x, height, z, block);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
int stack = decorator.getHeight(rng, realX, realZ, getData());
|
||||
if (decorator.isScaleStack()) {
|
||||
stack = (int) Math.ceil((double) (max - height) * ((double) stack / 100));
|
||||
} else {
|
||||
stack = Math.min(stack, max - height);
|
||||
}
|
||||
|
||||
if (stack == 1) {
|
||||
data.set(x, height, z, decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData()));
|
||||
return;
|
||||
}
|
||||
|
||||
int engineHeight = getEngine().getHeight();
|
||||
int stack = Math.min(limit - height,
|
||||
DecoratorCore.computeStack(decorator, rng, realX, realZ, getData(), max - height));
|
||||
for (int i = 0; i < stack; i++) {
|
||||
int h = height + i;
|
||||
if (h > max || h > engineHeight) {
|
||||
continue;
|
||||
}
|
||||
double threshold = ((double) i) / (stack - 1);
|
||||
data.set(x, h, z, threshold >= decorator.getTopThreshold()
|
||||
double threshold = stack == 1 ? 1.0 : ((double) i) / (stack - 1);
|
||||
PlatformBlockState block = threshold >= decorator.getTopThreshold()
|
||||
? decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData())
|
||||
: decorator.getBlockData100(biome, rng, realX, h, realZ, getData()));
|
||||
: decorator.getBlockData100(biome, rng, realX, h, realZ, getData());
|
||||
if (block == null) {
|
||||
break;
|
||||
}
|
||||
data.set(x, h, z, block);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,10 @@ public class IrisSeaSurfaceDecorator extends IrisEngineDecorator {
|
||||
@Override
|
||||
public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1,
|
||||
Hunk<PlatformBlockState> data, IrisBiome biome, int height, int max) {
|
||||
int maxBaseY = Math.min(max, Math.min(getEngine().getHeight(), data.getHeight()) - 1);
|
||||
if (height < 0 || height >= maxBaseY) {
|
||||
return;
|
||||
}
|
||||
RNG rng = getRNG(realX, realZ);
|
||||
IrisDecorator decorator = DecoratorCore.pickDecorator(biome, getPart(), partRNG, rng, getData(), realX, realZ);
|
||||
|
||||
@@ -48,8 +52,7 @@ public class IrisSeaSurfaceDecorator extends IrisEngineDecorator {
|
||||
|
||||
if (!decorator.isStacking()) {
|
||||
int targetY = height + 1;
|
||||
if (height >= 0 && targetY < getEngine().getHeight()
|
||||
&& DecoratorCore.canReplaceStackTarget(data.get(x, targetY, z), false)) {
|
||||
if (DecoratorCore.canReplaceStackTarget(data.get(x, targetY, z), false)) {
|
||||
PlatformBlockState block = decorator.getBlockData100(biome, rng, realX, height, realZ, getData());
|
||||
if (block != null) {
|
||||
data.set(x, targetY, z, block);
|
||||
@@ -58,33 +61,15 @@ public class IrisSeaSurfaceDecorator extends IrisEngineDecorator {
|
||||
return;
|
||||
}
|
||||
|
||||
int stack = decorator.getHeight(rng, realX, realZ, getData());
|
||||
if (decorator.isScaleStack()) {
|
||||
stack = (int) Math.ceil((double) (max - height) * ((double) stack / 100));
|
||||
}
|
||||
|
||||
if (stack == 1) {
|
||||
int targetY = height + 1;
|
||||
if (targetY >= data.getHeight() || !DecoratorCore.canReplaceStackTarget(data.get(x, targetY, z), false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
PlatformBlockState block = decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData());
|
||||
if (block != null) {
|
||||
data.set(x, targetY, z, block);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
int engineHeight = getEngine().getHeight();
|
||||
int stack = Math.min(maxBaseY - height,
|
||||
DecoratorCore.computeStack(decorator, rng, realX, realZ, getData(), max - height));
|
||||
for (int i = 0; i < stack; i++) {
|
||||
int h = height + i;
|
||||
int targetY = h + 1;
|
||||
if (h >= max || targetY >= engineHeight
|
||||
|| !DecoratorCore.canReplaceStackTarget(data.get(x, targetY, z), false)) {
|
||||
if (!DecoratorCore.canReplaceStackTarget(data.get(x, targetY, z), false)) {
|
||||
break;
|
||||
}
|
||||
double threshold = ((double) i) / (stack - 1);
|
||||
double threshold = stack == 1 ? 1.0 : ((double) i) / (stack - 1);
|
||||
PlatformBlockState block = threshold >= decorator.getTopThreshold()
|
||||
? decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData())
|
||||
: decorator.getBlockData100(biome, rng, realX, h, realZ, getData());
|
||||
|
||||
@@ -40,11 +40,19 @@ import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.matter.MatterCavern;
|
||||
import art.arcane.iris.util.common.parallel.BurstExecutor;
|
||||
import art.arcane.iris.util.common.math.IrisBlockVector;
|
||||
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.common.data.B;
|
||||
import art.arcane.iris.util.common.data.VectorMap;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockState> {
|
||||
private static final int CLUMPS_PER_BATCH = 8;
|
||||
private static final int PREPARATION_BATCH_COUNT = 4;
|
||||
|
||||
private final RNG rng;
|
||||
|
||||
public IrisDepositModifier(Engine engine) {
|
||||
@@ -62,29 +70,36 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
|
||||
public void generateDeposits(Hunk<PlatformBlockState> terrain, int x, int z, boolean multicore, ChunkContext context) {
|
||||
IrisRegion region = context.getRegion().get(7, 7);
|
||||
IrisBiome biome = context.getBiome().get(7, 7);
|
||||
BurstExecutor burst = burst().burst(multicore);
|
||||
List<IrisDepositGenerator> generators = new ArrayList<>(
|
||||
getDimension().getDeposits().size() + region.getDeposits().size() + biome.getDeposits().size());
|
||||
generators.addAll(getDimension().getDeposits());
|
||||
generators.addAll(region.getDeposits());
|
||||
generators.addAll(biome.getDeposits());
|
||||
if (generators.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
BurstExecutor burst = burst().burst(multicore);
|
||||
long seed = x * 341873128712L + z * 132897987541L;
|
||||
long mask = 0;
|
||||
PreparedDeposit[] prepared = new PreparedDeposit[PREPARATION_BATCH_COUNT];
|
||||
int pending = 0;
|
||||
MantleChunk chunk = getEngine().getMantle().getMantle().getChunk(x, z).use();
|
||||
try {
|
||||
for (IrisDepositGenerator k : getDimension().getDeposits()) {
|
||||
long finalSeed = seed * ++mask;
|
||||
burst.queue(scopedDepositTask(
|
||||
() -> generate(k, chunk, terrain, rng.nextParallelRNG(finalSeed), x, z, false, context), context));
|
||||
}
|
||||
|
||||
for (IrisDepositGenerator k : region.getDeposits()) {
|
||||
long finalSeed = seed * ++mask;
|
||||
burst.queue(scopedDepositTask(
|
||||
() -> generate(k, chunk, terrain, rng.nextParallelRNG(finalSeed), x, z, false, context), context));
|
||||
}
|
||||
|
||||
for (IrisDepositGenerator k : biome.getDeposits()) {
|
||||
long finalSeed = seed * ++mask;
|
||||
burst.queue(scopedDepositTask(
|
||||
() -> generate(k, chunk, terrain, rng.nextParallelRNG(finalSeed), x, z, false, context), context));
|
||||
for (int i = 0; i < generators.size(); i++) {
|
||||
DepositPlan plan = plan(generators.get(i), rng.nextParallelRNG(seed * (i + 1L)));
|
||||
for (int first = 0; first < plan.attempts(); first += CLUMPS_PER_BATCH) {
|
||||
int firstAttempt = first;
|
||||
int limit = Math.min(plan.attempts(), first + CLUMPS_PER_BATCH);
|
||||
int index = pending++;
|
||||
burst.queue(scopedDepositTask(
|
||||
() -> prepared[index] = prepare(plan, firstAttempt, limit, x, z, null, context), context));
|
||||
if (pending == prepared.length) {
|
||||
completeBatches(burst, prepared, chunk, terrain, x, z, context);
|
||||
pending = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
completeBatches(burst, prepared, chunk, terrain, x, z, context);
|
||||
} finally {
|
||||
// complete() must run before release() even when queueing throws — already
|
||||
// submitted burst tasks must never write into a released chunk.
|
||||
@@ -96,6 +111,20 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
|
||||
}
|
||||
}
|
||||
|
||||
private void completeBatches(BurstExecutor burst, PreparedDeposit[] prepared, MantleChunk chunk,
|
||||
Hunk<PlatformBlockState> terrain, int x, int z, ChunkContext context) {
|
||||
burst.complete();
|
||||
try (IrisContext.Scope chunkScope = IrisContext.open(getEngine(), context.getGenerationSessionId(), context)) {
|
||||
for (int i = 0; i < prepared.length; i++) {
|
||||
PreparedDeposit deposit = prepared[i];
|
||||
prepared[i] = null;
|
||||
if (deposit != null) {
|
||||
place(deposit, chunk, terrain, x, z, null, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Runnable scopedDepositTask(Runnable task, ChunkContext context) {
|
||||
IrisEngine generationEngine = getEngine() instanceof IrisEngine irisEngine
|
||||
&& irisEngine.hasGenerationRuntimeScope() ? irisEngine : null;
|
||||
@@ -115,17 +144,36 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
|
||||
}
|
||||
|
||||
public void generate(IrisDepositGenerator k, MantleChunk chunk, Hunk<PlatformBlockState> data, RNG rng, int cx, int cz, boolean safe, HeightMap he, ChunkContext context) {
|
||||
IrisDimensionCarvingResolver.State carvingState = new IrisDimensionCarvingResolver.State();
|
||||
if (k.getSpawnChance() < rng.d())
|
||||
return;
|
||||
DepositPlan plan = plan(k, rng);
|
||||
for (int first = 0; first < plan.attempts(); first += CLUMPS_PER_BATCH) {
|
||||
int limit = Math.min(plan.attempts(), first + CLUMPS_PER_BATCH);
|
||||
place(prepare(plan, first, limit, cx, cz, he, context), chunk, data, cx, cz, he, context);
|
||||
}
|
||||
}
|
||||
|
||||
boolean oreDeposit = k.isOre(getData());
|
||||
private DepositPlan plan(IrisDepositGenerator generator, RNG rng) {
|
||||
if (generator.getSpawnChance() < rng.d()) {
|
||||
return new DepositPlan(generator, rng, false, 0);
|
||||
}
|
||||
boolean ore = generator.isOre(getData());
|
||||
int attempts = rng.i(generator.getMinPerChunk(), generator.getMaxPerChunk() + 1);
|
||||
return new DepositPlan(generator, rng, ore, attempts);
|
||||
}
|
||||
|
||||
for (int l = 0; l < rng.i(k.getMinPerChunk(), k.getMaxPerChunk() + 1); l++) {
|
||||
if (k.getPerClumpSpawnChance() < rng.d())
|
||||
private PreparedDeposit prepare(DepositPlan plan, int first, int limit, int cx, int cz, HeightMap he, ChunkContext context) {
|
||||
IrisDepositGenerator k = plan.generator();
|
||||
RNG rng = plan.rng();
|
||||
boolean oreDeposit = plan.ore();
|
||||
boolean needsCaveBiome = oreDeposit || k.usesCaveBiomeFilter();
|
||||
IrisDimensionCarvingResolver.State carvingState = needsCaveBiome ? new IrisDimensionCarvingResolver.State() : null;
|
||||
List<PreparedClump> clumps = new ArrayList<>(limit - first);
|
||||
for (int l = first; l < limit; l++) {
|
||||
RNG clumpRng = rng.nextParallelRNG(l + 1L);
|
||||
if (k.getPerClumpSpawnChance() < clumpRng.d()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
IrisObject clump = k.getClump(getEngine(), rng, getData());
|
||||
IrisObject clump = k.getClump(getEngine(), clumpRng, getData());
|
||||
|
||||
int dim = clump.getW();
|
||||
int min = dim / 2;
|
||||
@@ -136,8 +184,8 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
|
||||
max = 9;
|
||||
}
|
||||
|
||||
int x = rng.i(min, max + 1);
|
||||
int z = rng.i(min, max + 1);
|
||||
int x = clumpRng.i(min, max + 1);
|
||||
int z = clumpRng.i(min, max + 1);
|
||||
int terrainSurface = getDepositTerrainSurface(cx, cz, x, z, he, context);
|
||||
int height = k.getPlacementScope() == IrisDepositPlacementScope.TERRAIN
|
||||
? depositSurfaceLimit(terrainSurface, k.getSurfaceClearance())
|
||||
@@ -148,19 +196,20 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
|
||||
}
|
||||
|
||||
int y = sampleHeight(
|
||||
k.getHeightDistribution(), rng, k.getMinHeight(), k.getMaxHeight(),
|
||||
k.getHeightDistribution(), clumpRng, k.getMinHeight(), k.getMaxHeight(),
|
||||
Math.min(height, getEngine().getHeight() - 1));
|
||||
if (y == Integer.MIN_VALUE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
boolean clippedHeight = k.getHeightDistribution() == IrisDepositHeightDistribution.CLIPPED_UNIFORM;
|
||||
if (clippedHeight && y > height - 2)
|
||||
if (clippedHeight && y > height - 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int biomeY = Math.max(0, Math.min(getEngine().getHeight() - 1, y));
|
||||
IrisBiome surfaceBiome = context.getBiome().get(x, z);
|
||||
IrisBiome depositBiome = oreDeposit || k.usesCaveBiomeFilter()
|
||||
IrisBiome depositBiome = needsCaveBiome
|
||||
? getEngine().getCaveBiome(
|
||||
(cx << 4) + x, biomeY, (cz << 4) + z, carvingState)
|
||||
: null;
|
||||
@@ -172,13 +221,13 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
|
||||
if (depositBiome != null) {
|
||||
double frequencyMultiplier = depositBiome.getOreDepositFrequencyMultiplier();
|
||||
if (frequencyMultiplier < 1D
|
||||
&& !passesOreFrequency(frequencyMultiplier, rng.d())) {
|
||||
&& !passesOreFrequency(frequencyMultiplier, clumpRng.d())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
double sizeMultiplier = depositBiome.getOreDepositSizeMultiplier();
|
||||
if (sizeMultiplier != 1D) {
|
||||
IrisObject scaledClump = k.getClump(getEngine(), rng, getData(), sizeMultiplier);
|
||||
IrisObject scaledClump = k.getClump(getEngine(), clumpRng, getData(), sizeMultiplier);
|
||||
int scaledDimension = scaledClump.getW();
|
||||
x = clampDepositCenter(x, scaledDimension, 16);
|
||||
if (clippedHeight) {
|
||||
@@ -190,9 +239,30 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
|
||||
}
|
||||
}
|
||||
|
||||
IrisDimension dimension = getDimension();
|
||||
clumps.add(new PreparedClump(clump, x, y, z, clumpRng));
|
||||
}
|
||||
return new PreparedDeposit(k, oreDeposit, clumps);
|
||||
}
|
||||
|
||||
for (art.arcane.iris.util.common.math.IrisBlockVector j : clump.getBlocks().keys()) {
|
||||
private void place(PreparedDeposit deposit, MantleChunk chunk, Hunk<PlatformBlockState> data,
|
||||
int cx, int cz, HeightMap he, ChunkContext context) {
|
||||
if (deposit.clumps().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
IrisDepositGenerator k = deposit.generator();
|
||||
boolean oreDeposit = deposit.ore();
|
||||
IrisDimensionCarvingResolver.State carvingState = new IrisDimensionCarvingResolver.State();
|
||||
IrisDimension dimension = getDimension();
|
||||
for (PreparedClump prepared : deposit.clumps()) {
|
||||
IrisObject clump = prepared.object();
|
||||
int x = prepared.x();
|
||||
int y = prepared.y();
|
||||
int z = prepared.z();
|
||||
RNG rng = prepared.rng();
|
||||
|
||||
VectorMap<PlatformBlockState>.Cursor cursor = clump.getBlocks().cursor();
|
||||
while (cursor.next()) {
|
||||
IrisBlockVector j = cursor.key();
|
||||
int nx = j.getBlockX() + x;
|
||||
int ny = j.getBlockY() + y;
|
||||
int nz = j.getBlockZ() + z;
|
||||
@@ -236,7 +306,7 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
|
||||
}
|
||||
|
||||
if (chunk.get(nx, ny, nz, MatterCavern.class) == null) {
|
||||
PlatformBlockState ore = clump.getBlocks().get(j);
|
||||
PlatformBlockState ore = cursor.value();
|
||||
PlatformBlockState remapped = resolveDepositVariant(
|
||||
cx, cz, nx, ny, nz, ore, dimension, context, carvingState);
|
||||
PlatformBlockState finalBlock = remapped != null
|
||||
@@ -421,4 +491,13 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private record DepositPlan(IrisDepositGenerator generator, RNG rng, boolean ore, int attempts) {
|
||||
}
|
||||
|
||||
private record PreparedDeposit(IrisDepositGenerator generator, boolean ore, List<PreparedClump> clumps) {
|
||||
}
|
||||
|
||||
private record PreparedClump(IrisObject object, int x, int y, int z, RNG rng) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,7 +170,9 @@ public final class CoralGenerator {
|
||||
int y = (int) Math.round(oy + dy * t);
|
||||
int z = (int) Math.round(oz + dz * t - sway);
|
||||
canvas.set(x, y, z, CoralCanvas.Role.STRUCTURE);
|
||||
tip = new double[]{x, y, z};
|
||||
tip[0] = x;
|
||||
tip[1] = y;
|
||||
tip[2] = z;
|
||||
}
|
||||
return tip;
|
||||
}
|
||||
|
||||
@@ -63,11 +63,7 @@ public final class FungusGenerator {
|
||||
|
||||
private static void buildUpright(Map<Vector3i, FungusCellRole> roles, IrisFungus fungus, int height, RNG rng, long baseSeed) {
|
||||
int stemHeight = Math.max(1, height);
|
||||
Map<Vector3i, PlatformBlockState> stemCells = new HashMap<>();
|
||||
double[] top = FungusStemBuilder.build(stemCells, fungus, stemHeight, baseSeed);
|
||||
for (Vector3i v : stemCells.keySet()) {
|
||||
roles.put(v, FungusCellRole.STEM);
|
||||
}
|
||||
double[] top = FungusStemBuilder.build(roles, fungus, stemHeight, baseSeed);
|
||||
|
||||
int radius = pickRadius(fungus, rng);
|
||||
double cx = top[0];
|
||||
|
||||
@@ -20,7 +20,6 @@ package art.arcane.iris.engine.object.fungi;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisFungus;
|
||||
import art.arcane.iris.engine.object.tree.TreeFunctions;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.common.math.Vector3i;
|
||||
|
||||
import java.util.Map;
|
||||
@@ -29,7 +28,7 @@ public final class FungusStemBuilder {
|
||||
private FungusStemBuilder() {
|
||||
}
|
||||
|
||||
public static double[] build(Map<Vector3i, PlatformBlockState> stemCells, IrisFungus fungus, int stemHeight, long seed) {
|
||||
public static double[] build(Map<Vector3i, FungusCellRole> roles, IrisFungus fungus, int stemHeight, long seed) {
|
||||
int width = Math.max(1, Math.min(3, fungus.getStemWidth()));
|
||||
double maxLean = stemHeight * Math.tan(Math.toRadians(Math.max(0.0, fungus.getStemCurve())));
|
||||
double leanRad = Math.toRadians(fungus.getStemLeanAzimuth());
|
||||
@@ -40,6 +39,7 @@ public final class FungusStemBuilder {
|
||||
double waveAzimuth = Math.toRadians(fungus.getStemLeanAzimuth() + 90.0);
|
||||
double waveX = Math.sin(waveAzimuth);
|
||||
double waveZ = Math.cos(waveAzimuth);
|
||||
double wavePhase = TreeFunctions.valueNoise1D(seed, seed) * Math.PI * 2.0;
|
||||
|
||||
double topCx = 0.0;
|
||||
double topCz = 0.0;
|
||||
@@ -48,12 +48,16 @@ public final class FungusStemBuilder {
|
||||
for (int y = 0; y < stemHeight; y++) {
|
||||
double t = stemHeight <= 1 ? 0.0 : y / (double) (stemHeight - 1);
|
||||
double lean = maxLean * (t * t);
|
||||
double wave = waveAmp * Math.sin(2.0 * Math.PI * wavePeriods * t + TreeFunctions.valueNoise1D(seed, seed) * Math.PI * 2.0);
|
||||
double wave = waveAmp * Math.sin(2.0 * Math.PI * wavePeriods * t + wavePhase);
|
||||
double cx = lean * leanX + wave * waveX;
|
||||
double cz = lean * leanZ + wave * waveZ;
|
||||
|
||||
for (int[] xz : squarePositions(cx, cz, width)) {
|
||||
stemCells.put(new Vector3i(xz[0], y, xz[1]), null);
|
||||
int ox = (width % 2 == 1 ? (int) Math.round(cx) : (int) Math.floor(cx) + 1) - width / 2;
|
||||
int oz = (width % 2 == 1 ? (int) Math.round(cz) : (int) Math.floor(cz) + 1) - width / 2;
|
||||
for (int dx = 0; dx < width; dx++) {
|
||||
for (int dz = 0; dz < width; dz++) {
|
||||
roles.put(new Vector3i(ox + dx, y, oz + dz), FungusCellRole.STEM);
|
||||
}
|
||||
}
|
||||
|
||||
topCx = cx;
|
||||
@@ -62,34 +66,4 @@ public final class FungusStemBuilder {
|
||||
|
||||
return new double[]{topCx, topY, topCz};
|
||||
}
|
||||
|
||||
static int[][] squarePositions(double cx, double cz, int width) {
|
||||
if (width % 2 == 1) {
|
||||
int half = width / 2;
|
||||
int icx = (int) Math.round(cx);
|
||||
int icz = (int) Math.round(cz);
|
||||
int[][] out = new int[width * width][2];
|
||||
int idx = 0;
|
||||
for (int dx = -half; dx <= half; dx++) {
|
||||
for (int dz = -half; dz <= half; dz++) {
|
||||
out[idx][0] = icx + dx;
|
||||
out[idx][1] = icz + dz;
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
int ox = (int) Math.floor(cx) - width / 2 + 1;
|
||||
int oz = (int) Math.floor(cz) - width / 2 + 1;
|
||||
int[][] out = new int[width * width][2];
|
||||
int idx = 0;
|
||||
for (int dx = 0; dx < width; dx++) {
|
||||
for (int dz = 0; dz < width; dz++) {
|
||||
out[idx][0] = ox + dx;
|
||||
out[idx][1] = oz + dz;
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,9 +66,10 @@ public final class ProceduralTreeGenerator {
|
||||
Map<TreeBlockCanvas.Vec, PlatformBlockState> resolved = new HashMap<>();
|
||||
Set<TreeBlockCanvas.Vec> trunkPositions = new HashSet<>();
|
||||
Set<TreeBlockCanvas.Vec> leafPositions = new HashSet<>();
|
||||
RNG paletteRng = new RNG(tree.getSeed());
|
||||
for (Map.Entry<TreeBlockCanvas.Vec, TreeBlockCanvas.Cell> entry : canvas.getCells().entrySet()) {
|
||||
TreeBlockCanvas.Cell cell = entry.getValue();
|
||||
PlatformBlockState state = TreeBlockResolver.resolve(tree, data, cell, entry.getKey());
|
||||
PlatformBlockState state = TreeBlockResolver.resolve(tree, data, cell, entry.getKey(), paletteRng);
|
||||
if (state == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -31,10 +31,7 @@ public final class TreeBlockResolver {
|
||||
private TreeBlockResolver() {
|
||||
}
|
||||
|
||||
public static PlatformBlockState resolve(IrisProceduralTree tree, IrisData data, TreeBlockCanvas.Cell cell, TreeBlockCanvas.Vec pos) {
|
||||
RNG paletteRng = new RNG(tree.getSeed());
|
||||
RNG posRng = new RNG(tree.getSeed() ^ positionHash(pos));
|
||||
|
||||
public static PlatformBlockState resolve(IrisProceduralTree tree, IrisData data, TreeBlockCanvas.Cell cell, TreeBlockCanvas.Vec pos, RNG paletteRng) {
|
||||
switch (cell.role()) {
|
||||
case TRUNK -> {
|
||||
PlatformBlockState state = resolveBlock(tree.getTrunk(), tree.getTrunkPalette(), data, pos, paletteRng);
|
||||
@@ -51,7 +48,7 @@ public final class TreeBlockResolver {
|
||||
return resolveBlock(tree.getLeaves(), tree.getLeavesPalette(), data, pos, paletteRng);
|
||||
}
|
||||
case SECONDARY_LEAF -> {
|
||||
PlatformBlockState state = resolveSecondaryLeaf(tree, data, pos, paletteRng, posRng);
|
||||
PlatformBlockState state = resolveSecondaryLeaf(tree, data, pos, paletteRng);
|
||||
if (state == null) {
|
||||
state = resolveBlock(tree.getLeaves(), tree.getLeavesPalette(), data, pos, paletteRng);
|
||||
}
|
||||
@@ -94,12 +91,12 @@ public final class TreeBlockResolver {
|
||||
return state;
|
||||
}
|
||||
|
||||
private static PlatformBlockState resolveSecondaryLeaf(IrisProceduralTree tree, IrisData data, TreeBlockCanvas.Vec pos, RNG paletteRng, RNG posRng) {
|
||||
private static PlatformBlockState resolveSecondaryLeaf(IrisProceduralTree tree, IrisData data, TreeBlockCanvas.Vec pos, RNG paletteRng) {
|
||||
if (TreeTrunkBuilder.paletteSet(tree.getSecondaryLeavesPalette())) {
|
||||
return tree.getSecondaryLeavesPalette().get(paletteRng, pos.x(), pos.y(), pos.z(), data);
|
||||
}
|
||||
if (tree.getWeightedSecondaryLeaves() != null && !tree.getWeightedSecondaryLeaves().isEmpty()) {
|
||||
String picked = pickWeighted(tree, posRng);
|
||||
String picked = pickWeighted(tree, new RNG(tree.getSeed() ^ positionHash(pos)));
|
||||
return picked == null ? null : B.getStateOrNull(picked, false);
|
||||
}
|
||||
if (tree.getSecondaryLeaves() != null && !tree.getSecondaryLeaves().isEmpty()) {
|
||||
|
||||
@@ -67,11 +67,24 @@ public final class TreeDecoratorApplier {
|
||||
if (rng.nextDouble() > dec.getChance()) {
|
||||
continue;
|
||||
}
|
||||
if (canvas.has(e[0], e[1], e[2])) {
|
||||
int dx = e[0] - e[3];
|
||||
int dz = e[2] - e[4];
|
||||
boolean alongX = Math.abs(dx) >= Math.abs(dz);
|
||||
int stepX = alongX ? (dx >= 0 ? 1 : -1) : 0;
|
||||
int stepZ = alongX ? 0 : (dz >= 0 ? 1 : -1);
|
||||
int x = e[0] + stepX;
|
||||
int z = e[2] + stepZ;
|
||||
TreeBlockCanvas.Cell cell = canvas.get(x, e[1], z);
|
||||
while (cell != null && cell.role() != TreeBlockCanvas.Role.DECORATOR) {
|
||||
x += stepX;
|
||||
z += stepZ;
|
||||
cell = canvas.get(x, e[1], z);
|
||||
}
|
||||
if (cell != null) {
|
||||
continue;
|
||||
}
|
||||
String facing = dec.isAxisAware() ? facingAway(e[3], e[4], e[0], e[2]) : null;
|
||||
canvas.setDecor(e[0], e[1], e[2], idx, facing);
|
||||
canvas.setDecor(x, e[1], z, idx, facing);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,9 @@ public final class TreePlausibility {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<TreeBlockCanvas.Vec, Integer> distance = computeDistances(trunkPositions, realLeaves);
|
||||
Map<TreeBlockCanvas.Vec, Integer> distance = tree.isPlausible()
|
||||
? computeDistances(trunkPositions, realLeaves)
|
||||
: Map.of();
|
||||
|
||||
for (TreeBlockCanvas.Vec v : realLeaves) {
|
||||
PlatformBlockState leaf = resolved.get(v);
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
|
||||
package art.arcane.iris.engine.object.tree;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
|
||||
public final class TreeSupport {
|
||||
@@ -36,7 +35,7 @@ public final class TreeSupport {
|
||||
TreeBlockCanvas.Vec worstWood = null;
|
||||
int worstGap = -1;
|
||||
|
||||
for (TreeBlockCanvas.Vec leaf : new ArrayList<>(canvas.getLeaf())) {
|
||||
for (TreeBlockCanvas.Vec leaf : canvas.getLeaf()) {
|
||||
Integer d = dist.get(leaf);
|
||||
if (d != null && d <= LEGAL_DISTANCE) {
|
||||
continue;
|
||||
|
||||
@@ -121,7 +121,7 @@ public final class TreeTrunkBuilder {
|
||||
}
|
||||
|
||||
private static void markExposedEnds(TreeBlockCanvas canvas) {
|
||||
for (TreeBlockCanvas.Vec v : new ArrayList<>(canvas.getTrunk())) {
|
||||
for (TreeBlockCanvas.Vec v : canvas.getTrunk()) {
|
||||
if (!canvas.has(v.x(), v.y() + 1, v.z()) || !canvas.has(v.x(), v.y() - 1, v.z())) {
|
||||
canvas.markExposed(v.x(), v.y(), v.z());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
package art.arcane.iris.engine.decorator;
|
||||
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisDecorationPart;
|
||||
import art.arcane.iris.engine.object.IrisDecorator;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.project.hunk.Hunk;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyDouble;
|
||||
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class IrisSeaDecoratorBoundsTest {
|
||||
@Test
|
||||
public void floorPercentageCannotGrowAboveTheWaterColumn() {
|
||||
Fixture fixture = new Fixture(12, false);
|
||||
fixture.stacking(200, 30);
|
||||
|
||||
fixture.decorate(4, 8);
|
||||
|
||||
assertSame(fixture.body, fixture.output.get(0, 4, 0));
|
||||
assertSame(fixture.top, fixture.output.get(0, 7, 0));
|
||||
assertNull(fixture.output.get(0, 8, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void floorPercentageRespectsAbsoluteMaximum() {
|
||||
Fixture fixture = new Fixture(64, false);
|
||||
fixture.stacking(100, 3);
|
||||
|
||||
fixture.decorate(4, 60);
|
||||
|
||||
assertSame(fixture.body, fixture.output.get(0, 4, 0));
|
||||
assertSame(fixture.top, fixture.output.get(0, 6, 0));
|
||||
assertNull(fixture.output.get(0, 7, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void surfacePercentageRespectsAbsoluteMaximum() {
|
||||
Fixture fixture = new Fixture(64, true);
|
||||
fixture.stacking(100, 3);
|
||||
|
||||
fixture.decorate(4, 64);
|
||||
|
||||
assertSame(fixture.body, fixture.output.get(0, 5, 0));
|
||||
assertSame(fixture.top, fixture.output.get(0, 7, 0));
|
||||
assertNull(fixture.output.get(0, 8, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void floorStackAtOutputCeilingUsesOneTopBlock() {
|
||||
Fixture fixture = new Fixture(8, false);
|
||||
fixture.stacking(200, 30);
|
||||
|
||||
fixture.decorate(7, 20);
|
||||
|
||||
assertSame(fixture.top, fixture.output.get(0, 7, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unresolvedFloorPalettePreservesExistingWater() {
|
||||
Fixture fixture = new Fixture(8, false);
|
||||
fixture.stacking(100, 30);
|
||||
PlatformBlockState water = mock(PlatformBlockState.class);
|
||||
fixture.output.set(0, 3, 0, water);
|
||||
when(fixture.decorator.getBlockData100(any(), any(), anyDouble(), anyDouble(), anyDouble(), any())).thenReturn(null);
|
||||
|
||||
fixture.decorate(3, 8);
|
||||
|
||||
assertSame(water, fixture.output.get(0, 3, 0));
|
||||
assertNull(fixture.output.get(0, 4, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void surfaceStackUsesTheShorterOutputHeight() {
|
||||
Fixture fixture = new Fixture(8, true);
|
||||
when(fixture.engine.getHeight()).thenReturn(64);
|
||||
fixture.stacking(200, 30);
|
||||
|
||||
fixture.decorate(6, 64);
|
||||
|
||||
assertSame(fixture.top, fixture.output.get(0, 7, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void surfaceSingleRejectsOutputCeiling() {
|
||||
Fixture fixture = new Fixture(8, true);
|
||||
when(fixture.engine.getHeight()).thenReturn(64);
|
||||
|
||||
fixture.decorate(7, 64);
|
||||
|
||||
assertNull(fixture.output.get(0, 7, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void surfaceSingleStackRejectsNegativeBaseAndEngineCeiling() {
|
||||
Fixture fixture = new Fixture(16, true);
|
||||
when(fixture.engine.getHeight()).thenReturn(8);
|
||||
fixture.stacking(1, 30);
|
||||
when(fixture.decorator.isScaleStack()).thenReturn(false);
|
||||
|
||||
fixture.decorate(-1, 16);
|
||||
fixture.decorate(7, 16);
|
||||
|
||||
assertNull(fixture.output.get(0, 0, 0));
|
||||
assertNull(fixture.output.get(0, 8, 0));
|
||||
}
|
||||
|
||||
private static final class Fixture {
|
||||
private final IrisDecorator decorator = mock(IrisDecorator.class);
|
||||
private final IrisBiome biome = mock(IrisBiome.class);
|
||||
private final PlatformBlockState body = mock(PlatformBlockState.class);
|
||||
private final PlatformBlockState top = mock(PlatformBlockState.class);
|
||||
private final Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS);
|
||||
private final Hunk<PlatformBlockState> output;
|
||||
private final IrisEngineDecorator placement;
|
||||
|
||||
private Fixture(int height, boolean surface) {
|
||||
when(engine.getHeight()).thenReturn(height);
|
||||
output = Hunk.newArrayHunk(1, height, 1);
|
||||
IrisDecorationPart part = surface ? IrisDecorationPart.SEA_SURFACE : IrisDecorationPart.SEA_FLOOR;
|
||||
when(biome.getDecoratorBucket(part)).thenReturn(new IrisDecorator[]{decorator});
|
||||
when(decorator.passesChanceGate(any(), anyDouble(), anyDouble(), any())).thenReturn(true);
|
||||
when(decorator.getBlockData100(any(), any(), anyDouble(), anyDouble(), anyDouble(), any())).thenReturn(body);
|
||||
when(decorator.getBlockDataForTop(any(), any(), anyDouble(), anyDouble(), anyDouble(), any())).thenReturn(top);
|
||||
when(decorator.getTopThreshold()).thenReturn(1D);
|
||||
placement = surface ? new IrisSeaSurfaceDecorator(engine) : new IrisSeaFloorDecorator(engine);
|
||||
}
|
||||
|
||||
private void stacking(int percentage, int cap) {
|
||||
when(decorator.isStacking()).thenReturn(true);
|
||||
when(decorator.isScaleStack()).thenReturn(true);
|
||||
when(decorator.getHeight(any(), anyDouble(), anyDouble(), any())).thenReturn(percentage);
|
||||
when(decorator.getAbsoluteMaxStack()).thenReturn(cap);
|
||||
}
|
||||
|
||||
private void decorate(int height, int maximum) {
|
||||
placement.decorate(0, 0, 0, 0, 0, 0, 0, 0, output, biome, height, maximum);
|
||||
}
|
||||
}
|
||||
}
|
||||
+46
-13
@@ -3,6 +3,9 @@ package art.arcane.iris.engine.modifier;
|
||||
import art.arcane.iris.engine.IrisEngine;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisDepositGenerator;
|
||||
import art.arcane.iris.engine.object.IrisDepositHeightDistribution;
|
||||
import art.arcane.iris.engine.object.IrisDepositPlacementScope;
|
||||
import art.arcane.iris.engine.object.IrisObject;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
@@ -16,6 +19,7 @@ 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.math.RNG;
|
||||
import org.junit.Test;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
@@ -33,14 +37,12 @@ import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.same;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -90,6 +92,26 @@ public class IrisDepositModifierContextTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failedClumpChanceDoesNotResampleTheConfiguredAttemptCount() {
|
||||
Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS);
|
||||
IrisDepositGenerator generator = mock(IrisDepositGenerator.class);
|
||||
when(generator.getSpawnChance()).thenReturn(1D);
|
||||
when(generator.getMinPerChunk()).thenReturn(0);
|
||||
when(generator.getMaxPerChunk()).thenReturn(4);
|
||||
when(generator.getPerClumpSpawnChance()).thenReturn(0D);
|
||||
RNG rng = mock(RNG.class);
|
||||
when(rng.d()).thenReturn(0.5D);
|
||||
when(rng.i(0, 5)).thenReturn(4, 0);
|
||||
when(rng.nextParallelRNG(anyLong())).thenReturn(rng);
|
||||
|
||||
new IrisDepositModifier(engine).generate(generator, null, null, rng, 0, 0, false, null);
|
||||
|
||||
verify(generator, times(4)).getPerClumpSpawnChance();
|
||||
verify(rng, times(1)).i(0, 5);
|
||||
verify(generator, never()).getClump(any(), any(), any());
|
||||
}
|
||||
|
||||
private static final class Fixture implements AutoCloseable {
|
||||
private final ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
private final Engine engine;
|
||||
@@ -138,16 +160,27 @@ public class IrisDepositModifierContextTest {
|
||||
return null;
|
||||
}).when(scope).close();
|
||||
}
|
||||
modifier = spy(new IrisDepositModifier(engine));
|
||||
doAnswer(invocation -> {
|
||||
assertNotSame(caller, Thread.currentThread());
|
||||
assertEquals(scoped, workerRuntime.get());
|
||||
assertSame(engine, IrisContext.require().getEngine());
|
||||
assertSame(context, IrisContext.require().getChunkContext());
|
||||
assertEquals(71L, IrisContext.require().getGenerationSessionId());
|
||||
completed.incrementAndGet();
|
||||
return null;
|
||||
}).when(modifier).generate(any(), same(chunk), any(), any(), eq(2), eq(-3), eq(false), same(context));
|
||||
when(engine.getHeight()).thenReturn(16);
|
||||
IrisObject clump = new IrisObject(1, 1, 1);
|
||||
for (IrisDepositGenerator generator : new IrisDepositGenerator[]{dimensionDeposit, regionDeposit, biomeDeposit}) {
|
||||
when(generator.getSpawnChance()).thenReturn(1D);
|
||||
when(generator.getMinPerChunk()).thenReturn(1);
|
||||
when(generator.getMaxPerChunk()).thenReturn(1);
|
||||
when(generator.getPerClumpSpawnChance()).thenReturn(1D);
|
||||
when(generator.getPlacementScope()).thenReturn(IrisDepositPlacementScope.FULL_HEIGHT);
|
||||
when(generator.getHeightDistribution()).thenReturn(IrisDepositHeightDistribution.UNIFORM);
|
||||
when(generator.matchesBiome(any(), any())).thenReturn(true);
|
||||
doAnswer(invocation -> {
|
||||
assertNotSame(caller, Thread.currentThread());
|
||||
assertEquals(scoped, workerRuntime.get());
|
||||
assertSame(engine, IrisContext.require().getEngine());
|
||||
assertSame(context, IrisContext.require().getChunkContext());
|
||||
assertEquals(71L, IrisContext.require().getGenerationSessionId());
|
||||
completed.incrementAndGet();
|
||||
return clump;
|
||||
}).when(generator).getClump(any(), any(), any());
|
||||
}
|
||||
modifier = new IrisDepositModifier(engine);
|
||||
doAnswer(invocation -> {
|
||||
assertEquals(3, completed.get());
|
||||
assertEquals(scoped ? 3 : 0, closedScopes.get());
|
||||
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
package art.arcane.iris.engine.modifier;
|
||||
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisDepositGenerator;
|
||||
import art.arcane.iris.engine.object.IrisDepositHeightDistribution;
|
||||
import art.arcane.iris.engine.object.IrisDepositPlacementScope;
|
||||
import art.arcane.iris.engine.object.IrisObject;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
import art.arcane.iris.util.common.data.B;
|
||||
import art.arcane.iris.util.common.math.IrisBlockVector;
|
||||
import art.arcane.iris.util.common.parallel.BurstExecutor;
|
||||
import art.arcane.iris.util.common.parallel.MultiBurst;
|
||||
import art.arcane.iris.util.project.context.ChunkContext;
|
||||
import art.arcane.iris.util.project.hunk.Hunk;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class IrisDepositModifierOrderingTest {
|
||||
@Before
|
||||
public void bindPlatform() {
|
||||
IrisPlatforms.unbind();
|
||||
PlatformRegistries registries = mock(PlatformRegistries.class);
|
||||
PlatformBlockState stone = state("stone");
|
||||
when(registries.block(anyString())).thenReturn(stone);
|
||||
IrisPlatform platform = mock(IrisPlatform.class);
|
||||
when(platform.registries()).thenReturn(registries);
|
||||
IrisPlatforms.bind(platform);
|
||||
}
|
||||
|
||||
@After
|
||||
public void unbindPlatform() {
|
||||
IrisPlatforms.unbind();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reverseWorkerCompletionKeepsDimensionRegionBiomePlacementOrder() throws Exception {
|
||||
try (Fixture fixture = new Fixture(true, 1)) {
|
||||
fixture.generate();
|
||||
assertEquals(List.of("biome", "region", "dimension"), fixture.completed);
|
||||
fixture.assertBiomeWins();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inlinePreparationUsesTheSameOverlappingHostRules() throws Exception {
|
||||
try (Fixture fixture = new Fixture(false, 1)) {
|
||||
fixture.generate();
|
||||
assertEquals(List.of("dimension", "region", "biome"), fixture.completed);
|
||||
fixture.assertBiomeWins();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void manyAttemptBatchesKeepEveryClumpSeedAndOrderedHostReplacement() throws Exception {
|
||||
List<String> expectedSamples = new ArrayList<>();
|
||||
RNG generatorRng = new RNG(0L).nextParallelRNG(0L);
|
||||
for (String name : List.of("dimension", "region", "biome")) {
|
||||
for (int i = 0; i < 25; i++) {
|
||||
expectedSamples.add(name + ":" + generatorRng.nextParallelRNG(i + 1L).getSeed());
|
||||
}
|
||||
}
|
||||
Collections.sort(expectedSamples);
|
||||
|
||||
for (boolean parallel : new boolean[]{false, true}) {
|
||||
try (Fixture fixture = new Fixture(parallel, 25)) {
|
||||
fixture.generate();
|
||||
Collections.sort(fixture.samples);
|
||||
assertEquals(expectedSamples, fixture.samples);
|
||||
assertEquals(75, fixture.completed.size());
|
||||
fixture.assertBiomeWins();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static PlatformBlockState state(String material) {
|
||||
PlatformBlockState state = mock(PlatformBlockState.class);
|
||||
when(state.key()).thenReturn("minecraft:" + material);
|
||||
when(state.materialKey()).thenReturn("minecraft:" + material);
|
||||
return state;
|
||||
}
|
||||
|
||||
private static final class Fixture implements AutoCloseable {
|
||||
private final ExecutorService executor = Executors.newFixedThreadPool(3);
|
||||
private final List<String> completed = Collections.synchronizedList(new ArrayList<>());
|
||||
private final List<String> samples = Collections.synchronizedList(new ArrayList<>());
|
||||
private final CountDownLatch biomeReady = new CountDownLatch(1);
|
||||
private final CountDownLatch regionReady = new CountDownLatch(1);
|
||||
private final Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS);
|
||||
private final ChunkContext context = mock(ChunkContext.class, RETURNS_DEEP_STUBS);
|
||||
private final PlatformBlockState stone = state("stone");
|
||||
private final PlatformBlockState granite = state("granite");
|
||||
private final PlatformBlockState diorite = state("diorite");
|
||||
private final PlatformBlockState diamond = state("diamond_ore");
|
||||
private final Hunk<PlatformBlockState> output = Hunk.newArrayHunk(16, 16, 16);
|
||||
private final MantleChunk<Matter> chunk;
|
||||
private final boolean parallel;
|
||||
private final int attempts;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Fixture(boolean parallel, int attempts) {
|
||||
this.parallel = parallel;
|
||||
this.attempts = attempts;
|
||||
when(engine.getHeight()).thenReturn(16);
|
||||
when(engine.getCaveBiome(anyInt(), anyInt(), anyInt(), any())).thenReturn(null);
|
||||
when(engine.getDimension().getDepositVariants()).thenReturn(new KList<>());
|
||||
when(context.getRegion().get(anyInt(), anyInt()).getDepositVariants()).thenReturn(new KList<>());
|
||||
when(context.getGenerationSessionId()).thenReturn(11L);
|
||||
chunk = mock(MantleChunk.class);
|
||||
Mantle<Matter> mantle = engine.getMantle().getMantle();
|
||||
doReturn(chunk).when(mantle).getChunk(0, 0);
|
||||
when(chunk.use()).thenReturn(chunk);
|
||||
MultiBurst pool = mock(MultiBurst.class);
|
||||
BurstExecutor burst = new BurstExecutor(executor, 3);
|
||||
burst.setMulticore(parallel);
|
||||
when(engine.burst()).thenReturn(pool);
|
||||
when(pool.burst(parallel)).thenReturn(burst);
|
||||
IrisDepositGenerator dimensionDeposit = generator("dimension", stone, granite);
|
||||
IrisDepositGenerator regionDeposit = generator("region", granite, diorite);
|
||||
IrisDepositGenerator biomeDeposit = generator("biome", diorite, diamond);
|
||||
when(engine.getDimension().getDeposits()).thenReturn(new KList<>(dimensionDeposit));
|
||||
when(context.getRegion().get(7, 7).getDeposits()).thenReturn(new KList<>(regionDeposit));
|
||||
when(context.getBiome().get(7, 7).getDeposits()).thenReturn(new KList<>(biomeDeposit));
|
||||
for (int x = 0; x < 16; x++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
output.set(x, 4, z, stone);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IrisDepositGenerator generator(String name, PlatformBlockState requiredHost, PlatformBlockState replacement) {
|
||||
IrisDepositGenerator generator = mock(IrisDepositGenerator.class);
|
||||
when(generator.getSpawnChance()).thenReturn(1D);
|
||||
when(generator.getPerClumpSpawnChance()).thenReturn(1D);
|
||||
when(generator.getMinPerChunk()).thenReturn(attempts);
|
||||
when(generator.getMaxPerChunk()).thenReturn(attempts);
|
||||
when(generator.getMinHeight()).thenReturn(4);
|
||||
when(generator.getMaxHeight()).thenReturn(4);
|
||||
when(generator.getPlacementScope()).thenReturn(IrisDepositPlacementScope.FULL_HEIGHT);
|
||||
when(generator.getHeightDistribution()).thenReturn(IrisDepositHeightDistribution.UNIFORM);
|
||||
when(generator.matchesBiome(any(), any())).thenReturn(true);
|
||||
when(generator.canReplace(requiredHost)).thenReturn(true);
|
||||
IrisObject clump = new IrisObject(33, 1, 33);
|
||||
for (int x = -16; x <= 16; x++) {
|
||||
for (int z = -16; z <= 16; z++) {
|
||||
clump.getBlocks().put(new IrisBlockVector(x, 0, z), replacement);
|
||||
}
|
||||
}
|
||||
when(generator.getClump(any(), any(), any())).thenAnswer(invocation -> {
|
||||
assertTrue("Placement must release the first clump window before more are prepared",
|
||||
samples.size() < 32 || output.get(0, 4, 0) != stone);
|
||||
if (parallel && attempts == 1 && name.equals("dimension")) {
|
||||
assertTrue(regionReady.await(10, TimeUnit.SECONDS));
|
||||
} else if (parallel && attempts == 1 && name.equals("region")) {
|
||||
assertTrue(biomeReady.await(10, TimeUnit.SECONDS));
|
||||
}
|
||||
RNG rng = invocation.getArgument(1);
|
||||
samples.add(name + ":" + rng.getSeed());
|
||||
completed.add(name);
|
||||
if (name.equals("biome")) {
|
||||
biomeReady.countDown();
|
||||
} else if (name.equals("region")) {
|
||||
regionReady.countDown();
|
||||
}
|
||||
return clump;
|
||||
});
|
||||
return generator;
|
||||
}
|
||||
|
||||
private void generate() {
|
||||
try (MockedStatic<B> blocks = mockStatic(B.class, CALLS_REAL_METHODS)) {
|
||||
blocks.when(() -> B.toDeepSlateOre(any(), any())).thenAnswer(invocation -> invocation.getArgument(1));
|
||||
new IrisDepositModifier(engine).generateDeposits(output, 0, 0, parallel, context);
|
||||
}
|
||||
verify(chunk).release();
|
||||
}
|
||||
|
||||
private void assertBiomeWins() {
|
||||
for (int x = 0; x < 16; x++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
assertSame("Deposit collision at " + x + ",4," + z, diamond, output.get(x, 4, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
executor.shutdownNow();
|
||||
assertTrue(executor.awaitTermination(10, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package art.arcane.iris.engine.object.fungi;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisFungus;
|
||||
import art.arcane.iris.util.common.math.Vector3i;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class FungusStemBuilderTest {
|
||||
@Test
|
||||
public void straightStemsKeepOddAndEvenWidthAnchors() {
|
||||
for (int width = 1; width <= 3; width++) {
|
||||
IrisFungus fungus = new IrisFungus();
|
||||
fungus.setStemWidth(width);
|
||||
fungus.setStemCurve(0D);
|
||||
fungus.setStemWaveAmplitude(0D);
|
||||
Map<Vector3i, FungusCellRole> roles = new HashMap<>();
|
||||
|
||||
double[] top = FungusStemBuilder.build(roles, fungus, 7, 421L);
|
||||
|
||||
assertArrayEquals(new double[]{0D, 6D, 0D}, top, 0D);
|
||||
assertEquals(width * width * 7, roles.size());
|
||||
int origin = width == 3 ? -1 : 0;
|
||||
for (int y = 0; y < 7; y++) {
|
||||
for (int x = origin; x < origin + width; x++) {
|
||||
for (int z = origin; z < origin + width; z++) {
|
||||
assertEquals(FungusCellRole.STEM, roles.get(new Vector3i(x, y, z)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void curvedStemsKeepEveryLayerAndReturnTheirCapAnchor() {
|
||||
IrisFungus fungus = new IrisFungus();
|
||||
fungus.setStemWidth(3);
|
||||
fungus.setStemCurve(22D);
|
||||
fungus.setStemLeanAzimuth(225D);
|
||||
fungus.setStemWaveAmplitude(2D);
|
||||
fungus.setStemWavePeriods(1.5D);
|
||||
Map<Vector3i, FungusCellRole> roles = new HashMap<>();
|
||||
|
||||
double[] top = FungusStemBuilder.build(roles, fungus, 24, -733L);
|
||||
|
||||
assertEquals(24 * 9, roles.size());
|
||||
assertTrue(top[0] < 0D);
|
||||
assertTrue(top[2] < 0D);
|
||||
assertEquals(23D, top[1], 0D);
|
||||
assertEquals(FungusCellRole.STEM, roles.get(new Vector3i(
|
||||
(int) Math.round(top[0]), 23, (int) Math.round(top[2]))));
|
||||
for (int y = 0; y < 24; y++) {
|
||||
int count = 0;
|
||||
for (Vector3i position : roles.keySet()) {
|
||||
if (position.getY() == y) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
assertEquals(9, count);
|
||||
}
|
||||
}
|
||||
}
|
||||
+24
@@ -6,9 +6,11 @@ import art.arcane.iris.engine.object.IrisTreeBranches;
|
||||
import art.arcane.iris.engine.object.IrisTreeFunction;
|
||||
import art.arcane.iris.engine.object.IrisTreeLeafMode;
|
||||
import art.arcane.iris.engine.object.IrisTreeProfile;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
@@ -18,6 +20,9 @@ import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class ProceduralTreeGeneratorTest {
|
||||
|
||||
@@ -268,4 +273,23 @@ public class ProceduralTreeGeneratorTest {
|
||||
distances.get(new TreeBlockCanvas.Vec(x, 0, 0)));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void disabledPlausibilityMakesDisconnectedLeavesPersistent() {
|
||||
IrisProceduralTree tree = new IrisProceduralTree();
|
||||
tree.setPlausible(false);
|
||||
TreeBlockCanvas.Vec position = new TreeBlockCanvas.Vec(40, 20, -10);
|
||||
PlatformBlockState leaf = mock(PlatformBlockState.class);
|
||||
PlatformBlockState persistent = mock(PlatformBlockState.class);
|
||||
PlatformBlockState supported = mock(PlatformBlockState.class);
|
||||
when(leaf.key()).thenReturn("minecraft:oak_leaves[persistent=false,distance=7]");
|
||||
when(leaf.withProperty("persistent", "true")).thenReturn(persistent);
|
||||
when(persistent.withProperty("distance", "1")).thenReturn(supported);
|
||||
Map<TreeBlockCanvas.Vec, PlatformBlockState> resolved = new HashMap<>();
|
||||
resolved.put(position, leaf);
|
||||
|
||||
TreePlausibility.apply(resolved, Set.of(new TreeBlockCanvas.Vec(0, 0, 0)), Set.of(position), tree);
|
||||
|
||||
assertSame(supported, resolved.get(position));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package art.arcane.iris.engine.object.tree;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisBlockData;
|
||||
import art.arcane.iris.engine.object.IrisMaterialPalette;
|
||||
import art.arcane.iris.engine.object.IrisProceduralTree;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class TreeBlockResolverTest {
|
||||
@Test
|
||||
public void sharedPaletteRandomSourceMatchesFreshPerVoxelSources() {
|
||||
IrisProceduralTree tree = new IrisProceduralTree();
|
||||
tree.setSeed(84261L);
|
||||
tree.setTrunkPalette(palette("oak_log", "spruce_log"));
|
||||
tree.setLeavesPalette(palette("oak_leaves", "birch_leaves"));
|
||||
tree.setSecondaryTrunkPalette(palette("birch_log", "jungle_log"));
|
||||
tree.setSecondaryLeavesPalette(palette("spruce_leaves", "jungle_leaves"));
|
||||
RNG shared = new RNG(tree.getSeed());
|
||||
List<String> freshBlocks = new ArrayList<>();
|
||||
List<String> sharedBlocks = new ArrayList<>();
|
||||
Set<String> materials = new HashSet<>();
|
||||
TreeBlockCanvas.Role[] roles = {
|
||||
TreeBlockCanvas.Role.TRUNK, TreeBlockCanvas.Role.LEAF,
|
||||
TreeBlockCanvas.Role.SECONDARY_TRUNK, TreeBlockCanvas.Role.SECONDARY_LEAF
|
||||
};
|
||||
for (int x = -8; x <= 8; x++) {
|
||||
for (int y = 0; y < 24; y++) {
|
||||
TreeBlockCanvas.Vec position = new TreeBlockCanvas.Vec(x, y, x - y);
|
||||
TreeBlockCanvas.Cell cell = new TreeBlockCanvas.Cell(
|
||||
roles[Math.floorMod(x + y, roles.length)], TreeBlockCanvas.Axis.NONE, false, -1, null);
|
||||
PlatformBlockState expected = TreeBlockResolver.resolve(tree, null, cell, position, new RNG(tree.getSeed()));
|
||||
PlatformBlockState actual = TreeBlockResolver.resolve(tree, null, cell, position, shared);
|
||||
freshBlocks.add(position + ":" + expected.key());
|
||||
sharedBlocks.add(position + ":" + actual.key());
|
||||
materials.add(actual.key());
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(freshBlocks, sharedBlocks);
|
||||
assertEquals(new RNG(tree.getSeed()).nextLong(), shared.nextLong());
|
||||
assertTrue(materials.size() > roles.length);
|
||||
}
|
||||
|
||||
private static IrisMaterialPalette palette(String first, String second) {
|
||||
IrisBlockData firstBlock = block(first);
|
||||
IrisBlockData secondBlock = block(second);
|
||||
return new IrisMaterialPalette().setPalette(new KList<>(firstBlock, secondBlock));
|
||||
}
|
||||
|
||||
private static IrisBlockData block(String material) {
|
||||
PlatformBlockState state = mock(PlatformBlockState.class);
|
||||
when(state.key()).thenReturn("minecraft:" + material);
|
||||
IrisBlockData block = mock(IrisBlockData.class);
|
||||
when(block.getWeight()).thenReturn(1);
|
||||
when(block.getBlockData(null)).thenReturn(state);
|
||||
return block;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package art.arcane.iris.engine.object.tree;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisProceduralTree;
|
||||
import art.arcane.iris.engine.object.IrisTreeDecorator;
|
||||
import art.arcane.iris.engine.object.IrisTreeDecoratorTarget;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class TreeDecoratorApplierTest {
|
||||
@Test
|
||||
public void branchTipsExtendOutwardThroughLeavesWithoutReplacingWood() {
|
||||
TreeBlockCanvas canvas = new TreeBlockCanvas();
|
||||
for (int x = -3; x <= 3; x++) {
|
||||
canvas.setTrunk(x, 4, 0, TreeBlockCanvas.Role.TRUNK, TreeBlockCanvas.Axis.X);
|
||||
}
|
||||
canvas.setLeaf(-4, 4, 0, TreeBlockCanvas.Role.LEAF);
|
||||
canvas.setLeaf(4, 4, 0, TreeBlockCanvas.Role.LEAF);
|
||||
IrisTreeDecorator decorator = new IrisTreeDecorator()
|
||||
.setTarget(IrisTreeDecoratorTarget.BRANCH_TIP)
|
||||
.setBlock("minecraft:shroomlight")
|
||||
.setChance(1D)
|
||||
.setAxisAware(true);
|
||||
IrisProceduralTree tree = new IrisProceduralTree();
|
||||
tree.setDecorators(new KList<>(decorator));
|
||||
List<int[]> endpoints = List.of(new int[]{-3, 4, 0, 0, 0}, new int[]{3, 4, 0, 0, 0});
|
||||
|
||||
TreeDecoratorApplier.apply(canvas, tree, 4321L, endpoints);
|
||||
|
||||
assertEquals(TreeBlockCanvas.Role.DECORATOR, canvas.get(-5, 4, 0).role());
|
||||
assertEquals("west", canvas.get(-5, 4, 0).facing());
|
||||
assertEquals(TreeBlockCanvas.Role.DECORATOR, canvas.get(5, 4, 0).role());
|
||||
assertEquals("east", canvas.get(5, 4, 0).facing());
|
||||
assertEquals(7, canvas.getTrunk().size());
|
||||
assertEquals(2, canvas.getLeaf().size());
|
||||
assertEquals(11, canvas.getCells().size());
|
||||
|
||||
TreeDecoratorApplier.apply(canvas, tree, 4321L, endpoints);
|
||||
|
||||
assertEquals(11, canvas.getCells().size());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user