Merged wave1 into parallel waits

- Terrain bake and object bake from turbo/wave1
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-22 02:56:35 -04:00
commit cb008b6428
46 files changed
+4854 -913

No files matched your search

@@ -0,0 +1,7 @@
package art.arcane.iris.generation.bake;
public interface BakeCache {
ColumnBake get(int chunkX, int chunkZ);
ColumnBake getIfPresent(int chunkX, int chunkZ);
}
@@ -0,0 +1,26 @@
package art.arcane.iris.generation.bake;
public enum BakePlane {
REGION(false),
BASE_BIOME(false),
BASE_HEIGHT(true),
NATURAL_HEIGHT(true),
NATURAL_TRUE_BIOME(false),
HEIGHT(true),
TRUE_BIOME(false),
ROCK(false),
FLUID(false),
CAVE_BIOME(false);
static final BakePlane[] PLANES = values();
private final boolean doubles;
BakePlane(boolean doubles) {
this.doubles = doubles;
}
public boolean doubles() {
return doubles;
}
}
@@ -0,0 +1,41 @@
package art.arcane.iris.generation.bake;
import java.util.Arrays;
import java.util.IdentityHashMap;
import java.util.Objects;
public final class BakeRegistry<T> {
private final Object lock = new Object();
private volatile IdentityHashMap<T, Integer> ids = new IdentityHashMap<>();
private volatile Object[] values = new Object[16];
private int size;
public short id(T value) {
Objects.requireNonNull(value, "Registered value");
Integer known = ids.get(value);
if (known != null) {
return known.shortValue();
}
synchronized (lock) {
Integer existing = ids.get(value);
if (existing != null) {
return existing.shortValue();
}
if (size == Short.MAX_VALUE) {
throw new IllegalStateException("Bake registry is full");
}
Object[] grown = size == values.length ? Arrays.copyOf(values, size * 2) : values;
grown[size] = value;
values = grown;
IdentityHashMap<T, Integer> next = new IdentityHashMap<>(ids);
next.put(value, size);
ids = next;
return (short) size++;
}
}
@SuppressWarnings("unchecked")
public T value(int id) {
return (T) values[id];
}
}
@@ -0,0 +1,153 @@
package art.arcane.iris.generation.bake;
import art.arcane.iris.generation.cache.ConcurrentClockCache;
import art.arcane.iris.generation.stream.ProvisionalSampling;
import java.util.Objects;
public final class ChunkBakeCache implements BakeCache {
private static final ThreadLocal<int[]> SPARSE = ThreadLocal.withInitial(() -> new int[1]);
private final PlaneSource source;
private final ThreadLocal<Recent> recent = ThreadLocal.withInitial(Recent::new);
private volatile ConcurrentClockCache<ChunkPlanes> entries;
public ChunkBakeCache(PlaneSource source, int capacityChunks) {
this.source = Objects.requireNonNull(source, "Bake plane source");
this.entries = new ConcurrentClockCache<>(Math.max(16, capacityChunks));
}
@Override
public ColumnBake get(int chunkX, int chunkZ) {
return planes(chunkX, chunkZ).columnBake(source);
}
@Override
public ColumnBake getIfPresent(int chunkX, int chunkZ) {
ChunkPlanes planes = planesIfPresent(chunkX, chunkZ);
return planes == null ? null : planes.columnBake();
}
public int capacity() {
return entries.capacity();
}
public synchronized void ensureCapacity(int chunks) {
ConcurrentClockCache<ChunkPlanes> current = entries;
if (current.capacity() >= chunks) {
return;
}
ConcurrentClockCache<ChunkPlanes> resized = new ConcurrentClockCache<>(chunks);
current.forEach(planes -> resized.putIfAbsent(key(planes.chunkX(), planes.chunkZ()), planes));
entries = resized;
}
public void clear() {
ConcurrentClockCache<ChunkPlanes> current = entries;
entries = new ConcurrentClockCache<>(current.capacity());
current.clear();
}
public long[] census() {
long[] counts = new long[BakePlane.PLANES.length + 1];
entries.forEach(planes -> {
counts[BakePlane.PLANES.length]++;
for (BakePlane plane : BakePlane.PLANES) {
if (planes.planeIfPresent(plane) != null) {
counts[plane.ordinal()]++;
}
}
});
return counts;
}
public ChunkPlanes planes(int chunkX, int chunkZ) {
ConcurrentClockCache<ChunkPlanes> current = entries;
long key = key(chunkX, chunkZ);
ChunkPlanes existing = current.get(key);
return existing != null ? existing : current.putIfAbsent(key, new ChunkPlanes(chunkX, chunkZ));
}
public ChunkPlanes planesIfPresent(int chunkX, int chunkZ) {
return entries.get(key(chunkX, chunkZ));
}
public ChunkPlanes.Plane planeIfPresent(BakePlane plane, int chunkX, int chunkZ) {
long key = key(chunkX, chunkZ);
Recent last = recent.get();
ConcurrentClockCache<ChunkPlanes> current = entries;
ChunkPlanes planes;
if (last.key == key && last.entries == current) {
planes = last.planes;
} else {
planes = current.get(key);
if (planes == null) {
return null;
}
last.key = key;
last.planes = planes;
last.entries = current;
}
return planes.planeIfPresent(plane);
}
public ChunkPlanes.Plane plane(BakePlane plane, int chunkX, int chunkZ) {
return planes(chunkX, chunkZ).plane(plane);
}
public Object complete(BakePlane plane, int chunkX, int chunkZ) {
return remembered(chunkX, chunkZ).complete(plane, source);
}
public boolean memoAllowed() {
return SPARSE.get()[0] == 0 && !source.mainThread();
}
public void storeDouble(BakePlane plane, int blockX, int blockZ, double value, long revision) {
if (revision != ProvisionalSampling.revision()) {
return;
}
remembered(blockX >> 4, blockZ >> 4).plane(plane).storeDouble(((blockZ & 15) << 4) | (blockX & 15), value);
}
public void storeObject(BakePlane plane, int blockX, int blockZ, Object value, long revision) {
if (revision != ProvisionalSampling.revision()) {
return;
}
remembered(blockX >> 4, blockZ >> 4).plane(plane).storeObject(((blockZ & 15) << 4) | (blockX & 15), value);
}
private ChunkPlanes remembered(int chunkX, int chunkZ) {
ConcurrentClockCache<ChunkPlanes> current = entries;
long key = key(chunkX, chunkZ);
ChunkPlanes existing = current.get(key);
ChunkPlanes planes = existing != null ? existing : current.putIfAbsent(key, new ChunkPlanes(chunkX, chunkZ));
Recent last = recent.get();
last.key = key;
last.planes = planes;
last.entries = current;
return planes;
}
public static void enterSparse() {
SPARSE.get()[0]++;
}
public static void exitSparse() {
SPARSE.get()[0]--;
}
public static boolean inSparseScope() {
return SPARSE.get()[0] > 0;
}
public static long key(int chunkX, int chunkZ) {
return ((long) chunkX << 32) ^ (chunkZ & 0xffffffffL);
}
private static final class Recent {
private long key;
private ChunkPlanes planes;
private ConcurrentClockCache<ChunkPlanes> entries;
}
}
@@ -0,0 +1,208 @@
package art.arcane.iris.generation.bake;
import art.arcane.iris.generation.stream.ProvisionalSampling;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicReferenceArray;
public final class ChunkPlanes {
private static final VarHandle READY = MethodHandles.arrayElementVarHandle(long[].class);
private final int chunkX;
private final int chunkZ;
private final AtomicReferenceArray<Plane> planes = new AtomicReferenceArray<>(BakePlane.PLANES.length);
private volatile ColumnBake columnBake;
ChunkPlanes(int chunkX, int chunkZ) {
this.chunkX = chunkX;
this.chunkZ = chunkZ;
}
public int chunkX() {
return chunkX;
}
public int chunkZ() {
return chunkZ;
}
public Plane planeIfPresent(BakePlane kind) {
return planes.getAcquire(kind.ordinal());
}
public Plane plane(BakePlane kind) {
int slot = kind.ordinal();
Plane plane = planes.getAcquire(slot);
if (plane != null) {
return plane;
}
Plane created = new Plane(kind);
Plane witness = planes.compareAndExchangeRelease(slot, null, created);
return witness == null ? created : witness;
}
Object complete(BakePlane kind, PlaneSource source) {
Plane plane = plane(kind);
while (true) {
if (plane.complete) {
return plane.values;
}
Pending running = plane.filling.get();
if (running != null) {
if (running.owner == Thread.currentThread()) {
return plane.unpublishedFill(source, chunkX, chunkZ);
}
running.await();
continue;
}
Pending pending = new Pending(Thread.currentThread());
if (!plane.filling.compareAndSet(null, pending)) {
continue;
}
try {
return plane.fill(source, chunkX, chunkZ);
} finally {
plane.filling.set(null);
pending.complete();
}
}
}
ColumnBake columnBake() {
return columnBake;
}
ColumnBake columnBake(PlaneSource source) {
ColumnBake bake = columnBake;
if (bake != null) {
return bake;
}
long revision = ProvisionalSampling.revision();
bake = source.assemble(this);
if (revision == ProvisionalSampling.revision()) {
columnBake = bake;
}
return bake;
}
public static final class Plane {
private final BakePlane kind;
private final Object values;
private final long[] ready = new long[4];
private final AtomicReference<Pending> filling = new AtomicReference<>();
private volatile boolean complete;
private Plane(BakePlane kind) {
this.kind = kind;
this.values = kind.doubles() ? new double[256] : new Object[256];
}
public boolean ready(int index) {
return complete || ((long) READY.getAcquire(ready, index >>> 6) & (1L << (index & 63))) != 0L;
}
public double doubleAt(int index) {
return ((double[]) values)[index];
}
public Object objectAt(int index) {
return ((Object[]) values)[index];
}
public void storeDouble(int index, double value) {
((double[]) values)[index] = value;
markReady(index);
}
public void storeObject(int index, Object value) {
((Object[]) values)[index] = value;
markReady(index);
}
private void markReady(int index) {
READY.getAndBitwiseOrRelease(ready, index >>> 6, 1L << (index & 63));
}
private long[] missing() {
long[] missing = new long[4];
for (int word = 0; word < 4; word++) {
missing[word] = ~(long) READY.getAcquire(ready, word);
}
return missing;
}
private Object fill(PlaneSource source, int chunkX, int chunkZ) {
long[] missing = missing();
Object computed = kind.doubles() ? new double[256] : new Object[256];
long revision = ProvisionalSampling.revision();
source.fill(kind, chunkX, chunkZ, missing, computed);
if (revision != ProvisionalSampling.revision()) {
return merge(missing, computed);
}
for (int index = 0; index < 256; index++) {
if ((missing[index >>> 6] & (1L << (index & 63))) != 0L) {
if (kind.doubles()) {
((double[]) values)[index] = ((double[]) computed)[index];
} else {
((Object[]) values)[index] = ((Object[]) computed)[index];
}
}
}
for (int word = 0; word < 4; word++) {
READY.setRelease(ready, word, -1L);
}
complete = true;
return values;
}
private Object unpublishedFill(PlaneSource source, int chunkX, int chunkZ) {
long[] missing = missing();
Object computed = kind.doubles() ? new double[256] : new Object[256];
source.fill(kind, chunkX, chunkZ, missing, computed);
return merge(missing, computed);
}
private Object merge(long[] missing, Object computed) {
for (int index = 0; index < 256; index++) {
if ((missing[index >>> 6] & (1L << (index & 63))) == 0L) {
if (kind.doubles()) {
((double[]) computed)[index] = ((double[]) values)[index];
} else {
((Object[]) computed)[index] = ((Object[]) values)[index];
}
}
}
return computed;
}
}
private static final class Pending {
private final Thread owner;
private boolean done;
private Pending(Thread owner) {
this.owner = owner;
}
private synchronized void complete() {
done = true;
notifyAll();
}
private synchronized void await() {
boolean interrupted = false;
while (!done) {
try {
wait();
} catch (InterruptedException e) {
interrupted = true;
}
}
if (interrupted) {
Thread.currentThread().interrupt();
}
}
}
}
@@ -0,0 +1,55 @@
package art.arcane.iris.generation.bake;
import art.arcane.iris.generation.terrain.Terrain3DColumn;
public final class ColumnBake {
public static final int APRON_WIDTH = 18;
public final int chunkX;
public final int chunkZ;
public final short[] regionId;
public final short[] baseBiomeId;
public final short[] trueBiomeId;
public final short[] caveBiomeId;
public final double[] baseHeight;
public final double[] naturalHeight;
public final double[] height;
public final int[] roundedHeight;
public final float[] fluidSurface;
public final short[] rockState;
public final short[] fluidState;
public final Terrain3DColumn[] terrainColumns;
public final double[] apronHeight;
public ColumnBake(Planes planes) {
this.chunkX = planes.chunkX();
this.chunkZ = planes.chunkZ();
this.regionId = planes.regionId();
this.baseBiomeId = planes.baseBiomeId();
this.trueBiomeId = planes.trueBiomeId();
this.caveBiomeId = planes.caveBiomeId();
this.baseHeight = planes.baseHeight();
this.naturalHeight = planes.naturalHeight();
this.height = planes.height();
this.roundedHeight = planes.roundedHeight();
this.fluidSurface = planes.fluidSurface();
this.rockState = planes.rockState();
this.fluidState = planes.fluidState();
this.terrainColumns = planes.terrainColumns();
this.apronHeight = planes.apronHeight();
}
public static int index(int localX, int localZ) {
return (localZ << 4) | localX;
}
public static int apronIndex(int localX, int localZ) {
return (localZ + 1) * APRON_WIDTH + localX + 1;
}
public record Planes(int chunkX, int chunkZ, short[] regionId, short[] baseBiomeId, short[] trueBiomeId,
short[] caveBiomeId, double[] baseHeight, double[] naturalHeight, double[] height,
int[] roundedHeight, float[] fluidSurface, short[] rockState, short[] fluidState,
Terrain3DColumn[] terrainColumns, double[] apronHeight) {
}
}
@@ -0,0 +1,48 @@
package art.arcane.iris.generation.bake;
import java.util.concurrent.atomic.LongAdder;
public final class GeneratorEvaluations {
private static final LongAdder BAKED_CHUNKS = new LongAdder();
private static final LongAdder BAKED = new LongAdder();
private static final LongAdder BATCHED = new LongAdder();
private static final LongAdder SCALAR = new LongAdder();
private GeneratorEvaluations() {
}
public static void bakedChunk(long evaluations) {
BAKED_CHUNKS.increment();
BAKED.add(evaluations);
}
public static void batched(long evaluations) {
BATCHED.add(evaluations);
}
public static void scalar(int evaluations) {
SCALAR.add(evaluations);
}
public static Snapshot snapshot() {
return new Snapshot(BAKED_CHUNKS.sum(), BAKED.sum(), BATCHED.sum(), SCALAR.sum());
}
public static void reset() {
BAKED_CHUNKS.reset();
BAKED.reset();
BATCHED.reset();
SCALAR.reset();
}
public record Snapshot(long bakedChunks, long bakedEvaluations, long batchedEvaluations, long scalarEvaluations) {
public double bakedEvaluationsPerChunk() {
return bakedChunks == 0L ? 0D : (double) bakedEvaluations / bakedChunks;
}
public Snapshot minus(Snapshot earlier) {
return new Snapshot(bakedChunks - earlier.bakedChunks, bakedEvaluations - earlier.bakedEvaluations,
batchedEvaluations - earlier.batchedEvaluations, scalarEvaluations - earlier.scalarEvaluations);
}
}
}
@@ -0,0 +1,9 @@
package art.arcane.iris.generation.bake;
public interface PlaneSource {
void fill(BakePlane plane, int chunkX, int chunkZ, long[] missing, Object values);
ColumnBake assemble(ChunkPlanes planes);
boolean mainThread();
}
@@ -0,0 +1,161 @@
package art.arcane.iris.generation.bake;
import art.arcane.iris.generation.noise.GeneratorProgram;
import art.arcane.iris.generation.noise.IrisGenerator;
import art.arcane.iris.generation.noise.TerrainBatchScratch;
import art.arcane.volmlib.util.math.M;
import java.util.Arrays;
public final class TerrainProgram {
private static final int SKIPPED = 0;
private static final int CONSTANT = 1;
private static final int ACTIVE = 2;
private final IrisGenerator[][] generators;
private final GeneratorProgram[][] programs;
private final long heightSeed;
private TerrainProgram(IrisGenerator[][] generators, GeneratorProgram[][] programs, long heightSeed) {
this.generators = generators;
this.programs = programs;
this.heightSeed = heightSeed;
}
public static TerrainProgram compile(IrisGenerator[][] generators, long heightSeed) {
GeneratorProgram[][] programs = new GeneratorProgram[generators.length][];
for (int group = 0; group < generators.length; group++) {
programs[group] = new GeneratorProgram[generators[group].length];
for (int index = 0; index < generators[group].length; index++) {
IrisGenerator generator = generators[group][index];
programs[group][index] = generator.supportsBatch() ? generator.compileProgram(heightSeed) : null;
}
}
return new TerrainProgram(generators, programs, heightSeed);
}
public long heightSeed() {
return heightSeed;
}
public double average(int group, TerrainBatchScratch scratch, double low, double high, double x, double z) {
IrisGenerator[] members = generators[group];
if (members.length == 0) {
return 0D;
}
if (!Double.isFinite(low) || !Double.isFinite(high)) {
throw new IllegalStateException("Terrain generator bounds were not finite at " + x + "," + z
+ " (generator=" + members[0].getLoadKey() + ", bounds=" + low + ".." + high + ").");
}
double height = 0D;
if (low == high) {
for (int member = 0; member < members.length; member++) {
height += low;
}
return height / members.length;
}
GeneratorEvaluations.scalar(members.length);
GeneratorProgram[] compiled = programs[group];
for (int member = 0; member < members.length; member++) {
GeneratorProgram program = compiled[member];
double noise = program != null ? program.height(scratch, x, z) : members[member].getHeight(x, z, heightSeed);
if (!Double.isFinite(noise)) {
throw new IllegalStateException("Terrain generator noise was not finite at " + x + "," + z
+ " (generator=" + members[member].getLoadKey() + ", noise=" + noise
+ ", bounds=" + low + ".." + high + ").");
}
height += M.lerp(low, high, noise);
}
return height / members.length;
}
public int groups() {
return generators.length;
}
public int groupSize(int group) {
return generators[group].length;
}
public long accumulate(int group, TerrainBatchScratch scratch, double[] xs, double[] zs, int count,
double[] lows, double[] highs, double[] heights) {
IrisGenerator[] members = generators[group];
if (members.length == 0 || count == 0) {
return 0L;
}
int mark = scratch.mark();
int indexMark = scratch.indexMark();
int[] modes = scratch.indices(count);
int activeCount = 0;
for (int index = 0; index < count; index++) {
double low = lows[index];
double high = highs[index];
if (low == 0D && high == 0D) {
modes[index] = SKIPPED;
continue;
}
if (!Double.isFinite(low) || !Double.isFinite(high)) {
throw new IllegalStateException("Terrain generator bounds were not finite at " + xs[index] + "," + zs[index]
+ " (generator=" + members[0].getLoadKey() + ", bounds=" + low + ".." + high + ").");
}
if (low == high) {
modes[index] = CONSTANT;
double sum = 0D;
for (int member = 0; member < members.length; member++) {
sum += low;
}
heights[index] += sum / members.length;
continue;
}
modes[index] = ACTIVE;
activeCount++;
}
if (activeCount == 0) {
scratch.resetIndices(indexMark);
return 0L;
}
double[] activeX = scratch.buffer(activeCount);
double[] activeZ = scratch.buffer(activeCount);
double[] activeLow = scratch.buffer(activeCount);
double[] activeHigh = scratch.buffer(activeCount);
int[] activeIndex = scratch.indices(activeCount);
int cursor = 0;
for (int index = 0; index < count; index++) {
if (modes[index] == ACTIVE) {
activeX[cursor] = xs[index];
activeZ[cursor] = zs[index];
activeLow[cursor] = lows[index];
activeHigh[cursor] = highs[index];
activeIndex[cursor++] = index;
}
}
double[] sums = scratch.buffer(activeCount);
double[] noise = scratch.buffer(activeCount);
Arrays.fill(sums, 0, activeCount, 0D);
GeneratorProgram[] compiled = programs[group];
for (int member = 0; member < members.length; member++) {
GeneratorProgram program = compiled[member];
if (program != null) {
program.heights(scratch, activeX, activeZ, activeCount, noise);
} else {
for (int index = 0; index < activeCount; index++) {
noise[index] = members[member].getHeight(activeX[index], activeZ[index], heightSeed);
}
}
for (int index = 0; index < activeCount; index++) {
if (!Double.isFinite(noise[index])) {
throw new IllegalStateException("Terrain generator noise was not finite at " + activeX[index] + "," + activeZ[index]
+ " (generator=" + members[member].getLoadKey() + ", noise=" + noise[index]
+ ", bounds=" + activeLow[index] + ".." + activeHigh[index] + ").");
}
sums[index] += M.lerp(activeLow[index], activeHigh[index], noise[index]);
}
}
for (int index = 0; index < activeCount; index++) {
heights[activeIndex[index]] += sums[index] / members.length;
}
scratch.reset(mark);
scratch.resetIndices(indexMark);
return (long) activeCount * members.length;
}
}
@@ -515,7 +515,10 @@ public class IrisBiome extends IrisRegistrant implements Rarity {
}
public CNG getChildrenGenerator(RNG random, int sig, double scale) {
long seed = deriveChildStyleSeed(random, sig).getSeed();
return getChildrenGenerator(deriveChildStyleSeed(random, sig).getSeed(), scale);
}
public CNG getChildrenGenerator(long seed, double scale) {
IrisData data = getLoader();
Engine engine = data == null ? null : data.getEngine();
CachedChildGenerator recent = recentChildGenerator;
@@ -533,6 +536,10 @@ public class IrisBiome extends IrisRegistrant implements Rarity {
return random.nextParallelRNG(signature * CHILDREN_SEED_SALT);
}
public static long childStyleSeed(RNG random, int signature) {
return deriveChildStyleSeed(random, signature).getSeed();
}
public KList<NativeBlockState> generateLayers(IrisDimension dim, double wx, double wz, RNG random, int maxDepth, int height, IrisData rdata, IrisComplex complex) {
return IrisBiomeLayerGenerator.generateLayers(this, dim, wx, wz, random, maxDepth, height, rdata, complex);
}
@@ -30,27 +30,41 @@ import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.math.BlockPosition;
import art.arcane.iris.generation.block.B;
import art.arcane.volmlib.util.collection.KList;
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.Arrays;
final class CaveObjectPlacementTransaction implements IObjectPlacer {
private static final int CACHE_MISS = Integer.MIN_VALUE;
private static final int POSITION_BITS = 21;
private static final int POSITION_LIMIT = 1 << (POSITION_BITS - 1);
private static final long POSITION_MASK = (1L << POSITION_BITS) - 1L;
private final IObjectPlacer delegate;
private final Engine engine;
private final int anchorY;
private final int minDepthBelowSurface;
private final int worldHeight;
private final KList<BufferedMutation> mutations;
private final Map<PositionKey, NativeBlockState> bufferedBlocks;
private static final byte BLOCK = 0;
private static final byte TILE = 1;
private static final byte DATA = 2;
private int[] mutationX = new int[64];
private int[] mutationY = new int[64];
private int[] mutationZ = new int[64];
private Object[] mutationValues = new Object[64];
private byte[] mutationKinds = new byte[64];
private boolean[] mutationCanopies = new boolean[64];
private int mutationCount;
private final Long2ObjectOpenHashMap<NativeBlockState> bufferedBlocks;
private final Long2IntOpenHashMap surfaceHeights;
private final Long2IntOpenHashMap caveCeilings;
private int blockWrites;
private BlockPosition canopyPosition;
private boolean originKnown;
private int originX;
private int originZ;
CaveObjectPlacementTransaction(IObjectPlacer delegate, int anchorY, int minDepthBelowSurface) {
this.delegate = delegate;
@@ -58,8 +72,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
this.anchorY = anchorY;
this.minDepthBelowSurface = Math.max(0, minDepthBelowSurface);
this.worldHeight = engine == null ? 0 : engine.getHeight();
this.mutations = new KList<>();
this.bufferedBlocks = new HashMap<>();
this.bufferedBlocks = new Long2ObjectOpenHashMap<>();
this.surfaceHeights = new Long2IntOpenHashMap();
this.caveCeilings = new Long2IntOpenHashMap();
this.surfaceHeights.defaultReturnValue(CACHE_MISS);
@@ -73,28 +86,41 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
}
IrisComplex complex = engine == null ? null : engine.getComplex();
for (BufferedMutation mutation : mutations) {
boolean previousChecked = false;
int previousX = 0;
int previousY = 0;
int previousZ = 0;
for (int index = 0; index < mutationCount; index++) {
int x = mutationX[index];
int y = mutationY[index];
int z = mutationZ[index];
if (previousChecked && x == previousX && y == previousY && z == previousZ) {
continue;
}
if (complex != null
&& !complex.allowsNewDiscreteContentAt(mutation.x(), mutation.z())) {
&& !complex.allowsNewDiscreteContentAt(x, z)) {
discard();
return CommitResult.REJECTED_TRANSITION;
}
if (!isWithinBounds(mutation.x(), mutation.y(), mutation.z())) {
if (!isWithinBounds(x, y, z)) {
discard();
return CommitResult.REJECTED_BOUNDS;
}
SubterrainCell feature = engine.getSubterrainCell(mutation.x(), mutation.y(), mutation.z());
SubterrainCell feature = engine.getSubterrainCell(x, y, z);
if (SubterrainRasterizer.protectsPlacement(feature)
|| feature != null && feature.solid() && delegate.isSolid(mutation.x(), mutation.y(), mutation.z())) {
|| feature != null && feature.solid() && delegate.isSolid(x, y, z)) {
discard();
return CommitResult.REJECTED_SUBTERRAIN;
}
HydrologyCaveCell hydrology = delegate.getData(
mutation.x(), mutation.y(), mutation.z(), HydrologyCaveCell.class);
HydrologyCaveCell hydrology = delegate.getData(x, y, z, HydrologyCaveCell.class);
if (hydrology != null && hydrology.protectsPlacement()) {
discard();
return CommitResult.REJECTED_HYDROLOGY;
}
previousChecked = true;
previousX = x;
previousY = y;
previousZ = z;
}
applyMutations();
@@ -106,18 +132,26 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
boolean canopyOpen = false;
boolean skipCanopy = false;
try {
for (BufferedMutation mutation : mutations) {
if (mutation.value() instanceof NativeBlockState) {
for (int index = 0; index < mutationCount; index++) {
int x = mutationX[index];
int y = mutationY[index];
int z = mutationZ[index];
Object value = mutationValues[index];
boolean canopy = mutationCanopies[index];
if (value instanceof NativeBlockState) {
if (canopyOpen) {
delegate.endTreeCanopy();
canopyOpen = false;
}
skipCanopy = mutation.canopy()
&& !delegate.beginTreeCanopy(new BlockPosition(mutation.x(), mutation.y(), mutation.z()));
canopyOpen = mutation.canopy() && !skipCanopy;
skipCanopy = canopy && !delegate.beginTreeCanopy(new BlockPosition(x, y, z));
canopyOpen = canopy && !skipCanopy;
}
if (!mutation.canopy() || !skipCanopy) {
mutation.apply(delegate);
if (!canopy || !skipCanopy) {
switch (mutationKinds[index]) {
case BLOCK -> delegate.set(x, y, z, (NativeBlockState) value);
case TILE -> delegate.setTile(x, y, z, (TileData) value);
default -> delegate.setData(x, y, z, value);
}
}
}
} finally {
@@ -128,11 +162,31 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
}
void discard() {
mutations.clear();
Arrays.fill(mutationValues, 0, mutationCount, null);
mutationCount = 0;
bufferedBlocks.clear();
blockWrites = 0;
}
private void buffer(byte kind, int x, int y, int z, Object value) {
if (mutationCount == mutationValues.length) {
int capacity = mutationCount * 2;
mutationX = Arrays.copyOf(mutationX, capacity);
mutationY = Arrays.copyOf(mutationY, capacity);
mutationZ = Arrays.copyOf(mutationZ, capacity);
mutationValues = Arrays.copyOf(mutationValues, capacity);
mutationKinds = Arrays.copyOf(mutationKinds, capacity);
mutationCanopies = Arrays.copyOf(mutationCanopies, capacity);
}
mutationX[mutationCount] = x;
mutationY[mutationCount] = y;
mutationZ[mutationCount] = z;
mutationValues[mutationCount] = value;
mutationKinds[mutationCount] = kind;
mutationCanopies[mutationCount] = isCanopy(x, y, z);
mutationCount++;
}
int getCaveCeiling(int x, int z) {
long key = Cache.key(x, z);
int cached = caveCeilings.get(key);
@@ -164,8 +218,8 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
if (state == null) {
return;
}
mutations.add(new BlockMutation(x, y, z, state, isCanopy(x, y, z)));
bufferedBlocks.put(new PositionKey(x, y, z), state);
buffer(BLOCK, x, y, z, state);
bufferedBlocks.put(bufferKey(x, y, z), state);
blockWrites++;
}
@@ -190,7 +244,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
@Override
public NativeBlockState get(int x, int y, int z) {
NativeBlockState buffered = bufferedBlocks.get(new PositionKey(x, y, z));
NativeBlockState buffered = bufferedBlocks.isEmpty() ? null : bufferedBlocks.get(bufferKey(x, y, z));
return buffered == null ? delegate.get(x, y, z) : buffered;
}
@@ -211,7 +265,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
@Override
public boolean isSolid(int x, int y, int z) {
NativeBlockState buffered = bufferedBlocks.get(new PositionKey(x, y, z));
NativeBlockState buffered = bufferedBlocks.isEmpty() ? null : bufferedBlocks.get(bufferKey(x, y, z));
return buffered == null ? delegate.isSolid(x, y, z) : B.isSolid(buffered);
}
@@ -242,7 +296,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
@Override
public void setTile(int x, int y, int z, TileData tile) {
if (tile != null) {
mutations.add(new TileMutation(x, y, z, tile, isCanopy(x, y, z)));
buffer(TILE, x, y, z, tile);
}
}
@@ -251,21 +305,20 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
if (data == null) {
return;
}
mutations.add(new DataMutation(x, y, z, data, isCanopy(x, y, z)));
buffer(DATA, x, y, z, data);
if (data instanceof NativeBlockState state) {
bufferedBlocks.put(new PositionKey(x, y, z), state);
bufferedBlocks.put(bufferKey(x, y, z), state);
blockWrites++;
}
}
@Override
public <T> @Nullable T getData(int x, int y, int z, Class<T> type) {
for (int i = mutations.size() - 1; i >= 0; i--) {
BufferedMutation mutation = mutations.get(i);
if (mutation.x() != x || mutation.y() != y || mutation.z() != z) {
for (int index = mutationCount - 1; index >= 0; index--) {
if (mutationX[index] != x || mutationY[index] != y || mutationZ[index] != z) {
continue;
}
Object value = mutation.value();
Object value = mutationValues[index];
if (type.isInstance(value)) {
return type.cast(value);
}
@@ -278,6 +331,22 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
return engine;
}
private long bufferKey(int x, int y, int z) {
if (!originKnown) {
originX = x;
originZ = z;
originKnown = true;
}
int dx = x - originX;
int dz = z - originZ;
if (dx < -POSITION_LIMIT || dx >= POSITION_LIMIT || dz < -POSITION_LIMIT || dz >= POSITION_LIMIT
|| y < -POSITION_LIMIT || y >= POSITION_LIMIT) {
throw new IllegalStateException("Cave object write at " + x + "," + y + "," + z
+ " is too far from its first write at " + originX + "," + originZ);
}
return ((dx & POSITION_MASK) << (POSITION_BITS * 2)) | ((dz & POSITION_MASK) << POSITION_BITS) | (y & POSITION_MASK);
}
private boolean isWithinBounds(int x, int y, int z) {
if (engine == null || y < 0 || y >= worldHeight) {
return false;
@@ -325,42 +394,4 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
REJECTED_HYDROLOGY,
REJECTED_SUBTERRAIN
}
private interface BufferedMutation {
int x();
int y();
int z();
Object value();
boolean canopy();
void apply(IObjectPlacer placer);
}
private record BlockMutation(int x, int y, int z, NativeBlockState value, boolean canopy) implements BufferedMutation {
@Override
public void apply(IObjectPlacer placer) {
placer.set(x, y, z, value);
}
}
private record TileMutation(int x, int y, int z, TileData value, boolean canopy) implements BufferedMutation {
@Override
public void apply(IObjectPlacer placer) {
placer.setTile(x, y, z, value);
}
}
private record DataMutation(int x, int y, int z, Object value, boolean canopy) implements BufferedMutation {
@Override
public void apply(IObjectPlacer placer) {
placer.setData(x, y, z, value);
}
}
private record PositionKey(int x, int y, int z) {
}
}
@@ -88,6 +88,8 @@ public class MantleObjectComponent extends IrisMantleComponent {
private static final Map<String, CaveRejectLogState> CAVE_REJECT_LOG_STATE = new ConcurrentHashMap<>();
private static final Set<String> MISSING_LOAD_KEY_WARNED = ConcurrentHashMap.newKeySet();
private static final long SOURCE_PLAN_STATS_INTERVAL_MS = 30_000L;
private static final int MAXIMUM_TREE_MATERIALS = 8192;
private static final Map<NativeBlockState, TreeBlockMaterial> TREE_MATERIALS = new ConcurrentHashMap<>();
private final Object collisionRuleLock;
private final ObjectSourcePlanCache sourcePlans;
@@ -124,7 +126,14 @@ public class MantleObjectComponent extends IrisMantleComponent {
}
private static void writeTreeMaterial(IObjectPlacer placer, int x, int y, int z, NativeBlockState data) {
placer.setData(x, y, z, TreeBlockMaterial.of(data));
TreeBlockMaterial material = TREE_MATERIALS.get(data);
if (material == null) {
material = TreeBlockMaterial.of(data);
if (TREE_MATERIALS.size() < MAXIMUM_TREE_MATERIALS) {
TREE_MATERIALS.putIfAbsent(data, material);
}
}
placer.setData(x, y, z, material);
}
@Override
@@ -157,19 +166,23 @@ public class MantleObjectComponent extends IrisMantleComponent {
ObjectDestinationTransaction transaction = new ObjectDestinationTransaction(writer, x, z);
int radius = getRadius();
int sourceChunkRadius = sourceChunkRadius(radius);
replaySourceChunks(
x,
z,
radius,
(sourceX, sourceZ) -> {
ObjectSourcePlan plan = sourcePlans.acquire(sourceX, sourceZ, x, z, sourceChunkRadius,
() -> buildSourcePlan(writer, sourceX, sourceZ, context));
if (plan != null) {
transaction.apply(plan);
plan.persistContinuations(writer, sourceX, sourceZ, x, z);
}
}
);
int side = (sourceChunkRadius * 2) + 1;
ObjectSourcePlan[] plans = new ObjectSourcePlan[side * side];
int[] slot = new int[1];
replaySourceChunks(x, z, radius, (sourceX, sourceZ) -> plans[slot[0]++] = sourcePlans.tryAcquire(
sourceX, sourceZ, x, z, sourceChunkRadius, () -> buildSourcePlan(writer, sourceX, sourceZ, context)));
slot[0] = 0;
replaySourceChunks(x, z, radius, (sourceX, sourceZ) -> {
ObjectSourcePlan plan = plans[slot[0]++];
if (plan == ObjectSourcePlanCache.BUSY) {
plan = sourcePlans.acquire(sourceX, sourceZ, x, z, sourceChunkRadius,
() -> buildSourcePlan(writer, sourceX, sourceZ, context));
}
if (plan != null) {
transaction.apply(plan);
plan.persistContinuations(writer, sourceX, sourceZ, x, z);
}
});
transaction.commit();
logSourcePlanStats();
}
@@ -244,7 +244,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
synchronized (chunk) {
Matter matter = chunk.getOrCreate(y >> 4);
if ((t instanceof NativeBlockState || t instanceof MatterCavern)
&& (hasProtectedHydrology(matter, x, y, z) || protectsSubterrainPlacement(x, y, z))) {
&& hasProtectedHydrology(matter, x, y, z)) {
return;
}
if (t instanceof NativeBlockState) {
@@ -0,0 +1,89 @@
package art.arcane.iris.generation.mantle;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
final class MutationLog {
private static final byte CUSTOM = 1;
private static final byte CANOPY = 2;
private int size;
private int[] x = new int[32];
private int[] y = new int[32];
private int[] z = new int[32];
private Object[] values = new Object[32];
private byte[] flags = new byte[32];
static MutationLog of(List<ObjectDestinationTransaction.Mutation> mutations) {
MutationLog log = new MutationLog();
for (ObjectDestinationTransaction.Mutation mutation : mutations) {
Objects.requireNonNull(mutation, "mutation");
ObjectDestinationTransaction.DataKey key = mutation.key();
switch (mutation) {
case ObjectDestinationTransaction.SetMutation set -> {
Object value = Objects.requireNonNull(set.value(), "mutation value");
if (ObjectSourcePlan.typeOf(value) != key.type()) {
throw new IllegalArgumentException("Object mutation type " + key.type().getName()
+ " does not match its value " + value.getClass().getName());
}
log.add(key.x(), key.y(), key.z(), value, false, set.canopy());
}
case ObjectDestinationTransaction.CustomBlockMutation block -> log.add(key.x(), key.y(), key.z(),
Objects.requireNonNull(block.state(), "custom block state"), true, block.canopy());
}
}
return log;
}
void add(int blockX, int blockY, int blockZ, Object value, boolean custom, boolean canopy) {
if (size == values.length) {
int capacity = size * 2;
x = Arrays.copyOf(x, capacity);
y = Arrays.copyOf(y, capacity);
z = Arrays.copyOf(z, capacity);
values = Arrays.copyOf(values, capacity);
flags = Arrays.copyOf(flags, capacity);
}
x[size] = blockX;
y[size] = blockY;
z[size] = blockZ;
values[size] = value;
flags[size] = (byte) ((custom ? CUSTOM : 0) | (canopy ? CANOPY : 0));
size++;
}
int size() {
return size;
}
int x(int index) {
return x[index];
}
int y(int index) {
return y[index];
}
int z(int index) {
return z[index];
}
Object value(int index) {
return values[index];
}
boolean custom(int index) {
return (flags[index] & CUSTOM) != 0;
}
boolean canopy(int index) {
return (flags[index] & CANOPY) != 0;
}
Class<?> type(int index) {
return custom(index) ? NativeBlockState.class : ObjectSourcePlan.typeOf(values[index]);
}
}
@@ -8,7 +8,6 @@ import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
import art.arcane.iris.generation.subterrain.SubterrainRasterizer;
import art.arcane.iris.generation.block.TileData;
import art.arcane.iris.structure.object.IObjectPlacer;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.math.BlockPosition;
import art.arcane.iris.generation.block.B;
@@ -18,10 +17,11 @@ import org.jetbrains.annotations.Nullable;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -32,7 +32,7 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
private final int destinationChunkX;
private final int destinationChunkZ;
private final int worldHeight;
private final List<Mutation> mutations;
private final MutationLog log = new MutationLog();
private final List<PlacementRange> placements = new ArrayList<>();
private final Long2ObjectOpenHashMap<Int2ObjectOpenHashMap<OverlayCell>> overlay;
private BlockPosition canopyPosition;
@@ -40,13 +40,17 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
private int hydrologyGateChunkX;
private int hydrologyGateChunkZ;
private boolean hydrologyGateOpen;
private boolean subterrainKnown;
private int subterrainX;
private int subterrainY;
private int subterrainZ;
private boolean subterrainProtects;
ObjectDestinationTransaction(MantleWriter writer, int destinationChunkX, int destinationChunkZ) {
this.writer = writer;
this.destinationChunkX = destinationChunkX;
this.destinationChunkZ = destinationChunkZ;
this.worldHeight = writer.getMantle().getWorldHeight();
this.mutations = new ArrayList<>();
this.overlay = new Long2ObjectOpenHashMap<>();
}
@@ -55,11 +59,11 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
}
int mutationCheckpoint() {
return mutations.size();
return log.size();
}
ObjectSourcePlan sourcePlanSince(int checkpoint) {
if (checkpoint < 0 || checkpoint > mutations.size()) {
if (checkpoint < 0 || checkpoint > log.size()) {
throw new IllegalArgumentException("Mutation checkpoint is outside the transaction");
}
ArrayList<PlacementRange> accepted = new ArrayList<>();
@@ -68,65 +72,91 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
accepted.add(new PlacementRange(placement.start() - checkpoint, placement.end() - checkpoint));
}
}
return new ObjectSourcePlan(mutations.subList(checkpoint, mutations.size()), accepted);
return new ObjectSourcePlan(log, checkpoint, accepted);
}
@Override
public int beginObjectPlacement() {
return mutations.size();
return log.size();
}
@Override
public void endObjectPlacement(int checkpoint) {
if (checkpoint >= 0 && checkpoint < mutations.size()) {
placements.add(new PlacementRange(checkpoint, mutations.size()));
if (checkpoint >= 0 && checkpoint < log.size()) {
placements.add(new PlacementRange(checkpoint, log.size()));
}
}
void apply(ObjectSourcePlan plan) {
int slot = plan.destinationSlot(destinationChunkX, destinationChunkZ);
if (slot < 0) {
return;
}
int baseX = destinationChunkX << 4;
int baseZ = destinationChunkZ << 4;
boolean skipCanopy = false;
for (Mutation mutation : plan.mutationsFor(destinationChunkX, destinationChunkZ)) {
if (mutation.key().type() == NativeBlockState.class) {
NativeBlockState existing = mutation.canopy() ? get(mutation.key().x(), mutation.key().y(), mutation.key().z()) : null;
int end = plan.slotEnd(slot);
for (int index = plan.slotStart(slot); index < end; index++) {
int cell = plan.cellAt(index);
int x = baseX | ObjectSourcePlan.cellX(cell);
int y = ObjectSourcePlan.cellY(cell);
int z = baseZ | ObjectSourcePlan.cellZ(cell);
Object value = plan.valueAt(index);
boolean custom = ObjectSourcePlan.isCustom(cell);
boolean canopy = ObjectSourcePlan.isCanopy(cell);
if (custom || value instanceof NativeBlockState) {
NativeBlockState existing = canopy ? get(x, y, z) : null;
skipCanopy = existing != null && existing.isSolid();
}
if (mutation.canopy() && skipCanopy) {
if (canopy && skipCanopy) {
continue;
}
switch (mutation) {
case SetMutation set -> setData(set.key().x(), set.key().y(), set.key().z(), set.value(), set);
case CustomBlockMutation custom -> set(custom.key().x(), custom.key().y(), custom.key().z(), custom.state(), custom);
if (custom) {
set(x, y, z, (NativeBlockState) value, true, canopy);
} else {
setData(x, y, z, value, true, canopy);
}
}
}
private void commitFenced() {
LinkedHashMap<DataKey, Object> originals = new LinkedHashMap<>();
CommitJournal journal = new CommitJournal();
try {
for (Mutation mutation : mutations) {
if (!isDestination(mutation.x(), mutation.z())) {
for (int position = 0; position < log.size(); position++) {
int x = log.x(position);
int z = log.z(position);
if (!isDestination(x, z)) {
continue;
}
captureOriginal(originals, mutation.key());
if (mutation.key().type() == NativeBlockState.class) {
TileWrapper tile = writer.getDataIfPresent(
mutation.key().x(), mutation.key().y(), mutation.key().z(), TileWrapper.class);
if (tile != null) {
originals.putIfAbsent(new DataKey(
mutation.key().x(), mutation.key().y(), mutation.key().z(), TileWrapper.class), tile);
int y = log.y(position);
Object value = log.value(position);
boolean custom = log.custom(position);
Class<?> type = log.type(position);
if (!journal.captured(x, y, z, type)) {
journal.capture(x, y, z, type, publishedOrCleared(x, y, z, type));
}
if (type == NativeBlockState.class) {
if (!journal.captured(x, y, z, TileWrapper.class)) {
TileWrapper tile = writer.getDataIfPresent(x, y, z, TileWrapper.class);
if (tile != null) {
journal.capture(x, y, z, TileWrapper.class, tile);
}
}
Identifier identifier = writer.getDataIfPresent(
mutation.key().x(), mutation.key().y(), mutation.key().z(), Identifier.class);
if (identifier != null || mutation instanceof CustomBlockMutation) {
originals.putIfAbsent(new DataKey(
mutation.key().x(), mutation.key().y(), mutation.key().z(), Identifier.class),
identifier == null ? CLEARED : identifier);
if (!journal.captured(x, y, z, Identifier.class)) {
Identifier identifier = writer.getDataIfPresent(x, y, z, Identifier.class);
if (identifier != null || custom) {
journal.capture(x, y, z, Identifier.class, identifier == null ? CLEARED : identifier);
}
}
}
mutation.apply(writer);
if (custom) {
writer.set(x, y, z, (NativeBlockState) value);
} else {
writer.setData(x, y, z, value);
}
}
} catch (Throwable failure) {
rollback(originals, failure);
journal.rollback(failure);
if (failure instanceof RuntimeException runtimeException) {
throw runtimeException;
}
@@ -149,10 +179,10 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
@Override
public void set(int x, int y, int z, NativeBlockState state) {
set(x, y, z, state, null);
set(x, y, z, state, false, false);
}
private void set(int x, int y, int z, NativeBlockState state, CustomBlockMutation replayed) {
private void set(int x, int y, int z, NativeBlockState state, boolean replayed, boolean replayedCanopy) {
if (state == null) {
return;
}
@@ -167,8 +197,7 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
cell.block = baseState;
cell.tile = CLEARED;
cell.identifier = identifier;
mutations.add(replayed != null
? replayed : new CustomBlockMutation(new DataKey(x, y, z, NativeBlockState.class), state, isCanopy(x, y, z)));
log.add(x, y, z, state, true, replayed ? replayedCanopy : isCanopy(x, y, z));
return;
}
setData(x, y, z, state);
@@ -262,10 +291,10 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
@Override
public <T> void setData(int x, int y, int z, T data) {
setData(x, y, z, data, null);
setData(x, y, z, data, false, false);
}
private void setData(int x, int y, int z, Object data, SetMutation replayed) {
private void setData(int x, int y, int z, Object data, boolean replayed, boolean replayedCanopy) {
if (data == null || y < 0 || y >= worldHeight
|| protectsSubterrain(x, y, z)) {
return;
@@ -285,8 +314,7 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
} else {
cell.put(type, data);
}
mutations.add(replayed != null
? replayed : new SetMutation(new DataKey(x, y, z, type), data, isCanopy(x, y, z)));
log.add(x, y, z, data, false, replayed ? replayedCanopy : isCanopy(x, y, z));
}
@Override
@@ -368,18 +396,6 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
return value == null ? CLEARED : value;
}
private void captureOriginal(LinkedHashMap<DataKey, Object> originals, DataKey key) {
originals.computeIfAbsent(
key,
candidate -> publishedOrCleared(
candidate.x(),
candidate.y(),
candidate.z(),
candidate.type()
)
);
}
private boolean isDestination(int x, int z) {
return (x >> 4) == destinationChunkX && (z >> 4) == destinationChunkZ;
}
@@ -396,8 +412,17 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
}
private boolean protectsSubterrain(int x, int y, int z) {
if (subterrainKnown && x == subterrainX && y == subterrainY && z == subterrainZ) {
return subterrainProtects;
}
Engine engine = getEngine();
return engine != null && SubterrainRasterizer.protectsPlacement(engine.getSubterrainCell(x, y, z));
boolean protects = engine != null && SubterrainRasterizer.protectsPlacement(engine.getSubterrainCell(x, y, z));
subterrainX = x;
subterrainY = y;
subterrainZ = z;
subterrainProtects = protects;
subterrainKnown = true;
return protects;
}
private boolean hasProtectedHydrology(int x, int y, int z) {
@@ -419,22 +444,44 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
return hydrologyGateOpen;
}
private void rollback(LinkedHashMap<DataKey, Object> originals, Throwable failure) {
ArrayList<Map.Entry<DataKey, Object>> entries = new ArrayList<>(originals.entrySet());
for (int index = entries.size() - 1; index >= 0; index--) {
Map.Entry<DataKey, Object> entry = entries.get(index);
DataKey key = entry.getKey();
try {
writer.restoreData(key.x(), key.y(), key.z(), key.type(),
entry.getValue() == CLEARED ? null : entry.getValue());
} catch (Throwable rollbackFailure) {
if (rollbackFailure != failure) {
failure.addSuppressed(rollbackFailure);
private final class CommitJournal {
private final Reference2IntOpenHashMap<Class<?>> typeIds = new Reference2IntOpenHashMap<>();
private final LongOpenHashSet keys = new LongOpenHashSet();
private final ArrayList<Original> originals = new ArrayList<>();
private long key(int x, int y, int z, Class<?> type) {
int typeId = typeIds.computeIfAbsent(type, (Class<?> ignored) -> typeIds.size());
return ((long) typeId << 32) | ((long) y << 8) | ((long) (z & 15) << 4) | (x & 15);
}
private boolean captured(int x, int y, int z, Class<?> type) {
return keys.contains(key(x, y, z, type));
}
private void capture(int x, int y, int z, Class<?> type, Object value) {
if (keys.add(key(x, y, z, type))) {
originals.add(new Original(x, y, z, type, value));
}
}
private void rollback(Throwable failure) {
for (int index = originals.size() - 1; index >= 0; index--) {
Original original = originals.get(index);
try {
writer.restoreData(original.x(), original.y(), original.z(), original.type(),
original.value() == CLEARED ? null : original.value());
} catch (Throwable rollbackFailure) {
if (rollbackFailure != failure) {
failure.addSuppressed(rollbackFailure);
}
}
}
}
}
private record Original(int x, int y, int z, Class<?> type, Object value) {
}
private static final class OverlayCell {
private Object block;
private Object hydrology;
@@ -506,8 +553,6 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
DataKey key();
boolean canopy();
void apply(IObjectPlacer placer);
}
record SetMutation(DataKey key, Object value, boolean canopy) implements Mutation {
@@ -520,11 +565,6 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
public int z() {
return key.z();
}
@Override
public void apply(IObjectPlacer placer) {
placer.setData(key.x(), key.y(), key.z(), value);
}
}
record CustomBlockMutation(DataKey key, NativeBlockState state, boolean canopy) implements Mutation {
@@ -537,10 +577,5 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
public int z() {
return key.z();
}
@Override
public void apply(IObjectPlacer placer) {
placer.set(key.x(), key.y(), key.z(), state);
}
}
}
@@ -20,7 +20,6 @@ import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
final class ObjectSourcePlan {
static final ObjectSourcePlan EMPTY = new ObjectSourcePlan(List.of());
@@ -45,18 +44,21 @@ final class ObjectSourcePlan {
private final int estimatedRetainedBytes;
ObjectSourcePlan(List<ObjectDestinationTransaction.Mutation> mutations) {
this(mutations, List.of());
this(MutationLog.of(mutations), 0, List.of());
}
ObjectSourcePlan(List<ObjectDestinationTransaction.Mutation> mutations, List<ObjectDestinationTransaction.PlacementRange> accepted) {
int count = mutations.size();
this(MutationLog.of(mutations), 0, accepted);
}
ObjectSourcePlan(MutationLog log, int from, List<ObjectDestinationTransaction.PlacementRange> accepted) {
int count = log.size() - from;
long[] mutationDestinations = new long[count];
Long2IntOpenHashMap destinationSlots = new Long2IntOpenHashMap();
destinationSlots.defaultReturnValue(-1);
LongArrayList firstSeen = new LongArrayList();
for (int index = 0; index < count; index++) {
ObjectDestinationTransaction.Mutation mutation = Objects.requireNonNull(mutations.get(index), "mutation");
long destination = destinationKey(mutation.x() >> 4, mutation.z() >> 4);
long destination = destinationKey(log.x(from + index) >> 4, log.z(from + index) >> 4);
mutationDestinations[index] = destination;
if (destinationSlots.putIfAbsent(destination, firstSeen.size()) == -1) {
firstSeen.add(destination);
@@ -80,38 +82,23 @@ final class ObjectSourcePlan {
valueIndexes = new int[count];
ValueTable table = new ValueTable();
for (int index = 0; index < count; index++) {
ObjectDestinationTransaction.Mutation mutation = mutations.get(index);
int logged = from + index;
int position = cursor[destinationSlots.get(mutationDestinations[index])]++;
positions[index] = position;
ObjectDestinationTransaction.DataKey key = mutation.key();
if (key.y() < 0 || key.y() > MAXIMUM_Y) {
throw new IllegalArgumentException("Object mutation height is outside the plan range: " + key.y());
int y = log.y(logged);
if (y < 0 || y > MAXIMUM_Y) {
throw new IllegalArgumentException("Object mutation height is outside the plan range: " + y);
}
Object value;
int custom;
switch (mutation) {
case ObjectDestinationTransaction.SetMutation set -> {
value = Objects.requireNonNull(set.value(), "mutation value");
if (typeOf(value) != key.type()) {
throw new IllegalArgumentException("Object mutation type " + key.type().getName()
+ " does not match its value " + value.getClass().getName());
}
custom = 0;
}
case ObjectDestinationTransaction.CustomBlockMutation block -> {
value = Objects.requireNonNull(block.state(), "custom block state");
custom = CUSTOM_BIT;
}
}
cells[position] = (key.y() << Y_SHIFT) | custom | (mutation.canopy() ? CANOPY_BIT : 0) | ((key.z() & 15) << 4) | (key.x() & 15);
valueIndexes[position] = table.indexOf(value);
cells[position] = (y << Y_SHIFT) | (log.custom(logged) ? CUSTOM_BIT : 0) | (log.canopy(logged) ? CANOPY_BIT : 0)
| ((log.z(logged) & 15) << 4) | (log.x(logged) & 15);
valueIndexes[position] = table.indexOf(log.value(logged));
}
values = table.values.toArray();
ArrayList<Placement> crossing = new ArrayList<>();
for (int ordinal = 0; ordinal < accepted.size(); ordinal++) {
ObjectDestinationTransaction.PlacementRange range = accepted.get(ordinal);
Placement placement = crossingPlacement(ordinal, range.start(), range.end(), mutations, mutationDestinations, destinationSlots, positions);
Placement placement = crossingPlacement(ordinal, range.start(), range.end(), log, from, mutationDestinations, destinationSlots, positions);
if (placement != null) {
crossing.add(placement);
}
@@ -147,12 +134,13 @@ final class ObjectSourcePlan {
int ordinal,
int start,
int end,
List<ObjectDestinationTransaction.Mutation> mutations,
MutationLog log,
int from,
long[] mutationDestinations,
Long2IntOpenHashMap destinationSlots,
int[] positions
) {
if (start < 0 || end > mutations.size() || start >= end) {
if (start < 0 || end > log.size() - from || start >= end) {
throw new IllegalArgumentException("Object placement range is outside the plan");
}
long first = mutationDestinations[start];
@@ -172,11 +160,12 @@ final class ObjectSourcePlan {
int maximumX = Integer.MIN_VALUE;
int maximumZ = Integer.MIN_VALUE;
for (int mutationIndex = start; mutationIndex < end; mutationIndex++) {
ObjectDestinationTransaction.Mutation mutation = mutations.get(mutationIndex);
minimumX = Math.min(minimumX, mutation.x());
minimumZ = Math.min(minimumZ, mutation.z());
maximumX = Math.max(maximumX, mutation.x());
maximumZ = Math.max(maximumZ, mutation.z());
int mutationX = log.x(from + mutationIndex);
int mutationZ = log.z(from + mutationIndex);
minimumX = Math.min(minimumX, mutationX);
minimumZ = Math.min(minimumZ, mutationZ);
maximumX = Math.max(maximumX, mutationX);
maximumZ = Math.max(maximumZ, mutationZ);
long destination = mutationDestinations[mutationIndex];
int touchedIndex = touchedIndexes.get(destination);
if (touchedIndex < 0) {
@@ -216,6 +205,55 @@ final class ObjectSourcePlan {
return destinations.length == 0;
}
int slotStart(int slot) {
return offsets[slot];
}
int slotEnd(int slot) {
return offsets[slot + 1];
}
int cellAt(int index) {
return cells[index];
}
Object valueAt(int index) {
return values[valueIndexes[index]];
}
static int cellX(int cell) {
return cell & 15;
}
static int cellZ(int cell) {
return (cell >>> 4) & 15;
}
static int cellY(int cell) {
return cell >>> Y_SHIFT;
}
static boolean isCustom(int cell) {
return (cell & CUSTOM_BIT) != 0;
}
static boolean isCanopy(int cell) {
return (cell & CANOPY_BIT) != 0;
}
ObjectDestinationTransaction.Mutation mutationAt(int chunkX, int chunkZ, int index) {
int cell = cells[index];
int x = (chunkX << 4) | cellX(cell);
int z = (chunkZ << 4) | cellZ(cell);
int y = cellY(cell);
Object value = valueAt(index);
return isCustom(cell)
? new ObjectDestinationTransaction.CustomBlockMutation(
new ObjectDestinationTransaction.DataKey(x, y, z, NativeBlockState.class), (NativeBlockState) value, isCanopy(cell))
: new ObjectDestinationTransaction.SetMutation(
new ObjectDestinationTransaction.DataKey(x, y, z, typeOf(value)), value, isCanopy(cell));
}
List<ObjectDestinationTransaction.Mutation> mutationsFor(int chunkX, int chunkZ) {
int slot = destinationSlot(chunkX, chunkZ);
return slot < 0 ? List.of() : decode(chunkX, chunkZ, offsets[slot], offsets[slot + 1] - offsets[slot]);
@@ -266,19 +304,8 @@ final class ObjectSourcePlan {
private List<ObjectDestinationTransaction.Mutation> decode(int chunkX, int chunkZ, int start, int length) {
ObjectDestinationTransaction.Mutation[] decoded = new ObjectDestinationTransaction.Mutation[length];
int baseX = chunkX << 4;
int baseZ = chunkZ << 4;
for (int index = 0; index < length; index++) {
int cell = cells[start + index];
int x = baseX | (cell & 15);
int z = baseZ | ((cell >>> 4) & 15);
int y = cell >>> Y_SHIFT;
Object value = values[valueIndexes[start + index]];
decoded[index] = (cell & CUSTOM_BIT) != 0
? new ObjectDestinationTransaction.CustomBlockMutation(
new ObjectDestinationTransaction.DataKey(x, y, z, NativeBlockState.class), (NativeBlockState) value, (cell & CANOPY_BIT) != 0)
: new ObjectDestinationTransaction.SetMutation(
new ObjectDestinationTransaction.DataKey(x, y, z, typeOf(value)), value, (cell & CANOPY_BIT) != 0);
decoded[index] = mutationAt(chunkX, chunkZ, start + index);
}
return List.of(decoded);
}
@@ -4,6 +4,7 @@ import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import art.arcane.volmlib.util.cache.CacheKey;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
@@ -14,6 +15,7 @@ import java.util.function.Supplier;
import static art.arcane.iris.generation.cache.Cache.key;
final class ObjectSourcePlanCache {
static final ObjectSourcePlan BUSY = new ObjectSourcePlan(List.of());
private static final long MAXIMUM_RETAINED_BYTES = 512L * 1024L * 1024L;
private static final int ENTRY_BYTES = 64;
@@ -68,6 +70,19 @@ final class ObjectSourcePlanCache {
return plan;
}
ObjectSourcePlan tryAcquire(int sourceChunkX, int sourceChunkZ, int destinationChunkX, int destinationChunkZ,
int sourceChunkRadius, Supplier<ObjectSourcePlan> builder) {
State current = state;
long source = CacheKey.mix(key(sourceChunkX, sourceChunkZ));
if (current.plans.getIfPresent(source) == null) {
Pending pending = current.pending.get(source);
if (pending != null && pending.owner != Thread.currentThread()) {
return BUSY;
}
}
return acquire(sourceChunkX, sourceChunkZ, destinationChunkX, destinationChunkZ, sourceChunkRadius, builder);
}
private ObjectSourcePlan claim(State current, long source, Entry entry, int destinationChunkX, int destinationChunkZ,
Supplier<ObjectSourcePlan> builder) {
int slot = entry.slot(destinationChunkX, destinationChunkZ);
@@ -0,0 +1,289 @@
package art.arcane.iris.generation.noise;
import art.arcane.volmlib.util.interpolation.IrisInterpolation;
import java.util.Arrays;
public final class GeneratorProgram {
private static final int SURFACE_GRID = 6;
private final IrisGenerator generator;
private final long superSeed;
private final NoiseLayerProgram[] layers;
private final double totalOpacity;
private final NoiseLayerProgram cliffs;
private final boolean multiplicitive;
private final double opacity;
private final double offsetX;
private final double offsetZ;
private final double zoom;
private final double surfaceDetail;
private final double cliffHeightMin;
private final double cliffHeightMax;
private final boolean cellCracks;
GeneratorProgram(IrisGenerator generator, long superSeed, NoiseLayerProgram[] layers, double totalOpacity, NoiseLayerProgram cliffs) {
this.generator = generator;
this.superSeed = superSeed;
this.layers = layers;
this.totalOpacity = totalOpacity;
this.cliffs = cliffs;
this.multiplicitive = generator.isMultiplicitive();
this.opacity = generator.getOpacity();
this.offsetX = generator.getOffsetX();
this.offsetZ = generator.getOffsetZ();
this.zoom = generator.getZoom();
this.surfaceDetail = generator.getSurfaceDetail();
this.cliffHeightMin = generator.getCliffHeightMin();
this.cliffHeightMax = generator.getCliffHeightMax();
this.cellCracks = generator.getCellFractureHeight() != 0;
}
public IrisGenerator generator() {
return generator;
}
public void heights(TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] out) {
if (count == 0) {
return;
}
if (layers.length == 0) {
Arrays.fill(out, 0, count, 0D);
return;
}
if (surfaceDetail == 1D) {
sample(scratch, xs, zs, count, out);
return;
}
surfaceHeights(scratch, xs, zs, count, out);
}
public double height(TerrainBatchScratch scratch, double x, double z) {
if (layers.length == 0) {
return 0D;
}
if (surfaceDetail == 1D) {
return sample(scratch, x, z);
}
double lowerX = Math.floor(x / SURFACE_GRID) * SURFACE_GRID;
double lowerZ = Math.floor(z / SURFACE_GRID) * SURFACE_GRID;
double dx = (x - lowerX) / SURFACE_GRID;
double dz = (z - lowerZ) / SURFACE_GRID;
IrisGenerator.SurfaceCacheStripe[] stripes = generator.surfaceStripes();
double northWest = lattice(scratch, stripes, lowerX, lowerZ);
if (dx == 0D && dz == 0D) {
return northWest;
}
double north = dx == 0D ? northWest
: IrisInterpolation.lerp(northWest, lattice(scratch, stripes, lowerX + SURFACE_GRID, lowerZ), dx);
double smooth = north;
if (dz != 0D) {
double southWest = lattice(scratch, stripes, lowerX, lowerZ + SURFACE_GRID);
double south = dx == 0D ? southWest
: IrisInterpolation.lerp(southWest, lattice(scratch, stripes, lowerX + SURFACE_GRID, lowerZ + SURFACE_GRID), dx);
smooth = IrisInterpolation.lerp(north, south, dz);
}
return surfaceDetail == 0D ? smooth : smooth + (sample(scratch, x, z) - smooth) * surfaceDetail;
}
private double lattice(TerrainBatchScratch scratch, IrisGenerator.SurfaceCacheStripe[] stripes, double x, double z) {
if (x != (int) x || z != (int) z) {
return sample(scratch, x, z);
}
int mark = scratch.mark();
double[] cached = scratch.buffer(1);
boolean hit = IrisGenerator.cachedSurface(stripes, x, z, superSeed, cached, 0);
double value = cached[0];
scratch.reset(mark);
if (hit) {
return value;
}
double height = sample(scratch, x, z);
IrisGenerator.storeSurface(stripes, x, z, superSeed, height);
return height;
}
private double sample(TerrainBatchScratch scratch, double x, double z) {
double sampleX = (x + offsetX) / zoom;
double sampleZ = (z + offsetZ) / zoom;
double value = multiplicitive ? 1D : 0D;
for (NoiseLayerProgram layer : layers) {
double noise = layer.sample(scratch, sampleX, sampleZ);
value = multiplicitive ? value * noise : value + noise;
}
value = multiplicitive ? value * opacity : (value / totalOpacity) * opacity;
if (Double.isNaN(value)) {
value = 0D;
}
if (cliffs != null) {
double height = IrisInterpolation.lerp(cliffHeightMin, cliffHeightMax, cliffs.sample(scratch, sampleX, sampleZ));
value = (Math.round((value * 255D) / height) * height) / 255D;
}
return cellCracks ? generator.cell(x, z, value, 0D) : value;
}
private void surfaceHeights(TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] out) {
int mark = scratch.mark();
int indexMark = scratch.indexMark();
int[] northWest = scratch.indices(count);
int[] northEast = scratch.indices(count);
int[] southWest = scratch.indices(count);
int[] southEast = scratch.indices(count);
double[] fractionX = scratch.buffer(count);
double[] fractionZ = scratch.buffer(count);
scratch.clearLattice(count * 4);
int detailed = 0;
double previousX = Double.NaN;
double previousZ = Double.NaN;
int cellNorthWest = -1;
int cellNorthEast = -1;
int cellSouthWest = -1;
int cellSouthEast = -1;
for (int index = 0; index < count; index++) {
double lowerX = Math.floor(xs[index] / SURFACE_GRID) * SURFACE_GRID;
double lowerZ = Math.floor(zs[index] / SURFACE_GRID) * SURFACE_GRID;
double dx = (xs[index] - lowerX) / SURFACE_GRID;
double dz = (zs[index] - lowerZ) / SURFACE_GRID;
fractionX[index] = dx;
fractionZ[index] = dz;
if (Double.doubleToRawLongBits(lowerX) != Double.doubleToRawLongBits(previousX)
|| Double.doubleToRawLongBits(lowerZ) != Double.doubleToRawLongBits(previousZ)) {
previousX = lowerX;
previousZ = lowerZ;
cellNorthWest = scratch.lattice(lowerX, lowerZ);
cellNorthEast = -1;
cellSouthWest = -1;
cellSouthEast = -1;
}
if (dx != 0D && cellNorthEast < 0) {
cellNorthEast = scratch.lattice(lowerX + SURFACE_GRID, lowerZ);
}
if (dz != 0D && cellSouthWest < 0) {
cellSouthWest = scratch.lattice(lowerX, lowerZ + SURFACE_GRID);
}
if (dx != 0D && dz != 0D && cellSouthEast < 0) {
cellSouthEast = scratch.lattice(lowerX + SURFACE_GRID, lowerZ + SURFACE_GRID);
}
northWest[index] = cellNorthWest;
northEast[index] = dx == 0D ? -1 : cellNorthEast;
southWest[index] = dz == 0D ? -1 : cellSouthWest;
southEast[index] = dx == 0D || dz == 0D ? -1 : cellSouthEast;
if (dx != 0D || dz != 0D) {
detailed++;
}
}
int nodes = scratch.latticeCount();
double[] lattice = scratch.buffer(nodes);
resolveLattice(scratch, nodes, lattice);
double[] detail = null;
int[] detailIndex = null;
if (surfaceDetail != 0D && detailed > 0) {
double[] detailX = scratch.buffer(detailed);
double[] detailZ = scratch.buffer(detailed);
detail = scratch.buffer(detailed);
detailIndex = scratch.indices(count);
int cursor = 0;
for (int index = 0; index < count; index++) {
if (fractionX[index] != 0D || fractionZ[index] != 0D) {
detailX[cursor] = xs[index];
detailZ[cursor] = zs[index];
detailIndex[index] = cursor++;
}
}
sample(scratch, detailX, detailZ, detailed, detail);
}
for (int index = 0; index < count; index++) {
double dx = fractionX[index];
double dz = fractionZ[index];
double nw = lattice[northWest[index]];
if (dx == 0D && dz == 0D) {
out[index] = nw;
continue;
}
double north = dx == 0D ? nw : IrisInterpolation.lerp(nw, lattice[northEast[index]], dx);
double smooth = north;
if (dz != 0D) {
double sw = lattice[southWest[index]];
double south = dx == 0D ? sw : IrisInterpolation.lerp(sw, lattice[southEast[index]], dx);
smooth = IrisInterpolation.lerp(north, south, dz);
}
out[index] = surfaceDetail == 0D ? smooth : smooth + (detail[detailIndex[index]] - smooth) * surfaceDetail;
}
scratch.resetIndices(indexMark);
scratch.reset(mark);
}
private void resolveLattice(TerrainBatchScratch scratch, int nodes, double[] lattice) {
IrisGenerator.SurfaceCacheStripe[] stripes = generator.surfaceStripes();
int indexMark = scratch.indexMark();
int mark = scratch.mark();
int[] missing = scratch.indices(nodes);
double[] missingX = scratch.buffer(nodes);
double[] missingZ = scratch.buffer(nodes);
int missingCount = 0;
for (int node = 0; node < nodes; node++) {
double x = scratch.latticeX(node);
double z = scratch.latticeZ(node);
if (x == (int) x && z == (int) z && IrisGenerator.cachedSurface(stripes, x, z, superSeed, lattice, node)) {
continue;
}
missing[missingCount] = node;
missingX[missingCount] = x;
missingZ[missingCount++] = z;
}
if (missingCount > 0) {
double[] sampled = scratch.buffer(missingCount);
sample(scratch, missingX, missingZ, missingCount, sampled);
for (int index = 0; index < missingCount; index++) {
int node = missing[index];
lattice[node] = sampled[index];
if (missingX[index] == (int) missingX[index] && missingZ[index] == (int) missingZ[index]) {
IrisGenerator.storeSurface(stripes, missingX[index], missingZ[index], superSeed, sampled[index]);
}
}
}
scratch.reset(mark);
scratch.resetIndices(indexMark);
}
private void sample(TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] out) {
int mark = scratch.mark();
double[] sampleX = scratch.buffer(count);
double[] sampleZ = scratch.buffer(count);
double[] noise = scratch.buffer(count);
for (int index = 0; index < count; index++) {
sampleX[index] = (xs[index] + offsetX) / zoom;
sampleZ[index] = (zs[index] + offsetZ) / zoom;
}
Arrays.fill(out, 0, count, multiplicitive ? 1D : 0D);
for (NoiseLayerProgram layer : layers) {
layer.evaluate(scratch, sampleX, sampleZ, count, noise);
if (multiplicitive) {
for (int index = 0; index < count; index++) {
out[index] *= noise[index];
}
} else {
for (int index = 0; index < count; index++) {
out[index] += noise[index];
}
}
}
for (int index = 0; index < count; index++) {
double value = multiplicitive ? out[index] * opacity : (out[index] / totalOpacity) * opacity;
out[index] = Double.isNaN(value) ? 0D : value;
}
if (cliffs != null) {
cliffs.evaluate(scratch, sampleX, sampleZ, count, noise);
for (int index = 0; index < count; index++) {
double height = IrisInterpolation.lerp(cliffHeightMin, cliffHeightMax, noise[index]);
out[index] = (Math.round((out[index] * 255D) / height) * height) / 255D;
}
}
if (cellCracks) {
for (int index = 0; index < count; index++) {
out[index] = generator.cell(xs[index], zs[index], out[index], 0D);
}
}
scratch.reset(mark);
}
}
@@ -41,10 +41,7 @@ import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.function.DoubleBinaryOperator;
@Accessors(chain = true)
@NoArgsConstructor
@@ -211,7 +208,7 @@ public class IrisGenerator extends IrisRegistrant {
if (surfaceDetail == 1D) {
return sampleHeight(rx, rz, superSeed, data, engine);
}
return surfaceHeight(rx, rz, superSeed, data, engine, null);
return surfaceHeight(rx, rz, superSeed, data, engine);
}
public boolean supportsBatch() {
@@ -227,90 +224,84 @@ public class IrisGenerator extends IrisRegistrant {
return !hasCliffs() || cliffHeightGenerator.supportsBatch();
}
public void getHeights(double[] xs, double[] zs, long superSeed, double[] target, int count) {
Objects.checkFromIndexSize(0, count, xs.length);
Objects.checkFromIndexSize(0, count, zs.length);
Objects.checkFromIndexSize(0, count, target.length);
if (count > 4096) {
throw new IllegalArgumentException("Generator batch exceeds 4096 samples");
public GeneratorProgram compileProgram(long superSeed) {
if (!Double.isFinite(surfaceDetail) || surfaceDetail < 0D || surfaceDetail > 1D) {
throw new IllegalArgumentException("Generator surfaceDetail must be finite and between 0 and 1");
}
if (count < 8 || !supportsBatch()) {
for (int index = 0; index < count; index++) {
target[index] = getHeight(xs[index], zs[index], superSeed);
}
return;
}
if (composite.isEmpty()) {
Arrays.fill(target, 0, count, 0D);
return;
if (!supportsBatch()) {
throw new IllegalStateException("Generator " + getLoadKey() + " cannot be compiled into a batch program");
}
IrisData data = getLoader();
Engine engine = data == null ? null : data.getEngine();
DoubleBinaryOperator prepared = prepareHeight(superSeed, data, engine);
for (int index = 0; index < count; index++) {
target[index] = surfaceDetail == 1D ? prepared.applyAsDouble(xs[index], zs[index])
: surfaceHeight(xs[index], zs[index], superSeed, data, engine, prepared);
}
}
private DoubleBinaryOperator prepareHeight(long superSeed, IrisData data, Engine engine) {
int hc = (int) ((cliffHeightMin * 10) + 10 + cliffHeightMax * getSeed() + offsetX + offsetZ);
DoubleBinaryOperator[] layers = new DoubleBinaryOperator[composite.size()];
NoiseLayerProgram[] layers = new NoiseLayerProgram[composite.size()];
double totalOpacity = 0D;
for (int index = 0; index < layers.length; index++) {
IrisNoiseGenerator layer = composite.get(index);
layers[index] = layer.prepareBatch(getSeed() + superSeed + hc, data, engine);
layers[index] = layer.compileProgram(getSeed() + superSeed + hc, data, engine);
if (!multiplicitive) {
totalOpacity += layer.getOpacity();
}
}
double sumOpacity = totalOpacity;
double cliffSeed = (double) (superSeed + 294596 + hc) - 34857;
DoubleBinaryOperator cliffs = hasCliffs() ? cliffHeightGenerator.prepareBatch(
(long) (getSeed() + cliffSeed + hc), data, engine) : null;
return (rx, rz) -> {
double sampleX = (rx + offsetX) / zoom;
double sampleZ = (rz + offsetZ) / zoom;
double h = multiplicitive ? 1D : 0D;
for (DoubleBinaryOperator layer : layers) {
double sampled = layer.applyAsDouble(sampleX, sampleZ);
h = multiplicitive ? h * sampled : h + sampled;
}
double value = multiplicitive ? h * opacity : (h / sumOpacity) * opacity;
if (Double.isNaN(value)) {
value = 0D;
}
if (cliffs != null) {
double height = IrisInterpolation.lerp(cliffHeightMin, cliffHeightMax,
cliffs.applyAsDouble(sampleX, sampleZ));
value = (Math.round((value * 255D) / height) * height) / 255D;
}
return hasCellCracks() ? cell(rx, rz, value, superSeed + 48622 + hc) : value;
};
NoiseLayerProgram cliffs = hasCliffs()
? cliffHeightGenerator.compileProgram((long) (getSeed() + cliffSeed + hc), data, engine)
: null;
return new GeneratorProgram(this, superSeed, layers, totalOpacity, cliffs);
}
private double surfaceHeight(double rx, double rz, long superSeed, IrisData data, Engine engine,
DoubleBinaryOperator prepared) {
SurfaceCacheStripe[] surfaceStripes() {
IrisData data = getLoader();
return surfaceCache(data, data == null ? null : data.getEngine()).stripes();
}
static boolean cachedSurface(SurfaceCacheStripe[] stripes, double x, double z, long superSeed, double[] target, int index) {
long key = ((long) (int) x << 32) | ((int) z & 0xffffffffL);
long mixed = surfaceMix(key);
SurfaceSample cached = stripes[(int) mixed & (SURFACE_CACHE_STRIPES - 1)]
.get(key, superSeed, (int) (mixed >>> 4) & (SURFACE_CACHE_STRIPE_SIZE - 1));
if (cached == null) {
return false;
}
target[index] = cached.height();
return true;
}
static void storeSurface(SurfaceCacheStripe[] stripes, double x, double z, long superSeed, double height) {
long key = ((long) (int) x << 32) | ((int) z & 0xffffffffL);
long mixed = surfaceMix(key);
stripes[(int) mixed & (SURFACE_CACHE_STRIPES - 1)]
.put((int) (mixed >>> 4) & (SURFACE_CACHE_STRIPE_SIZE - 1), new SurfaceSample(key, superSeed, height));
}
private static long surfaceMix(long key) {
long mixed = key ^ (key >>> 33);
mixed *= 0xff51afd7ed558ccdL;
mixed ^= mixed >>> 33;
return mixed;
}
private double surfaceHeight(double rx, double rz, long superSeed, IrisData data, Engine engine) {
SurfaceCacheStripe[] stripes = surfaceCache(data, engine).stripes();
double lowerX = Math.floor(rx / SURFACE_GRID) * SURFACE_GRID;
double lowerZ = Math.floor(rz / SURFACE_GRID) * SURFACE_GRID;
double dx = (rx - lowerX) / SURFACE_GRID;
double dz = (rz - lowerZ) / SURFACE_GRID;
double northWest = cachedSurfaceHeight(lowerX, lowerZ, superSeed, stripes, data, engine, prepared);
double northWest = cachedSurfaceHeight(lowerX, lowerZ, superSeed, stripes, data, engine);
if (dx == 0D && dz == 0D) {
return northWest;
}
double north = dx == 0D ? northWest : IrisInterpolation.lerp(northWest,
cachedSurfaceHeight(lowerX + SURFACE_GRID, lowerZ, superSeed, stripes, data, engine, prepared), dx);
cachedSurfaceHeight(lowerX + SURFACE_GRID, lowerZ, superSeed, stripes, data, engine), dx);
double smooth = north;
if (dz != 0D) {
double southWest = cachedSurfaceHeight(lowerX, lowerZ + SURFACE_GRID, superSeed, stripes, data, engine, prepared);
double southWest = cachedSurfaceHeight(lowerX, lowerZ + SURFACE_GRID, superSeed, stripes, data, engine);
double south = dx == 0D ? southWest : IrisInterpolation.lerp(southWest,
cachedSurfaceHeight(lowerX + SURFACE_GRID, lowerZ + SURFACE_GRID, superSeed, stripes, data, engine, prepared), dx);
cachedSurfaceHeight(lowerX + SURFACE_GRID, lowerZ + SURFACE_GRID, superSeed, stripes, data, engine), dx);
smooth = IrisInterpolation.lerp(north, south, dz);
}
return surfaceDetail == 0D ? smooth
: smooth + ((prepared == null ? sampleHeight(rx, rz, superSeed, data, engine) : prepared.applyAsDouble(rx, rz)) - smooth) * surfaceDetail;
: smooth + (sampleHeight(rx, rz, superSeed, data, engine) - smooth) * surfaceDetail;
}
private double sampleHeight(double rx, double rz, long superSeed, IrisData data, Engine engine) {
@@ -350,21 +341,19 @@ public class IrisGenerator extends IrisRegistrant {
return v;
}
private double cachedSurfaceHeight(double x, double z, long superSeed, SurfaceCacheStripe[] stripes, IrisData data, Engine engine, DoubleBinaryOperator prepared) {
private double cachedSurfaceHeight(double x, double z, long superSeed, SurfaceCacheStripe[] stripes, IrisData data, Engine engine) {
if (x != (int) x || z != (int) z) {
return prepared == null ? sampleHeight(x, z, superSeed, data, engine) : prepared.applyAsDouble(x, z);
return sampleHeight(x, z, superSeed, data, engine);
}
long key = ((long) (int) x << 32) | ((int) z & 0xffffffffL);
long mixed = key ^ (key >>> 33);
mixed *= 0xff51afd7ed558ccdL;
mixed ^= mixed >>> 33;
long mixed = surfaceMix(key);
SurfaceCacheStripe stripe = stripes[(int) mixed & (SURFACE_CACHE_STRIPES - 1)];
int slot = (int) (mixed >>> 4) & (SURFACE_CACHE_STRIPE_SIZE - 1);
SurfaceSample cached = stripe.get(key, superSeed, slot);
if (cached != null) {
return cached.height();
}
double height = prepared == null ? sampleHeight(x, z, superSeed, data, engine) : prepared.applyAsDouble(x, z);
double height = sampleHeight(x, z, superSeed, data, engine);
stripe.put(slot, new SurfaceSample(key, superSeed, height));
return height;
}
@@ -457,7 +446,7 @@ public class IrisGenerator extends IrisRegistrant {
private record SurfaceSample(long key, long seed, double height) {
}
private static final class SurfaceCacheStripe {
static final class SurfaceCacheStripe {
private final AtomicReferenceArray<SurfaceSample> entries = new AtomicReferenceArray<>(SURFACE_CACHE_STRIPE_SIZE);
private SurfaceSample get(long key, long seed, int slot) {
@@ -39,6 +39,7 @@ import art.arcane.volmlib.util.noise.NoiseGenerator;
import art.arcane.volmlib.util.noise.OffsetNoiseGenerator;
import art.arcane.volmlib.util.noise.PerlinNoise;
import art.arcane.volmlib.util.noise.SimplexNoise;
import art.arcane.volmlib.util.noise.program.NoiseProgram;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
@@ -47,8 +48,8 @@ import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.Accessors;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.DoubleBinaryOperator;
@Snippet("generator")
@Accessors(chain = true)
@@ -198,44 +199,29 @@ public class IrisNoiseGenerator {
return true;
}
DoubleBinaryOperator prepareBatch(long superSeed, IrisData data, Engine engine) {
NoiseLayerProgram compileProgram(long superSeed, IrisData data, Engine engine) {
if (!enabled) {
double value = offsetY;
return (x, z) -> value;
return NoiseLayerProgram.disabled(this);
}
if (hasDeepFracture()) {
FractureSamples samples = new FractureSamples();
if (samples.prepare(this, superSeed, data, engine)) {
return samples::sample;
int enabledChildren = 0;
for (IrisNoiseGenerator child : fracture) {
if (child.isEnabled()) {
enabledChildren++;
}
}
DoubleBinaryOperator[] children = new DoubleBinaryOperator[fracture.size()];
double[] offsets = new double[fracture.size()];
NoiseLayerProgram[] children = new NoiseLayerProgram[enabledChildren];
double[] offsets = new double[enabledChildren];
int salt = 33;
for (int index = 0; index < children.length; index++) {
IrisNoiseGenerator child = fracture.get(index);
int compiled = 0;
for (IrisNoiseGenerator child : fracture) {
if (child.isEnabled()) {
children[index] = child.prepareBatch(superSeed + seed + salt, data, engine);
offsets[index] = child.getOpacity() / 2D;
children[compiled] = child.compileProgram(superSeed + seed + salt, data, engine);
offsets[compiled++] = child.getOpacity() / 2D;
}
salt += 819;
}
CNG generator = getGenerator(superSeed, data, engine);
return (x, z) -> samplePrepared(generator, children, offsets, x, z);
}
private double samplePrepared(CNG generator, DoubleBinaryOperator[] children, double[] offsets,
double xv, double zv) {
double x = xv;
double z = zv;
for (int index = 0; index < children.length; index++) {
DoubleBinaryOperator child = children[index];
if (child != null) {
x += child.applyAsDouble(xv, zv) - offsets[index];
z += child.applyAsDouble(zv, xv) - offsets[index];
}
}
return sample(generator, x, z);
CNG generator = Objects.requireNonNull(getGenerator(superSeed, data, engine), "Noise layer generator");
return NoiseLayerProgram.of(this, NoiseProgram.compile2D(generator), children, offsets);
}
public KList<IrisNoiseGenerator> getAllComposites() {
@@ -0,0 +1,184 @@
package art.arcane.iris.generation.noise;
import art.arcane.volmlib.util.interpolation.IrisInterpolation;
import art.arcane.volmlib.util.noise.program.NoiseProgram;
import java.util.Arrays;
public final class NoiseLayerProgram {
private final boolean enabled;
private final double offsetY;
private final NoiseProgram program;
private final double zoom;
private final double opacity;
private final double offsetX;
private final double offsetZ;
private final double exponent;
private final boolean negative;
private final boolean parametric;
private final boolean bezier;
private final boolean sinCentered;
private final NoiseLayerProgram[] children;
private final double[] childOffsets;
private NoiseLayerProgram(IrisNoiseGenerator layer, NoiseProgram program, NoiseLayerProgram[] children, double[] childOffsets) {
this.enabled = program != null;
this.offsetY = layer.getOffsetY();
this.program = program;
this.zoom = layer.getZoom();
this.opacity = layer.getOpacity();
this.offsetX = layer.getOffsetX();
this.offsetZ = layer.getOffsetZ();
this.exponent = layer.getExponent();
this.negative = layer.isNegative();
this.parametric = layer.isParametric();
this.bezier = layer.isBezier();
this.sinCentered = layer.isSinCentered();
this.children = children;
this.childOffsets = childOffsets;
}
static NoiseLayerProgram disabled(IrisNoiseGenerator layer) {
return new NoiseLayerProgram(layer, null, new NoiseLayerProgram[0], new double[0]);
}
static NoiseLayerProgram of(IrisNoiseGenerator layer, NoiseProgram program, NoiseLayerProgram[] children, double[] childOffsets) {
return new NoiseLayerProgram(layer, program, children, childOffsets);
}
public void evaluate(TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] out) {
if (!enabled) {
Arrays.fill(out, 0, count, offsetY);
return;
}
int mark = scratch.mark();
double[] sampleX = scratch.buffer(count);
double[] sampleZ = scratch.buffer(count);
if (children.length == 0) {
for (int index = 0; index < count; index++) {
sampleX[index] = (xs[index] / zoom) + offsetX;
sampleZ[index] = (zs[index] / zoom) + offsetZ;
}
} else {
System.arraycopy(xs, 0, sampleX, 0, count);
System.arraycopy(zs, 0, sampleZ, 0, count);
double[] forward = scratch.buffer(count);
double[] reverse = scratch.buffer(count);
for (int child = 0; child < children.length; child++) {
children[child].evaluatePair(scratch, xs, zs, count, forward, reverse);
double offset = childOffsets[child];
for (int index = 0; index < count; index++) {
sampleX[index] += forward[index] - offset;
sampleZ[index] += reverse[index] - offset;
}
}
for (int index = 0; index < count; index++) {
sampleX[index] = (sampleX[index] / zoom) + offsetX;
sampleZ[index] = (sampleZ[index] / zoom) + offsetZ;
}
}
program.fill2D(scratch.noise(), sampleX, sampleZ, count, out);
for (int index = 0; index < count; index++) {
out[index] = post(out[index]);
}
scratch.reset(mark);
}
void evaluatePair(TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] forward, double[] reverse) {
if (!enabled) {
Arrays.fill(forward, 0, count, offsetY);
Arrays.fill(reverse, 0, count, offsetY);
return;
}
int mark = scratch.mark();
int pairs = count * 2;
double[] sampleX = scratch.buffer(pairs);
double[] sampleZ = scratch.buffer(pairs);
System.arraycopy(xs, 0, sampleX, 0, count);
System.arraycopy(zs, 0, sampleZ, 0, count);
System.arraycopy(zs, 0, sampleX, count, count);
System.arraycopy(xs, 0, sampleZ, count, count);
if (children.length > 0) {
double[] childForward = scratch.buffer(count);
double[] childReverse = scratch.buffer(count);
for (int child = 0; child < children.length; child++) {
children[child].evaluatePair(scratch, xs, zs, count, childForward, childReverse);
double offset = childOffsets[child];
for (int index = 0; index < count; index++) {
sampleX[index] += childForward[index] - offset;
sampleZ[index] += childReverse[index] - offset;
sampleX[count + index] += childReverse[index] - offset;
sampleZ[count + index] += childForward[index] - offset;
}
}
}
for (int index = 0; index < pairs; index++) {
sampleX[index] = (sampleX[index] / zoom) + offsetX;
sampleZ[index] = (sampleZ[index] / zoom) + offsetZ;
}
double[] values = scratch.buffer(pairs);
program.fill2D(scratch.noise(), sampleX, sampleZ, pairs, values);
for (int index = 0; index < count; index++) {
forward[index] = post(values[index]);
reverse[index] = post(values[count + index]);
}
scratch.reset(mark);
}
public double sample(TerrainBatchScratch scratch, double x, double z) {
if (!enabled) {
return offsetY;
}
double sampleX = x;
double sampleZ = z;
if (children.length > 0) {
int mark = scratch.mark();
double[] pair = scratch.buffer(2);
for (int child = 0; child < children.length; child++) {
children[child].samplePair(scratch, x, z, pair);
double offset = childOffsets[child];
sampleX += pair[0] - offset;
sampleZ += pair[1] - offset;
}
scratch.reset(mark);
}
return post(program.sample2D((sampleX / zoom) + offsetX, (sampleZ / zoom) + offsetZ));
}
private void samplePair(TerrainBatchScratch scratch, double x, double z, double[] out) {
if (!enabled) {
out[0] = offsetY;
out[1] = offsetY;
return;
}
double forwardX = x;
double forwardZ = z;
double reverseX = z;
double reverseZ = x;
if (children.length > 0) {
int mark = scratch.mark();
double[] pair = scratch.buffer(2);
for (int child = 0; child < children.length; child++) {
children[child].samplePair(scratch, x, z, pair);
double offset = childOffsets[child];
forwardX += pair[0] - offset;
forwardZ += pair[1] - offset;
reverseX += pair[1] - offset;
reverseZ += pair[0] - offset;
}
scratch.reset(mark);
}
out[0] = post(program.sample2D((forwardX / zoom) + offsetX, (forwardZ / zoom) + offsetZ));
out[1] = post(program.sample2D((reverseX / zoom) + offsetX, (reverseZ / zoom) + offsetZ));
}
private double post(double raw) {
double n = raw * opacity;
n = negative ? (-n + opacity) : n;
n = (exponent != 1 ? n < 0 ? -Math.pow(-n, exponent) : Math.pow(n, exponent) : n) + offsetY;
n = parametric ? IrisInterpolation.parametric(n, 2D) : n;
n = bezier ? IrisInterpolation.bezier(n) : n;
n = sinCentered ? IrisInterpolation.sinCenter(n) : n;
return n;
}
}
@@ -0,0 +1,115 @@
package art.arcane.iris.generation.noise;
import art.arcane.volmlib.util.noise.program.NoiseScratch;
import java.util.Arrays;
public final class TerrainBatchScratch {
private static final int MINIMUM_BUFFER = 256;
private final NoiseScratch noise = new NoiseScratch();
private double[][] buffers = new double[32][];
private int[][] indexBuffers = new int[8][];
private int top;
private int indexTop;
private long[] latticeX = new long[64];
private long[] latticeZ = new long[64];
private int[] latticeSlots = new int[64];
private int latticeCapacity = 64;
private int latticeCount;
public NoiseScratch noise() {
return noise;
}
public int mark() {
return top;
}
public int indexMark() {
return indexTop;
}
public void reset(int mark) {
top = mark;
}
public void resetIndices(int mark) {
indexTop = mark;
}
public double[] buffer(int length) {
if (top == buffers.length) {
buffers = Arrays.copyOf(buffers, top * 2);
}
double[] buffer = buffers[top];
if (buffer == null || buffer.length < length) {
buffer = new double[Math.max(length, MINIMUM_BUFFER)];
buffers[top] = buffer;
}
top++;
return buffer;
}
public int[] indices(int length) {
if (indexTop == indexBuffers.length) {
indexBuffers = Arrays.copyOf(indexBuffers, indexTop * 2);
}
int[] buffer = indexBuffers[indexTop];
if (buffer == null || buffer.length < length) {
buffer = new int[Math.max(length, MINIMUM_BUFFER)];
indexBuffers[indexTop] = buffer;
}
indexTop++;
return buffer;
}
void clearLattice(int maximumEntries) {
int capacity = Integer.highestOneBit(Math.max(16, maximumEntries * 2) - 1) << 1;
if (latticeSlots.length < capacity) {
latticeSlots = new int[capacity];
} else {
Arrays.fill(latticeSlots, 0, capacity, 0);
}
latticeCapacity = capacity;
latticeCount = 0;
}
int lattice(double x, double z) {
long xBits = Double.doubleToRawLongBits(x);
long zBits = Double.doubleToRawLongBits(z);
long mixed = xBits * 0x9E3779B97F4A7C15L ^ Long.rotateLeft(zBits * 0xC2B2AE3D27D4EB4FL, 31);
mixed ^= mixed >>> 29;
int mask = latticeCapacity - 1;
int slot = (int) mixed & mask;
while (true) {
int stored = latticeSlots[slot];
if (stored == 0) {
if (latticeCount == latticeX.length) {
latticeX = Arrays.copyOf(latticeX, latticeCount * 2);
latticeZ = Arrays.copyOf(latticeZ, latticeCount * 2);
}
latticeX[latticeCount] = xBits;
latticeZ[latticeCount] = zBits;
latticeSlots[slot] = ++latticeCount;
return latticeCount - 1;
}
if (latticeX[stored - 1] == xBits && latticeZ[stored - 1] == zBits) {
return stored - 1;
}
slot = (slot + 1) & mask;
}
}
int latticeCount() {
return latticeCount;
}
double latticeX(int index) {
return Double.longBitsToDouble(latticeX[index]);
}
double latticeZ(int index) {
return Double.longBitsToDouble(latticeZ[index]);
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,92 @@
package art.arcane.iris.generation.stream;
import art.arcane.iris.generation.bake.BakePlane;
import art.arcane.iris.generation.bake.ChunkBakeCache;
import art.arcane.iris.generation.bake.ChunkPlanes;
import art.arcane.volmlib.util.stream.BasicStream;
import art.arcane.volmlib.util.stream.ProceduralStream;
import java.util.Objects;
public final class BakedDoubleStream extends BasicStream<Double> implements ChunkFillableStream2D, ChunkFillableDoubleStream2D {
private final ChunkBakeCache cache;
private final BakePlane plane;
private final ProceduralStream<Double> scalar;
public BakedDoubleStream(ChunkBakeCache cache, BakePlane plane, ProceduralStream<Double> scalar) {
super();
this.cache = Objects.requireNonNull(cache, "Bake cache");
this.plane = Objects.requireNonNull(plane, "Bake plane");
this.scalar = Objects.requireNonNull(scalar, "Scalar source");
}
@Override
public Double get(double x, double z) {
return getDouble(x, z);
}
@Override
public double getDouble(double x, double z) {
int blockX = (int) x;
int blockZ = (int) z;
int index = ((blockZ & 15) << 4) | (blockX & 15);
ChunkPlanes.Plane values = cache.planeIfPresent(plane, blockX >> 4, blockZ >> 4);
if (values != null && values.ready(index)) {
return values.doubleAt(index);
}
if (!cache.memoAllowed()) {
return scalar.getDouble(blockX, blockZ);
}
long revision = ProvisionalSampling.revision();
double value = scalar.getDouble(blockX, blockZ);
cache.storeDouble(plane, blockX, blockZ, value, revision);
return value;
}
@Override
public Double get(double x, double y, double z) {
return scalar.get(x, y, z);
}
@Override
public double getDouble(double x, double y, double z) {
return scalar.getDouble(x, y, z);
}
@Override
public double toDouble(Double t) {
return t;
}
@Override
public Double fromDouble(double d) {
return d;
}
@Override
public void fillChunkDoubles(int worldX, int worldZ, double[] target) {
if (target == null || target.length != 256) {
throw new IllegalArgumentException("Expected a 16x16 target array.");
}
int chunkX = worldX >> 4;
int chunkZ = worldZ >> 4;
if (cache.memoAllowed()) {
System.arraycopy(cache.complete(plane, chunkX, chunkZ), 0, target, 0, 256);
return;
}
int originX = chunkX << 4;
int originZ = chunkZ << 4;
for (int index = 0; index < 256; index++) {
target[index] = getDouble(originX + (index & 15), originZ + (index >> 4));
}
}
@Override
public void fillChunkRaw(int worldX, int worldZ, Object[] target) {
double[] values = new double[256];
fillChunkDoubles(worldX, worldZ, values);
for (int index = 0; index < 256; index++) {
target[index] = values[index];
}
}
}
@@ -0,0 +1,100 @@
package art.arcane.iris.generation.stream;
import art.arcane.iris.generation.bake.BakePlane;
import art.arcane.iris.generation.bake.ChunkBakeCache;
import art.arcane.iris.generation.bake.ChunkPlanes;
import art.arcane.volmlib.util.function.NoiseProvider;
import art.arcane.volmlib.util.stream.BasicStream;
import java.util.Objects;
import java.util.function.BooleanSupplier;
public final class BakedHeightStream extends BasicStream<Double> implements ChunkFillableStream2D, ChunkFillableDoubleStream2D {
private final ChunkBakeCache cache;
private final BooleanSupplier mainThread;
private final NoiseProvider nonblocking;
private final NoiseProvider resolved;
public BakedHeightStream(ChunkBakeCache cache, BooleanSupplier mainThread, NoiseProvider nonblocking, NoiseProvider resolved) {
super();
this.cache = Objects.requireNonNull(cache, "Bake cache");
this.mainThread = Objects.requireNonNull(mainThread, "Main thread check");
this.nonblocking = Objects.requireNonNull(nonblocking, "Nonblocking height");
this.resolved = Objects.requireNonNull(resolved, "Resolved height");
}
@Override
public Double get(double x, double z) {
return getDouble(x, z);
}
@Override
public double getDouble(double x, double z) {
if (mainThread.getAsBoolean()) {
return nonblocking.noise(x, z);
}
return column((int) x, (int) z, !ChunkBakeCache.inSparseScope());
}
@Override
public Double get(double x, double y, double z) {
return getDouble(x, z);
}
@Override
public double getDouble(double x, double y, double z) {
return getDouble(x, z);
}
@Override
public double toDouble(Double t) {
return t;
}
@Override
public Double fromDouble(double d) {
return d;
}
@Override
public void fillChunkDoubles(int worldX, int worldZ, double[] target) {
if (target == null || target.length != 256) {
throw new IllegalArgumentException("Expected a 16x16 target array.");
}
boolean main = mainThread.getAsBoolean();
boolean memo = !main && !ChunkBakeCache.inSparseScope();
if (memo && (worldX & 15) == 0 && (worldZ & 15) == 0) {
System.arraycopy(cache.complete(BakePlane.HEIGHT, worldX >> 4, worldZ >> 4), 0, target, 0, 256);
return;
}
for (int index = 0; index < 256; index++) {
int x = worldX + (index & 15);
int z = worldZ + (index >> 4);
target[index] = main ? nonblocking.noise(x, z) : column(x, z, memo);
}
}
@Override
public void fillChunkRaw(int worldX, int worldZ, Object[] target) {
double[] values = new double[256];
fillChunkDoubles(worldX, worldZ, values);
for (int index = 0; index < 256; index++) {
target[index] = values[index];
}
}
private double column(int x, int z, boolean memo) {
int index = ((z & 15) << 4) | (x & 15);
ChunkPlanes.Plane values = cache.planeIfPresent(BakePlane.HEIGHT, x >> 4, z >> 4);
if (values != null && values.ready(index)) {
return values.doubleAt(index);
}
if (!memo) {
return resolved.noise(x, z);
}
long revision = ProvisionalSampling.revision();
double value = resolved.noise(x, z);
cache.storeDouble(BakePlane.HEIGHT, x, z, value, revision);
return value;
}
}
@@ -0,0 +1,71 @@
package art.arcane.iris.generation.stream;
import art.arcane.iris.generation.bake.BakePlane;
import art.arcane.iris.generation.bake.ChunkBakeCache;
import art.arcane.iris.generation.bake.ChunkPlanes;
import art.arcane.volmlib.util.stream.BasicStream;
import art.arcane.volmlib.util.stream.ProceduralStream;
import java.util.Objects;
public final class BakedStream<T> extends BasicStream<T> implements ChunkFillableStream2D {
private final ChunkBakeCache cache;
private final BakePlane plane;
private final ProceduralStream<T> scalar;
public BakedStream(ChunkBakeCache cache, BakePlane plane, ProceduralStream<T> scalar) {
super();
this.cache = Objects.requireNonNull(cache, "Bake cache");
this.plane = Objects.requireNonNull(plane, "Bake plane");
this.scalar = Objects.requireNonNull(scalar, "Scalar source");
}
@Override
@SuppressWarnings("unchecked")
public T get(double x, double z) {
int blockX = (int) x;
int blockZ = (int) z;
int index = ((blockZ & 15) << 4) | (blockX & 15);
ChunkPlanes.Plane values = cache.planeIfPresent(plane, blockX >> 4, blockZ >> 4);
if (values != null && values.ready(index)) {
return (T) values.objectAt(index);
}
if (!cache.memoAllowed()) {
return scalar.get(blockX, blockZ);
}
long revision = ProvisionalSampling.revision();
T value = scalar.get(blockX, blockZ);
cache.storeObject(plane, blockX, blockZ, value, revision);
return value;
}
@Override
public T get(double x, double y, double z) {
return scalar.get(x, y, z);
}
@Override
public double toDouble(T t) {
return scalar.toDouble(t);
}
@Override
public T fromDouble(double d) {
return scalar.fromDouble(d);
}
@Override
public void fillChunkRaw(int worldX, int worldZ, Object[] target) {
int chunkX = worldX >> 4;
int chunkZ = worldZ >> 4;
if (cache.memoAllowed()) {
System.arraycopy(cache.complete(plane, chunkX, chunkZ), 0, target, 0, 256);
return;
}
int originX = chunkX << 4;
int originZ = chunkZ << 4;
for (int index = 0; index < 256; index++) {
target[index] = get(originX + (index & 15), originZ + (index >> 4));
}
}
}
@@ -0,0 +1,64 @@
package art.arcane.iris.generation.stream;
import art.arcane.volmlib.util.math.Rarity;
import art.arcane.volmlib.util.stream.BasicStream;
import art.arcane.volmlib.util.stream.ProceduralStream;
import java.util.List;
import java.util.Objects;
public final class RarityStream<T extends Rarity> extends BasicStream<T> {
private final ProceduralStream<?> noise;
private final Object[] options;
private final double[] cumulative;
public RarityStream(ProceduralStream<?> noise, List<T> options) {
super();
this.noise = Objects.requireNonNull(noise, "Rarity noise");
this.options = options.toArray();
this.cumulative = new double[this.options.length];
double buffer = 0;
for (int index = 0; index < this.options.length; index++) {
buffer += 1d / Rarity.get(this.options[index]);
cumulative[index] = buffer;
}
}
@Override
public T get(double x, double z) {
return pick(noise.getDouble(x, z));
}
@Override
public T get(double x, double y, double z) {
return pick(noise.getDouble(x, y, z));
}
@Override
public double toDouble(T t) {
return 0;
}
@Override
public T fromDouble(double d) {
return null;
}
@SuppressWarnings("unchecked")
public T pick(double noiseValue) {
int count = options.length;
if (count == 0) {
return null;
}
if (count == 1) {
return (T) options[0];
}
double threshold = cumulative[count - 1] * noiseValue;
for (int index = 0; index < count; index++) {
if (cumulative[index] >= threshold) {
return (T) options[index];
}
}
return (T) options[count - 1];
}
}
@@ -3,6 +3,7 @@ package art.arcane.iris.generation.terrain;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.image.IrisImageMapRuntime;
import art.arcane.iris.generation.image.IrisImageMapApplication;
import art.arcane.iris.generation.stream.RarityStream;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.studio.generation.BiomeBuffetLayout;
import art.arcane.volmlib.util.collection.KList;
@@ -24,7 +25,7 @@ public final class RegionSelection {
regions.add(region);
}
}
ProceduralStream<IrisRegion> selected = options.style().selectRarity(regions);
ProceduralStream<IrisRegion> selected = new RarityStream<>(options.style(), regions);
if (!options.imageMaps().has(IrisImageMapApplication.REGION)) {
return selected;
}
@@ -61,6 +61,7 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.BiConsumer;
@@ -144,6 +145,7 @@ public class IrisObject extends IrisRegistrant {
protected transient AtomicCache<AxisAlignedBB> aabb = new AtomicCache<>();
transient final AtomicCache<KList<IrisBlockVector>> surfaceSupportOffsets = new AtomicCache<>();
transient final AtomicCache<FloatingObjectFootprint> floatingFootprint = new AtomicCache<>();
transient final ConcurrentHashMap<ObjectPlacementBake.Key, ObjectPlacementBake> placementBakes = new ConcurrentHashMap<>();
@Getter
@Setter
private transient TreeGrounding treeGrounding;
@@ -30,7 +30,6 @@ import art.arcane.iris.generation.decoration.IrisVacuumSettings;
import art.arcane.iris.world.entity.IrisMarker;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.generation.runtime.GenerationFailures;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.volmlib.nativelib.terrain.structure.NativeStructureVolume;
@@ -52,6 +51,7 @@ import art.arcane.volmlib.util.matter.MatterMarker;
import it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap;
import java.util.Comparator;
import java.util.IdentityHashMap;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
@@ -475,127 +475,53 @@ final class IrisObjectPlacementRunner {
int vacuumLowest = Integer.MAX_VALUE;
int vacuumHighest = Integer.MIN_VALUE;
self.readLock.lock();
KMap<IrisBlockVector, String> markers = null;
try {
VectorMap<NativeBlockState> blocks = self.blocks;
VectorMap<TileData> states = self.states;
TreeGrounding tree = self.getTreeGrounding();
// Zero-tile objects are the common case: skip the two Key allocations VectorMap#get needs to prove null.
boolean hasStates = !states.isEmpty();
ObjectPlacementBake bake = ObjectPlacementBake.of(self, spin, ceilingHang, translating ? translateOffset : null);
PlacementStates palette = new PlacementStates(bake.palette(), config.getRotation(), spinx, spiny, spinz,
preventingDecay, placer.isDebugSmartBore(), excludedStiltMaterials);
double[] exact = warped || hasEdits || eroding ? bake.exact(self, spin) : null;
String[] markers = config.getMarkers().isNotEmpty() && placer.getEngine() != null
? resolveMarkers(config, rng, x, yv, z, rdata, bake) : null;
int[] tileBlocks = bake.tileBlocks();
TileData[] tileValues = bake.tileValues();
int tileCursor = 0;
int count = bake.count();
if (config.getMarkers().isNotEmpty() && placer.getEngine() != null) {
markers = new KMap<>();
KList<IrisBlockVector> list = StreamSupport.stream(blocks.keys().spliterator(), false)
.collect(KList.collector());
// Marker selection persists into the mantle, so it must be seed-deterministic.
// Derive a side stream keyed on the placement position instead of consuming the
// caller's rng: consuming draws there would shift every later placement in the
// chunk and invalidate existing worlds/goldenhashes.
RNG markerRng = rng.nextParallelRNG(((((long) x) << 32) | (z & 0xFFFFFFFFL)) * 31L + yv);
int markerIndex = 0;
for (IrisObjectMarker j : config.getMarkers()) {
IrisMarker marker = self.getLoader().getMarkerLoader().load(j.getMarker());
int markerSalt = markerIndex++;
if (marker == null || marker.isCompatExcluded()) {
continue;
}
int max = j.getMaximumMarkers();
for (IrisBlockVector i : list.shuffleCopy(markerRng.nextParallelRNG(markerSalt))) {
if (max <= 0) {
break;
}
NativeBlockState data = blocks.get(i);
if (data == null) {
continue;
}
for (NativeBlockState k : j.getMark(rdata)) {
if (max <= 0) {
break;
}
if (j.isExact() ? k.matches(data) : IrisObjectShaping.materialKey(k).equals(IrisObjectShaping.materialKey(data))) {
boolean a = !blocks.containsKey((IrisBlockVector) i.clone().add(new IrisBlockVector(0, 1, 0)));
boolean fff = !blocks.containsKey((IrisBlockVector) i.clone().add(new IrisBlockVector(0, 2, 0)));
if (!marker.isEmptyAbove() || (a && fff)) {
markers.put(i, j.getMarker());
max--;
}
}
}
}
}
}
VectorMap<NativeBlockState>.Cursor cursor = blocks.cursor();
while (cursor.next()) {
IrisBlockVector g = cursor.key();
NativeBlockState d;
for (int index = 0; index < count; index++) {
int entry = bake.paletteIndex(index);
TileData tile = null;
if (tileBlocks != null && tileCursor < tileBlocks.length && tileBlocks[tileCursor] == index) {
tile = tileValues[tileCursor++];
}
try {
d = cursor.value();
if (hasStates) {
tile = states.get(g);
if (stilting && palette.stiltLayer(entry)) {
int layer = bake.layerY(index);
if (layer < lowest) {
lowest = layer;
}
} catch (Throwable e) {
GenerationFailures.rethrowEngineFailure(e);
IrisLogging.reportError(e);
IrisLogging.warn("Failed to read block node " + g.getBlockX() + "," + g.getBlockY() + "," + g.getBlockZ() + " in object " + self.getLoadKey() + " (cme)");
d = IrisObject.States.air();
}
if (d == null) {
IrisLogging.warn("Failed to read block node " + g.getBlockX() + "," + g.getBlockY() + "," + g.getBlockZ() + " in object " + self.getLoadKey() + " (null)");
d = IrisObject.States.air();
}
if (placer.isDebugSmartBore() && IrisObject.States.vair().equals(d)) {
d = IrisObject.States.vairDebug();
}
NativeBlockState data = d;
IrisBlockVector i = g.clone();
spin.rotate(i);
if (ceilingHang) {
i.setY(-i.getBlockY());
}
if (translating) {
i.add(translateOffset);
}
if (stilting && IrisObjectShaping.isStiltLayerBlock(data)) {
if (i.getBlockY() < lowest) {
lowest = i.getBlockY();
}
if (i.getBlockY() > topLayer) {
topLayer = i.getBlockY();
if (layer > topLayer) {
topLayer = layer;
}
}
if (preventingDecay && IrisProceduralBlocks.hasProperty(data, "distance") && "false".equals(IrisProceduralBlocks.propertyValue(data, "persistent"))) {
data = data.withProperty("persistent", "true");
}
NativeBlockState data = palette.decayed(entry);
boolean edited = false;
if (hasEdits) {
double exactX = exact[index * 3];
double exactY = exact[index * 3 + 1];
double exactZ = exact[index * 3 + 2];
for (IrisObjectReplace j : config.getEdit()) {
if (rng.chance(j.getChance())) {
for (NativeBlockState k : j.getFind(rdata)) {
if (j.isExact() ? k.matches(data) : IrisObjectShaping.materialKey(k).equals(IrisObjectShaping.materialKey(data))) {
NativeBlockState newData = j.getReplace(rng, i.getX() + x, i.getY() + y, i.getZ() + z, rdata);
NativeBlockState newData = j.getReplace(rng, exactX + x, exactY + y, exactZ + z, rdata);
boolean sameMaterial = IrisObjectShaping.materialKey(newData).equals(IrisObjectShaping.materialKey(data));
if (sameMaterial && !(newData.isCustom() || data.isCustom()))
data = BlockDataMergeSupport.merge(data, newData);
else
data = newData;
edited = true;
Optional<TileData> t = j.getReplace().getTile(rng, x, y, z, rdata);
if (t.isPresent()) {
@@ -609,18 +535,21 @@ final class IrisObjectPlacementRunner {
}
}
data = config.getRotation().rotate(data, spinx, spiny, spinz);
if (data == null) {
StateFacts facts = edited ? palette.factsOf(data) : palette.facts(entry);
NativeBlockState rotated = facts.rotated();
if (rotated == null) {
continue;
}
xx = x + (int) Math.round(i.getX());
int yy = y + (int) Math.round(i.getY());
zz = z + (int) Math.round(i.getZ());
xx = x + bake.offsetX(index);
int yy = y + bake.offsetY(index);
zz = z + bake.offsetZ(index);
if (warped) {
xx += surfaceWarp.fitDouble(-warpHalf, warpHalf, i.getX() + x, i.getY() + y, i.getZ() + z);
zz += surfaceWarp.fitDouble(-warpHalf, warpHalf, i.getZ() + z, i.getY() + y, i.getX() + x);
double exactX = exact[index * 3];
double exactY = exact[index * 3 + 1];
double exactZ = exact[index * 3 + 2];
xx += surfaceWarp.fitDouble(-warpHalf, warpHalf, exactX + x, exactY + y, exactZ + z);
zz += surfaceWarp.fitDouble(-warpHalf, warpHalf, exactZ + z, exactY + y, exactX + x);
}
if (paint) {
@@ -628,8 +557,8 @@ final class IrisObjectPlacementRunner {
if (surface == IrisPaintSurfaceProjection.MISSING) {
continue;
}
if (!B.isVineBlock(data)) {
yy = (int) Math.round(i.getY()) + Math.floorDiv(self.h, 2) + surface + yrand;
if (!B.isVineBlock(rotated)) {
yy = bake.offsetY(index) + Math.floorDiv(self.h, 2) + surface + yrand;
}
}
@@ -637,13 +566,18 @@ final class IrisObjectPlacementRunner {
continue;
}
boolean canopy = tree != null && tree.isCanopy(g.getBlockX(), g.getBlockY(), g.getBlockZ());
boolean canopy = bake.canopy(index);
if (canopy && !placer.beginTreeCanopy(new BlockPosition(xx, yy, zz))) {
continue;
}
try {
data = normalizePlacedWaterlogging(data, placer, xx, yy, zz, waterlogCandidate);
data = rotated;
if (facts.waterloggable()) {
data = waterlogCandidate || facts.authoredWaterlogged()
? IrisProceduralBlocks.normalizeWaterlogging(data, placer.get(xx, yy, zz), waterlogCandidate)
: facts.dry();
}
if (!rawStructurePiece && B.isVineBlock(data)) {
data = attachVineFaces(placer, data, xx, yy, zz);
@@ -652,9 +586,7 @@ final class IrisObjectPlacementRunner {
// Short-circuit order matters for cost only: the mantle read is paid solely
// for vine blocks. Both operands are pure, so the value is unchanged.
boolean wouldReplace = !rawStructurePiece && B.isVineBlock(data) && B.isSolid(placer.get(xx, yy, zz));
String material = IrisObjectShaping.materialKey(data);
boolean air = material.equals("minecraft:air") || material.equals("minecraft:cave_air");
boolean place = shouldPlaceObjectBlock(rawStructurePiece, air, wouldReplace);
boolean place = shouldPlaceObjectBlock(rawStructurePiece, facts.air(), wouldReplace);
if (data.isCustom() || place) {
placer.set(xx, yy, zz, data);
@@ -668,8 +600,8 @@ final class IrisObjectPlacementRunner {
if (tile != null) {
placer.setTile(xx, yy, zz, tile);
}
if (markers != null && markers.containsKey(g)) {
placer.setData(xx, yy, zz, new MatterMarker(markers.get(g)));
if (markers != null && markers[index] != null) {
placer.setData(xx, yy, zz, new MatterMarker(markers[index]));
}
if (listener != null) {
listener.accept(new BlockPosition(xx, yy, zz), data);
@@ -687,27 +619,21 @@ final class IrisObjectPlacementRunner {
}
}
}
} finally {
self.readLock.unlock();
}
if (grounding != null) {
for (TreeGrounding.Extension extension : grounding.extensions()) {
for (int rootY = extension.topY(); rootY >= extension.bottomY(); rootY--) {
NativeBlockState rootState = normalizePlacedWaterlogging(extension.state(), placer,
extension.x(), rootY, extension.z(), waterlogCandidate);
placer.set(extension.x(), rootY, extension.z(), rootState);
if (listener != null) {
listener.accept(new BlockPosition(extension.x(), rootY, extension.z()), rootState);
if (grounding != null) {
for (TreeGrounding.Extension extension : grounding.extensions()) {
for (int rootY = extension.topY(); rootY >= extension.bottomY(); rootY--) {
NativeBlockState rootState = normalizePlacedWaterlogging(extension.state(), placer,
extension.x(), rootY, extension.z(), waterlogCandidate);
placer.set(extension.x(), rootY, extension.z(), rootState);
if (listener != null) {
listener.accept(new BlockPosition(extension.x(), rootY, extension.z()), rootState);
}
}
}
}
}
if (stilting) {
self.readLock.lock();
try {
VectorMap<NativeBlockState> blocks = self.blocks;
if (stilting) {
IrisStiltSettings settings = stiltSettings;
double erodeCentroidX = 0;
@@ -715,93 +641,51 @@ final class IrisObjectPlacementRunner {
double erodeMaxDist = 1;
if (eroding) {
int centroidCount = 0;
VectorMap<NativeBlockState>.Cursor centroidCursor = blocks.cursor();
while (centroidCursor.next()) {
IrisBlockVector rot = centroidCursor.key().clone();
spin.rotate(rot);
if (translating) {
rot.add(translateOffset);
}
if (rot.getBlockY() == lowest) {
NativeBlockState bd = centroidCursor.value();
if (bd != null && IrisObjectShaping.shouldStilt(bd, excludedStiltMaterials)) {
erodeCentroidX += rot.getX();
erodeCentroidZ += rot.getZ();
centroidCount++;
}
for (int index = 0; index < count; index++) {
if (bake.layerY(index) == lowest && palette.shouldStilt(bake.paletteIndex(index))) {
erodeCentroidX += exact[index * 3];
erodeCentroidZ += exact[index * 3 + 2];
centroidCount++;
}
}
if (centroidCount > 0) {
erodeCentroidX /= centroidCount;
erodeCentroidZ /= centroidCount;
}
VectorMap<NativeBlockState>.Cursor spreadCursor = blocks.cursor();
while (spreadCursor.next()) {
IrisBlockVector rot = spreadCursor.key().clone();
spin.rotate(rot);
if (translating) {
rot.add(translateOffset);
}
if (rot.getBlockY() == lowest) {
NativeBlockState bd = spreadCursor.value();
if (bd != null && IrisObjectShaping.shouldStilt(bd, excludedStiltMaterials)) {
double dx = rot.getX() - erodeCentroidX;
double dz = rot.getZ() - erodeCentroidZ;
double dist = Math.sqrt(dx * dx + dz * dz);
if (dist > erodeMaxDist) {
erodeMaxDist = dist;
}
for (int index = 0; index < count; index++) {
if (bake.layerY(index) == lowest && palette.shouldStilt(bake.paletteIndex(index))) {
double dx = exact[index * 3] - erodeCentroidX;
double dz = exact[index * 3 + 2] - erodeCentroidZ;
double dist = Math.sqrt(dx * dx + dz * dz);
if (dist > erodeMaxDist) {
erodeMaxDist = dist;
}
}
}
}
VectorMap<NativeBlockState>.Cursor stiltCursor = blocks.cursor();
while (stiltCursor.next()) {
IrisBlockVector g = stiltCursor.key();
NativeBlockState sourceData;
try {
sourceData = stiltCursor.value();
} catch (Throwable e) {
GenerationFailures.rethrowEngineFailure(e);
IrisLogging.reportError(e);
IrisLogging.warn("Failed to read block node " + g.getBlockX() + "," + g.getBlockY() + "," + g.getBlockZ() + " in object " + self.getLoadKey() + " (stilt cme)");
sourceData = IrisObject.States.air();
}
if (sourceData == null) {
IrisLogging.warn("Failed to read block node " + g.getBlockX() + "," + g.getBlockY() + "," + g.getBlockZ() + " in object " + self.getLoadKey() + " (stilt null)");
sourceData = IrisObject.States.air();
}
if (!IrisObjectShaping.shouldStilt(sourceData, excludedStiltMaterials)) {
int targetLayer = ceilingHang ? topLayer : lowest;
for (int index = 0; index < count; index++) {
int entry = bake.paletteIndex(index);
if (!palette.shouldStilt(entry)) {
continue;
}
NativeBlockState d = sourceData;
NativeBlockState d;
if (settings != null && settings.getPalette() != null) {
d = config.getStiltSettings().getPalette().get(rng, x, y, z, rdata);
} else {
String mat = IrisObjectShaping.materialKey(d);
if (mat.equals("minecraft:grass_block") || mat.equals("minecraft:mycelium") || mat.equals("minecraft:podzol") || mat.equals("minecraft:dirt_path")) {
d = IrisObject.States.dirt();
}
d = palette.stiltDefault(entry);
}
IrisBlockVector i = g.clone();
spin.rotate(i);
if (ceilingHang) {
i.setY(-i.getBlockY());
}
if (translating) {
i.add(translateOffset);
}
int targetLayer = ceilingHang ? topLayer : lowest;
if (i.getBlockY() != targetLayer)
if (bake.layerY(index) != targetLayer)
continue;
d = config.getRotation().rotate(d, spinx, spiny, spinz);
d = palette.rotate(d);
if (hasEdits) {
double exactX = exact[index * 3];
double exactY = exact[index * 3 + 1];
double exactZ = exact[index * 3 + 2];
for (IrisObjectReplace j : config.getEdit()) {
if (rng.chance(j.getChance())) {
for (NativeBlockState k : j.getFind(rdata)) {
@@ -809,7 +693,7 @@ final class IrisObjectPlacementRunner {
continue;
}
if (j.isExact() ? k.matches(d) : IrisObjectShaping.materialKey(k).equals(IrisObjectShaping.materialKey(d))) {
NativeBlockState newData = j.getReplace(rng, i.getX() + x, i.getY() + y, i.getZ() + z, rdata);
NativeBlockState newData = j.getReplace(rng, exactX + x, exactY + y, exactZ + z, rdata);
if (IrisObjectShaping.materialKey(newData).equals(IrisObjectShaping.materialKey(d))) {
d = BlockDataMergeSupport.merge(d, newData);
@@ -822,15 +706,18 @@ final class IrisObjectPlacementRunner {
}
}
if (d == null || !IrisObjectShaping.shouldStilt(d, excludedStiltMaterials))
if (d == null || !palette.shouldStiltPlaced(d))
continue;
xx = x + (int) Math.round(i.getX());
zz = z + (int) Math.round(i.getZ());
xx = x + bake.offsetX(index);
zz = z + bake.offsetZ(index);
if (warped) {
xx += surfaceWarp.fitDouble(-warpHalf, warpHalf, i.getX() + x, i.getY() + y, i.getZ() + z);
zz += surfaceWarp.fitDouble(-warpHalf, warpHalf, i.getZ() + z, i.getY() + y, i.getX() + x);
double exactX = exact[index * 3];
double exactY = exact[index * 3 + 1];
double exactZ = exact[index * 3 + 2];
xx += surfaceWarp.fitDouble(-warpHalf, warpHalf, exactX + x, exactY + y, exactZ + z);
zz += surfaceWarp.fitDouble(-warpHalf, warpHalf, exactZ + z, exactY + y, exactX + x);
}
if (organic) {
@@ -891,8 +778,6 @@ final class IrisObjectPlacementRunner {
int highest = placer.getHighest(xx, zz, self.getLoader(), true);
int lowerBound = highest - 1;
if (settings != null) {
lowerBound -= config.getStiltSettings().getOverStilt() - rng.i(0, config.getStiltSettings().getYRand());
@@ -901,8 +786,8 @@ final class IrisObjectPlacementRunner {
}
if (eroding) {
double dx = i.getX() - erodeCentroidX;
double dz = i.getZ() - erodeCentroidZ;
double dx = exact[index * 3] - erodeCentroidX;
double dz = exact[index * 3 + 2] - erodeCentroidZ;
double normalizedDist = Math.sqrt(dx * dx + dz * dz) / erodeMaxDist;
normalizedDist = Math.min(normalizedDist, 1.0);
int totalDepth = (lowest + y) - lowerBound;
@@ -933,11 +818,10 @@ final class IrisObjectPlacementRunner {
}
placer.set(xx, j, zz, normalizePlacedWaterlogging(d, placer, xx, j, zz, waterlogCandidate));
}
}
} finally {
self.readLock.unlock();
}
} finally {
self.readLock.unlock();
}
if (vacuuming && vacuumLowest != Integer.MAX_VALUE && placer.getEngine() != null) {
@@ -974,9 +858,76 @@ final class IrisObjectPlacementRunner {
if (state == null || !IrisProceduralBlocks.hasProperty(state, "waterlogged")) {
return state;
}
if (!enabled && !"true".equals(IrisProceduralBlocks.propertyValue(state, "waterlogged"))) {
return IrisProceduralBlocks.normalizeWaterlogging(state, null, false);
}
return IrisProceduralBlocks.normalizeWaterlogging(state, placer.get(x, y, z), enabled);
}
private String[] resolveMarkers(IrisObjectPlacement config, RNG rng, int x, int yv, int z, IrisData rdata,
ObjectPlacementBake bake) {
self.readLock.lock();
try {
VectorMap<NativeBlockState> blocks = self.blocks;
KMap<IrisBlockVector, String> markers = new KMap<>();
KList<IrisBlockVector> list = StreamSupport.stream(blocks.keys().spliterator(), false)
.collect(KList.collector());
// Marker selection persists into the mantle, so it must be seed-deterministic.
// Derive a side stream keyed on the placement position instead of consuming the
// caller's rng: consuming draws there would shift every later placement in the
// chunk and invalidate existing worlds/goldenhashes.
RNG markerRng = rng.nextParallelRNG(((((long) x) << 32) | (z & 0xFFFFFFFFL)) * 31L + yv);
int markerIndex = 0;
for (IrisObjectMarker j : config.getMarkers()) {
IrisMarker marker = self.getLoader().getMarkerLoader().load(j.getMarker());
int markerSalt = markerIndex++;
if (marker == null || marker.isCompatExcluded()) {
continue;
}
int max = j.getMaximumMarkers();
for (IrisBlockVector i : list.shuffleCopy(markerRng.nextParallelRNG(markerSalt))) {
if (max <= 0) {
break;
}
NativeBlockState data = blocks.get(i);
if (data == null) {
continue;
}
for (NativeBlockState k : j.getMark(rdata)) {
if (max <= 0) {
break;
}
if (j.isExact() ? k.matches(data) : IrisObjectShaping.materialKey(k).equals(IrisObjectShaping.materialKey(data))) {
boolean a = !blocks.containsKey((IrisBlockVector) i.clone().add(new IrisBlockVector(0, 1, 0)));
boolean fff = !blocks.containsKey((IrisBlockVector) i.clone().add(new IrisBlockVector(0, 2, 0)));
if (!marker.isEmptyAbove() || (a && fff)) {
markers.put(i, j.getMarker());
max--;
}
}
}
}
}
String[] resolved = new String[bake.count()];
VectorMap<NativeBlockState>.Cursor cursor = blocks.cursor();
int index = 0;
while (index < resolved.length && cursor.next()) {
resolved[index++] = markers.get(cursor.key());
}
return resolved;
} finally {
self.readLock.unlock();
}
}
private KList<IrisBlockVector> transformedPaintSupport(SpinKernel spin, IrisBlockVector translateOffset,
CNG warp, double half, int x, int y, int z) {
KList<IrisBlockVector> offsets = new KList<>();
@@ -1337,152 +1288,159 @@ final class IrisObjectPlacementRunner {
return result;
}
/**
* Precomputed vector rotation for a single placement.
* <p>
* This mirrors {@link IrisObjectRotation#rotate(IrisBlockVector, int, int, int)} branch for branch, with two
* differences: it mutates the vector handed to it instead of cloning, and the per-axis angle plus its cosine
* and sine are resolved once instead of once per block. The angle is a pure function of the axis clamp and the
* spin, both fixed for the placement, and Math.cos/Math.sin are pure, so every produced double is identical to
* the value the per-call form would have produced.
*/
private static final class SpinKernel {
private static final int MODE_NONE = 0;
private static final int MODE_FLIP = 1;
private static final int MODE_QUARTER = 2;
private static final int MODE_THREE_QUARTER = 3;
private static final int MODE_ANGLE = 4;
private record StateFacts(NativeBlockState rotated, boolean waterloggable, boolean authoredWaterlogged,
NativeBlockState dry, boolean air) {
private static final StateFacts ROTATED_AWAY = new StateFacts(null, false, false, null, false);
}
private final boolean rotates;
private final int xMode;
private final int yMode;
private final int zMode;
private final double xCos;
private final double xSin;
private final double yCos;
private final double ySin;
private final double zCos;
private final double zSin;
private static final class PlacementStates {
private static final Object ROTATED_AWAY = new Object();
private static final byte UNKNOWN = 0;
private static final byte NO = 1;
private static final byte YES = 2;
private SpinKernel(IrisObjectRotation rotation, int spinx, int spiny, int spinz) {
rotates = rotation.canRotate();
xMode = rotation.canRotateX() ? modeOf(rotation.getXAxis()) : MODE_NONE;
zMode = rotation.canRotateZ() ? modeOf(rotation.getZAxis()) : MODE_NONE;
yMode = rotation.canRotateY() ? modeOf(rotation.getYAxis()) : MODE_NONE;
private final NativeBlockState[] palette;
private final IrisObjectRotation rotation;
private final int spinx;
private final int spiny;
private final int spinz;
private final boolean preventingDecay;
private final boolean debugSmartBore;
private final Set<String> excludedStiltMaterials;
private final NativeBlockState[] decayed;
private final StateFacts[] facts;
private final byte[] stiltLayers;
private final byte[] stilts;
private final NativeBlockState[] stiltDefaults;
private IdentityHashMap<NativeBlockState, Object> rotations;
private IdentityHashMap<NativeBlockState, StateFacts> editedFacts;
private IdentityHashMap<NativeBlockState, Boolean> placedStilts;
double xAngle = xMode == MODE_ANGLE ? rotation.getXRotation(spinx) : 0;
double zAngle = zMode == MODE_ANGLE ? rotation.getZRotation(spinz) : 0;
double yAngle = yMode == MODE_ANGLE ? rotation.getYRotation(spiny) : 0;
xCos = Math.cos(xAngle);
xSin = Math.sin(xAngle);
zCos = Math.cos(zAngle);
zSin = Math.sin(zAngle);
yCos = Math.cos(yAngle);
ySin = Math.sin(yAngle);
private PlacementStates(NativeBlockState[] palette, IrisObjectRotation rotation, int spinx, int spiny, int spinz,
boolean preventingDecay, boolean debugSmartBore, Set<String> excludedStiltMaterials) {
this.palette = palette;
this.rotation = rotation;
this.spinx = spinx;
this.spiny = spiny;
this.spinz = spinz;
this.preventingDecay = preventingDecay;
this.debugSmartBore = debugSmartBore;
this.excludedStiltMaterials = excludedStiltMaterials;
this.decayed = new NativeBlockState[palette.length];
this.facts = new StateFacts[palette.length];
this.stiltLayers = new byte[palette.length];
this.stilts = new byte[palette.length];
this.stiltDefaults = new NativeBlockState[palette.length];
}
private static int modeOf(IrisAxisRotationClamp clamp) {
if (!clamp.isLocked()) {
return MODE_ANGLE;
}
if (Math.abs(clamp.getMax()) % 360D == 180D) {
return MODE_FLIP;
}
if (clamp.getMax() % 360D == 90D || clamp.getMax() % 360D == -270D) {
return MODE_QUARTER;
}
if (clamp.getMax() == -90D || clamp.getMax() % 360D == 270D) {
return MODE_THREE_QUARTER;
}
return MODE_ANGLE;
private NativeBlockState source(int entry) {
NativeBlockState state = palette[entry];
return debugSmartBore && IrisObject.States.vair().equals(state) ? IrisObject.States.vairDebug() : state;
}
/**
* Rotates in place. Axis order (X, then Z, then Y) is load bearing.
*/
private void rotate(IrisBlockVector v) {
if (!rotates) {
return;
private boolean stiltLayer(int entry) {
byte known = stiltLayers[entry];
if (known == UNKNOWN) {
known = IrisObjectShaping.isStiltLayerBlock(source(entry)) ? YES : NO;
stiltLayers[entry] = known;
}
return known == YES;
}
switch (xMode) {
case MODE_FLIP -> {
v.setZ(-v.getZ());
v.setY(-v.getY());
}
case MODE_QUARTER -> {
double z = v.getZ();
v.setZ(v.getY());
v.setY(-z);
}
case MODE_THREE_QUARTER -> {
double z = v.getZ();
v.setZ(-v.getY());
v.setY(z);
}
case MODE_ANGLE -> {
double y = xCos * v.getY() - xSin * v.getZ();
double z = xSin * v.getY() + xCos * v.getZ();
v.setY(y);
v.setZ(z);
}
default -> {
}
private NativeBlockState decayed(int entry) {
NativeBlockState resolved = decayed[entry];
if (resolved == null) {
NativeBlockState data = source(entry);
resolved = preventingDecay && IrisProceduralBlocks.hasProperty(data, "distance")
&& "false".equals(IrisProceduralBlocks.propertyValue(data, "persistent"))
? data.withProperty("persistent", "true") : data;
decayed[entry] = resolved;
}
return resolved;
}
switch (zMode) {
case MODE_FLIP -> {
v.setY(-v.getY());
v.setX(-v.getX());
}
case MODE_QUARTER -> {
double y = v.getY();
v.setY(v.getX());
v.setX(-y);
}
case MODE_THREE_QUARTER -> {
double y = v.getY();
v.setY(-v.getX());
v.setX(y);
}
case MODE_ANGLE -> {
double x = zCos * v.getX() - zSin * v.getY();
double y = zSin * v.getX() + zCos * v.getY();
v.setX(x);
v.setY(y);
}
default -> {
}
private StateFacts facts(int entry) {
StateFacts resolved = facts[entry];
if (resolved == null) {
resolved = computeFacts(decayed(entry));
facts[entry] = resolved;
}
return resolved;
}
switch (yMode) {
case MODE_FLIP -> {
v.setX(-v.getX());
v.setZ(-v.getZ());
}
case MODE_QUARTER -> {
double x = v.getX();
v.setX(v.getZ());
v.setZ(-x);
}
case MODE_THREE_QUARTER -> {
double x = v.getX();
v.setX(-v.getZ());
v.setZ(x);
}
case MODE_ANGLE -> {
double x = yCos * v.getX() + ySin * v.getZ();
double z = -ySin * v.getX() + yCos * v.getZ();
v.setX(x);
v.setZ(z);
}
default -> {
}
private StateFacts factsOf(NativeBlockState state) {
if (editedFacts == null) {
editedFacts = new IdentityHashMap<>();
}
StateFacts resolved = editedFacts.get(state);
if (resolved == null) {
resolved = computeFacts(state);
editedFacts.put(state, resolved);
}
return resolved;
}
private StateFacts computeFacts(NativeBlockState state) {
NativeBlockState rotated = rotate(state);
if (rotated == null) {
return StateFacts.ROTATED_AWAY;
}
boolean waterloggable = IrisProceduralBlocks.hasProperty(rotated, "waterlogged");
boolean authored = waterloggable && "true".equals(IrisProceduralBlocks.propertyValue(rotated, "waterlogged"));
NativeBlockState dry = waterloggable ? IrisProceduralBlocks.normalizeWaterlogging(rotated, null, false) : rotated;
String material = IrisObjectShaping.materialKey(rotated);
boolean air = material.equals("minecraft:air") || material.equals("minecraft:cave_air");
return new StateFacts(rotated, waterloggable, authored, dry, air);
}
private NativeBlockState rotate(NativeBlockState state) {
if (state == null) {
return null;
}
if (rotations == null) {
rotations = new IdentityHashMap<>();
}
Object memo = rotations.get(state);
if (memo == null) {
NativeBlockState rotated = rotation.rotate(state, spinx, spiny, spinz);
memo = rotated == null ? ROTATED_AWAY : rotated;
rotations.put(state, memo);
}
return memo == ROTATED_AWAY ? null : (NativeBlockState) memo;
}
private boolean shouldStilt(int entry) {
byte known = stilts[entry];
if (known == UNKNOWN) {
known = IrisObjectShaping.shouldStilt(palette[entry], excludedStiltMaterials) ? YES : NO;
stilts[entry] = known;
}
return known == YES;
}
private NativeBlockState stiltDefault(int entry) {
NativeBlockState resolved = stiltDefaults[entry];
if (resolved == null) {
NativeBlockState state = palette[entry];
String material = IrisObjectShaping.materialKey(state);
resolved = material.equals("minecraft:grass_block") || material.equals("minecraft:mycelium")
|| material.equals("minecraft:podzol") || material.equals("minecraft:dirt_path")
? IrisObject.States.dirt() : state;
stiltDefaults[entry] = resolved;
}
return resolved;
}
private boolean shouldStiltPlaced(NativeBlockState state) {
if (placedStilts == null) {
placedStilts = new IdentityHashMap<>();
}
Boolean known = placedStilts.get(state);
if (known == null) {
known = IrisObjectShaping.shouldStilt(state, excludedStiltMaterials);
placedStilts.put(state, known);
}
return known;
}
}
@@ -0,0 +1,259 @@
package art.arcane.iris.structure.object;
import art.arcane.iris.generation.block.TileData;
import art.arcane.iris.generation.block.VectorMap;
import art.arcane.iris.generation.decoration.tree.TreeGrounding;
import art.arcane.iris.generation.geometry.IrisBlockVector;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
final class ObjectPlacementBake {
private static final int MAXIMUM_VARIANTS_PER_OBJECT = 32;
private final long blocksRevision;
private final long statesRevision;
private final TreeGrounding grounding;
private final int count;
private final int[] offsetX;
private final int[] offsetY;
private final int[] offsetZ;
private final int[] layerY;
private final short[] paletteIndexes;
private final NativeBlockState[] palette;
private final int[] tileBlocks;
private final TileData[] tileValues;
private final long[] canopy;
private final Key key;
private volatile double[] exact;
private ObjectPlacementBake(Key key, long blocksRevision, long statesRevision, TreeGrounding grounding, int count,
int[] offsetX, int[] offsetY, int[] offsetZ, int[] layerY, short[] paletteIndexes,
NativeBlockState[] palette, int[] tileBlocks, TileData[] tileValues, long[] canopy) {
this.key = key;
this.blocksRevision = blocksRevision;
this.statesRevision = statesRevision;
this.grounding = grounding;
this.count = count;
this.offsetX = offsetX;
this.offsetY = offsetY;
this.offsetZ = offsetZ;
this.layerY = layerY;
this.paletteIndexes = paletteIndexes;
this.palette = palette;
this.tileBlocks = tileBlocks;
this.tileValues = tileValues;
this.canopy = canopy;
}
static ObjectPlacementBake of(IrisObject object, SpinKernel spin, boolean ceilingHang, IrisBlockVector translateOffset) {
Key key = Key.of(spin, ceilingHang, translateOffset);
ConcurrentHashMap<Key, ObjectPlacementBake> variants = object.placementBakes;
ObjectPlacementBake cached = variants.get(key);
if (cached != null && cached.matches(object)) {
return cached;
}
ObjectPlacementBake built = bake(object, key, spin, ceilingHang, translateOffset);
if (built.matches(object)) {
if (variants.size() >= MAXIMUM_VARIANTS_PER_OBJECT) {
variants.clear();
}
variants.put(key, built);
}
return built;
}
private static ObjectPlacementBake bake(IrisObject object, Key key, SpinKernel spin, boolean ceilingHang,
IrisBlockVector translateOffset) {
object.readLock.lock();
try {
VectorMap<NativeBlockState> blocks = object.blocks;
VectorMap<TileData> states = object.states;
long blocksRevision = blocks.modificationRevision();
long statesRevision = states.modificationRevision();
TreeGrounding grounding = object.getTreeGrounding();
boolean hasStates = !states.isEmpty();
IntArrayList x = new IntArrayList();
IntArrayList y = new IntArrayList();
IntArrayList z = new IntArrayList();
IntArrayList layer = new IntArrayList();
IntArrayList indexes = new IntArrayList();
IntArrayList tileBlocks = new IntArrayList();
List<TileData> tileValues = new ArrayList<>();
List<NativeBlockState> palette = new ArrayList<>();
IdentityHashMap<NativeBlockState, Integer> paletteSlots = new IdentityHashMap<>();
IntArrayList canopyBlocks = new IntArrayList();
boolean layerDiffers = false;
IrisBlockVector rotated = new IrisBlockVector(0, 0, 0);
VectorMap<NativeBlockState>.Cursor cursor = blocks.cursor();
int index = 0;
while (cursor.next()) {
IrisBlockVector source = cursor.key();
NativeBlockState state = cursor.value();
if (state == null) {
state = IrisObject.States.air();
}
transform(source, rotated, spin, ceilingHang, translateOffset);
int roundedY = (int) Math.round(rotated.getY());
int floorY = rotated.getBlockY();
x.add((int) Math.round(rotated.getX()));
y.add(roundedY);
z.add((int) Math.round(rotated.getZ()));
layer.add(floorY);
layerDiffers |= floorY != roundedY;
Integer slot = paletteSlots.get(state);
if (slot == null) {
slot = palette.size();
palette.add(state);
paletteSlots.put(state, slot);
}
indexes.add(slot);
if (hasStates) {
TileData tile = states.get(source);
if (tile != null) {
tileBlocks.add(index);
tileValues.add(tile);
}
}
if (grounding != null && grounding.isCanopy(source.getBlockX(), source.getBlockY(), source.getBlockZ())) {
canopyBlocks.add(index);
}
index++;
}
if (palette.size() > Short.MAX_VALUE) {
throw new IllegalStateException("Object " + object.getLoadKey() + " has more than "
+ Short.MAX_VALUE + " distinct block states");
}
short[] paletteIndexes = new short[index];
for (int block = 0; block < index; block++) {
paletteIndexes[block] = (short) indexes.getInt(block);
}
long[] canopy = null;
if (!canopyBlocks.isEmpty()) {
canopy = new long[(index + 63) >>> 6];
for (int block : canopyBlocks) {
canopy[block >>> 6] |= 1L << (block & 63);
}
}
return new ObjectPlacementBake(key, blocksRevision, statesRevision, grounding, index,
x.toIntArray(), y.toIntArray(), z.toIntArray(), layerDiffers ? layer.toIntArray() : null,
paletteIndexes, palette.toArray(new NativeBlockState[0]),
tileBlocks.isEmpty() ? null : tileBlocks.toIntArray(),
tileValues.isEmpty() ? null : tileValues.toArray(new TileData[0]), canopy);
} finally {
object.readLock.unlock();
}
}
private static void transform(IrisBlockVector source, IrisBlockVector target, SpinKernel spin, boolean ceilingHang,
IrisBlockVector translateOffset) {
target.setX(source.getX());
target.setY(source.getY());
target.setZ(source.getZ());
spin.rotate(target);
if (ceilingHang) {
target.setY(-target.getBlockY());
}
if (translateOffset != null) {
target.add(translateOffset);
}
}
boolean matches(IrisObject object) {
return object.blocks.modificationRevision() == blocksRevision
&& object.states.modificationRevision() == statesRevision
&& object.getTreeGrounding() == grounding;
}
int count() {
return count;
}
int offsetX(int index) {
return offsetX[index];
}
int offsetY(int index) {
return offsetY[index];
}
int offsetZ(int index) {
return offsetZ[index];
}
int layerY(int index) {
return layerY == null ? offsetY[index] : layerY[index];
}
int paletteIndex(int index) {
return paletteIndexes[index];
}
NativeBlockState[] palette() {
return palette;
}
int[] tileBlocks() {
return tileBlocks;
}
TileData[] tileValues() {
return tileValues;
}
boolean canopy(int index) {
return canopy != null && (canopy[index >>> 6] & (1L << (index & 63))) != 0L;
}
double[] exact(IrisObject object, SpinKernel spin) {
double[] resolved = exact;
if (resolved != null) {
return resolved;
}
object.readLock.lock();
try {
resolved = new double[count * 3];
IrisBlockVector rotated = new IrisBlockVector(0, 0, 0);
VectorMap<NativeBlockState>.Cursor cursor = object.blocks.cursor();
int index = 0;
while (cursor.next() && index < count) {
transform(cursor.key(), rotated, spin, key.ceilingHang(), key.translateOffset());
resolved[index * 3] = rotated.getX();
resolved[index * 3 + 1] = rotated.getY();
resolved[index * 3 + 2] = rotated.getZ();
index++;
}
if (index != count) {
throw new IllegalStateException("Object " + object.getLoadKey() + " changed while its placement bake was read");
}
} finally {
object.readLock.unlock();
}
exact = resolved;
return resolved;
}
record Key(boolean rotates, int xMode, int yMode, int zMode, long xCos, long xSin, long yCos, long ySin,
long zCos, long zSin, boolean ceilingHang, boolean translating, long translateX, long translateY,
long translateZ) {
static Key of(SpinKernel spin, boolean ceilingHang, IrisBlockVector translateOffset) {
boolean translating = translateOffset != null;
return new Key(spin.rotates, spin.xMode, spin.yMode, spin.zMode,
Double.doubleToLongBits(spin.xCos), Double.doubleToLongBits(spin.xSin),
Double.doubleToLongBits(spin.yCos), Double.doubleToLongBits(spin.ySin),
Double.doubleToLongBits(spin.zCos), Double.doubleToLongBits(spin.zSin), ceilingHang, translating,
translating ? Double.doubleToLongBits(translateOffset.getX()) : 0L,
translating ? Double.doubleToLongBits(translateOffset.getY()) : 0L,
translating ? Double.doubleToLongBits(translateOffset.getZ()) : 0L);
}
IrisBlockVector translateOffset() {
return translating ? new IrisBlockVector(Double.longBitsToDouble(translateX),
Double.longBitsToDouble(translateY), Double.longBitsToDouble(translateZ)) : null;
}
}
}
@@ -0,0 +1,152 @@
package art.arcane.iris.structure.object;
import art.arcane.iris.generation.geometry.IrisBlockVector;
/**
* Precomputed vector rotation for a single placement.
* <p>
* This mirrors {@link IrisObjectRotation#rotate(IrisBlockVector, int, int, int)} branch for branch, with two
* differences: it mutates the vector handed to it instead of cloning, and the per-axis angle plus its cosine
* and sine are resolved once instead of once per block. The angle is a pure function of the axis clamp and the
* spin, both fixed for the placement, and Math.cos/Math.sin are pure, so every produced double is identical to
* the value the per-call form would have produced.
*/
final class SpinKernel {
static final int MODE_NONE = 0;
static final int MODE_FLIP = 1;
static final int MODE_QUARTER = 2;
static final int MODE_THREE_QUARTER = 3;
static final int MODE_ANGLE = 4;
final boolean rotates;
final int xMode;
final int yMode;
final int zMode;
final double xCos;
final double xSin;
final double yCos;
final double ySin;
final double zCos;
final double zSin;
SpinKernel(IrisObjectRotation rotation, int spinx, int spiny, int spinz) {
rotates = rotation.canRotate();
xMode = rotation.canRotateX() ? modeOf(rotation.getXAxis()) : MODE_NONE;
zMode = rotation.canRotateZ() ? modeOf(rotation.getZAxis()) : MODE_NONE;
yMode = rotation.canRotateY() ? modeOf(rotation.getYAxis()) : MODE_NONE;
double xAngle = xMode == MODE_ANGLE ? rotation.getXRotation(spinx) : 0;
double zAngle = zMode == MODE_ANGLE ? rotation.getZRotation(spinz) : 0;
double yAngle = yMode == MODE_ANGLE ? rotation.getYRotation(spiny) : 0;
xCos = Math.cos(xAngle);
xSin = Math.sin(xAngle);
zCos = Math.cos(zAngle);
zSin = Math.sin(zAngle);
yCos = Math.cos(yAngle);
ySin = Math.sin(yAngle);
}
private static int modeOf(IrisAxisRotationClamp clamp) {
if (!clamp.isLocked()) {
return MODE_ANGLE;
}
if (Math.abs(clamp.getMax()) % 360D == 180D) {
return MODE_FLIP;
}
if (clamp.getMax() % 360D == 90D || clamp.getMax() % 360D == -270D) {
return MODE_QUARTER;
}
if (clamp.getMax() == -90D || clamp.getMax() % 360D == 270D) {
return MODE_THREE_QUARTER;
}
return MODE_ANGLE;
}
/**
* Rotates in place. Axis order (X, then Z, then Y) is load bearing.
*/
void rotate(IrisBlockVector v) {
if (!rotates) {
return;
}
switch (xMode) {
case MODE_FLIP -> {
v.setZ(-v.getZ());
v.setY(-v.getY());
}
case MODE_QUARTER -> {
double z = v.getZ();
v.setZ(v.getY());
v.setY(-z);
}
case MODE_THREE_QUARTER -> {
double z = v.getZ();
v.setZ(-v.getY());
v.setY(z);
}
case MODE_ANGLE -> {
double y = xCos * v.getY() - xSin * v.getZ();
double z = xSin * v.getY() + xCos * v.getZ();
v.setY(y);
v.setZ(z);
}
default -> {
}
}
switch (zMode) {
case MODE_FLIP -> {
v.setY(-v.getY());
v.setX(-v.getX());
}
case MODE_QUARTER -> {
double y = v.getY();
v.setY(v.getX());
v.setX(-y);
}
case MODE_THREE_QUARTER -> {
double y = v.getY();
v.setY(-v.getX());
v.setX(y);
}
case MODE_ANGLE -> {
double x = zCos * v.getX() - zSin * v.getY();
double y = zSin * v.getX() + zCos * v.getY();
v.setX(x);
v.setY(y);
}
default -> {
}
}
switch (yMode) {
case MODE_FLIP -> {
v.setX(-v.getX());
v.setZ(-v.getZ());
}
case MODE_QUARTER -> {
double x = v.getX();
v.setX(v.getZ());
v.setZ(-x);
}
case MODE_THREE_QUARTER -> {
double x = v.getX();
v.setX(-v.getZ());
v.setZ(x);
}
case MODE_ANGLE -> {
double x = yCos * v.getX() + ySin * v.getZ();
double z = -ySin * v.getX() + yCos * v.getZ();
v.setX(x);
v.setZ(z);
}
default -> {
}
}
}
}
@@ -0,0 +1,81 @@
package art.arcane.iris.generation.mantle;
import art.arcane.iris.testsupport.KeyedBlockState;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class MutationLogTest {
@Test
public void logKeepsPositionsValuesAndFlagsInOrder() {
NativeBlockState state = new KeyedBlockState("minecraft:stone");
MutationLog log = MutationLog.of(List.of(
new ObjectDestinationTransaction.SetMutation(new ObjectDestinationTransaction.DataKey(3, 70, -2, String.class), "marker", true),
new ObjectDestinationTransaction.CustomBlockMutation(new ObjectDestinationTransaction.DataKey(-17, 4, 40, NativeBlockState.class), state, false)));
for (int index = 0; index < 100; index++) {
log.add(index, index + 1, -index, "grow" + index, (index & 1) == 0, (index & 2) == 0);
}
assertEquals(102, log.size());
assertEquals(3, log.x(0));
assertEquals(70, log.y(0));
assertEquals(-2, log.z(0));
assertEquals("marker", log.value(0));
assertFalse(log.custom(0));
assertTrue(log.canopy(0));
assertSame(String.class, log.type(0));
assertSame(state, log.value(1));
assertTrue(log.custom(1));
assertFalse(log.canopy(1));
assertSame(NativeBlockState.class, log.type(1));
assertEquals(99, log.x(101));
assertEquals("grow99", log.value(101));
assertFalse(log.custom(101));
assertTrue(log.canopy(98));
assertFalse(log.canopy(100));
}
@Test
public void mismatchedMutationTypesAreRejected() {
assertThrows(IllegalArgumentException.class, () -> MutationLog.of(List.of(
new ObjectDestinationTransaction.SetMutation(new ObjectDestinationTransaction.DataKey(0, 4, 0, Integer.class), "text", false))));
}
@Test
public void planSlicesMatchThePlanBuiltFromTheSameMutations() {
MutationLog log = new MutationLog();
List<ObjectDestinationTransaction.Mutation> tail = new ArrayList<>();
for (int index = 0; index < 64; index++) {
int x = (index * 7) - 30;
int y = 4 + index;
int z = (index * 5) - 20;
String value = "value" + (index % 9);
boolean canopy = index % 4 == 0;
log.add(x, y, z, value, false, canopy);
if (index >= 10) {
tail.add(new ObjectDestinationTransaction.SetMutation(new ObjectDestinationTransaction.DataKey(x, y, z, String.class), value, canopy));
}
}
ObjectSourcePlan sliced = new ObjectSourcePlan(log, 10, List.of());
ObjectSourcePlan expected = new ObjectSourcePlan(tail);
assertEquals(expected.destinationCount(), sliced.destinationCount());
int total = 0;
for (int chunkX = -3; chunkX <= 30; chunkX++) {
for (int chunkZ = -3; chunkZ <= 30; chunkZ++) {
List<ObjectDestinationTransaction.Mutation> mutations = sliced.mutationsFor(chunkX, chunkZ);
assertEquals(expected.mutationsFor(chunkX, chunkZ), mutations);
total += mutations.size();
}
}
assertEquals(54, total);
}
}
@@ -163,6 +163,44 @@ public class ObjectSourcePlanCacheTest {
}
}
@Test
public void tryAcquireReportsBusySourcesWithoutConsumingThem() throws Exception {
ObjectSourcePlanCache cache = new ObjectSourcePlanCache(1024L * 1024L);
ObjectSourcePlan plan = planTouching(new int[]{0, 0}, new int[]{1, 0});
CountDownLatch entered = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
AtomicInteger builds = new AtomicInteger();
ExecutorService executor = Executors.newFixedThreadPool(1);
try {
Future<ObjectSourcePlan> owner = executor.submit(() -> cache.acquire(0, 0, 0, 0, 7, () -> {
builds.incrementAndGet();
entered.countDown();
await(release);
return plan;
}));
assertTrue(entered.await(5L, TimeUnit.SECONDS));
assertSame(ObjectSourcePlanCache.BUSY, cache.tryAcquire(0, 0, 1, 0, 7, () -> {
builds.incrementAndGet();
return plan;
}));
ObjectSourcePlan unrelated = planTouching(new int[]{5, 5});
assertSame(unrelated, cache.tryAcquire(5, 5, 5, 5, 7, () -> unrelated));
release.countDown();
assertSame(plan, owner.get(5L, TimeUnit.SECONDS));
assertSame(plan, cache.tryAcquire(0, 0, 1, 0, 7, () -> {
builds.incrementAndGet();
return plan;
}));
assertEquals(1, builds.get());
assertEquals(2L, cache.stats().drained());
} finally {
release.countDown();
executor.shutdownNow();
}
}
@Test
public void concurrentDestinationsShareOneSourceBuild() throws Exception {
ObjectSourcePlanCache cache = new ObjectSourcePlanCache(1024L * 1024L);
@@ -0,0 +1,160 @@
package art.arcane.iris.generation.noise;
import art.arcane.volmlib.util.collection.KList;
import org.junit.Test;
import java.util.SplittableRandom;
import static org.junit.Assert.assertEquals;
public class GeneratorProgramParityTest {
private static final NoiseStyle[] STYLES = {
NoiseStyle.SIMPLEX, NoiseStyle.PERLIN, NoiseStyle.IRIS, NoiseStyle.IRIS_HALF, NoiseStyle.FRACTAL_SMOKE,
NoiseStyle.FRACTAL_WATER, NoiseStyle.CLOVER, NoiseStyle.CELLULAR, NoiseStyle.NOWHERE,
NoiseStyle.CELLULAR_IRIS_DOUBLE, NoiseStyle.SIMPLEX_VASCULAR, NoiseStyle.FLAT, NoiseStyle.STATIC,
NoiseStyle.GLOB_IRIS_THICK, NoiseStyle.FRACTAL_BILLOW_PERLIN, NoiseStyle.PERLIN_IRIS
};
@Test
public void programHeightsMatchScalarHeightsBitForBit() {
SplittableRandom random = new SplittableRandom(0x5EED1E55L);
TerrainBatchScratch scratch = new TerrainBatchScratch();
for (int generatorIndex = 0; generatorIndex < 160; generatorIndex++) {
long shapeSeed = random.nextLong();
IrisGenerator scalar = generator(new SplittableRandom(shapeSeed));
IrisGenerator batch = generator(new SplittableRandom(shapeSeed));
long superSeed = random.nextLong();
GeneratorProgram program = batch.compileProgram(superSeed);
int count = 1 + random.nextInt(600);
double[] xs = new double[count];
double[] zs = new double[count];
double[] out = new double[count];
fillCoordinates(random, xs, zs, count);
program.heights(scratch, xs, zs, count, out);
for (int index = 0; index < count; index++) {
assertEquals("generator " + generatorIndex + " point " + xs[index] + "," + zs[index],
Double.doubleToRawLongBits(scalar.getHeight(xs[index], zs[index], superSeed)),
Double.doubleToRawLongBits(out[index]));
}
}
}
@Test
public void singlePointProgramHeightsMatchScalarHeightsBitForBit() {
SplittableRandom random = new SplittableRandom(0xFEEDL);
TerrainBatchScratch scratch = new TerrainBatchScratch();
for (int generatorIndex = 0; generatorIndex < 160; generatorIndex++) {
long shapeSeed = random.nextLong();
IrisGenerator scalar = generator(new SplittableRandom(shapeSeed));
IrisGenerator compiled = generator(new SplittableRandom(shapeSeed));
long superSeed = random.nextLong();
GeneratorProgram program = compiled.compileProgram(superSeed);
for (int sample = 0; sample < 400; sample++) {
double x = coordinate(random);
double z = random.nextInt(5) == 0 ? x : coordinate(random);
assertEquals("generator " + generatorIndex + " point " + x + "," + z,
Double.doubleToRawLongBits(scalar.getHeight(x, z, superSeed)),
Double.doubleToRawLongBits(program.height(scratch, x, z)));
}
}
}
@Test
public void chunkGridsMatchScalarHeightsBitForBit() {
SplittableRandom random = new SplittableRandom(0xC0FFEEL);
TerrainBatchScratch scratch = new TerrainBatchScratch();
double[] xs = new double[256];
double[] zs = new double[256];
double[] out = new double[256];
for (int generatorIndex = 0; generatorIndex < 60; generatorIndex++) {
long shapeSeed = random.nextLong();
IrisGenerator scalar = generator(new SplittableRandom(shapeSeed));
IrisGenerator batch = generator(new SplittableRandom(shapeSeed));
long superSeed = random.nextLong();
GeneratorProgram program = batch.compileProgram(superSeed);
for (int chunk = 0; chunk < 4; chunk++) {
int originX = random.nextInt(-200_000, 200_000) << 4;
int originZ = random.nextInt(-200_000, 200_000) << 4;
for (int index = 0; index < 256; index++) {
xs[index] = originX + (index & 15);
zs[index] = originZ + (index >> 4);
}
program.heights(scratch, xs, zs, 256, out);
for (int index = 0; index < 256; index++) {
assertEquals(Double.doubleToRawLongBits(scalar.getHeight(xs[index], zs[index], superSeed)),
Double.doubleToRawLongBits(out[index]));
}
}
}
}
private static void fillCoordinates(SplittableRandom random, double[] xs, double[] zs, int count) {
for (int index = 0; index < count; index++) {
xs[index] = coordinate(random);
zs[index] = random.nextInt(5) == 0 ? xs[index] : coordinate(random);
}
}
private static double coordinate(SplittableRandom random) {
return switch (random.nextInt(6)) {
case 0 -> random.nextInt(-4096, 4096);
case 1 -> random.nextInt(-30_000_000, 30_000_000);
case 2 -> random.nextInt(-512, 512) + random.nextDouble();
case 3 -> random.nextInt(-64, 64) * 6;
case 4 -> random.nextDouble(-30_000_000D, 30_000_000D);
default -> random.nextInt(-16, 16) * 0.25D;
};
}
private static IrisGenerator generator(SplittableRandom random) {
int layers = 1 + random.nextInt(3);
KList<IrisNoiseGenerator> composite = new KList<>();
for (int layer = 0; layer < layers; layer++) {
composite.add(layer(random, 0));
}
double[] details = {0D, 0.25D, 0.5D, 1D};
IrisGenerator generator = new IrisGenerator().setComposite(composite)
.setSurfaceDetail(details[random.nextInt(details.length)])
.setMultiplicitive(random.nextInt(4) == 0)
.setSeed(random.nextLong(-5000L, 5000L))
.setZoom(0.2D + random.nextDouble() * 3D)
.setOffsetX(random.nextInt(-40, 40) * 0.25D)
.setOffsetZ(random.nextInt(-40, 40) * 0.25D)
.setOpacity(0.1D + random.nextDouble());
if (random.nextInt(3) == 0) {
generator.setCliffHeightMin(random.nextDouble() * 4D).setCliffHeightMax(4D + random.nextDouble() * 12D)
.setCliffHeightGenerator(layer(random, 2));
}
if (random.nextInt(5) == 0) {
generator.setCellFractureHeight(random.nextDouble()).setCellFractureZoom(0.5D + random.nextDouble());
}
return generator;
}
private static IrisNoiseGenerator layer(SplittableRandom random, int depth) {
IrisNoiseGenerator layer = new IrisNoiseGenerator()
.setStyle(STYLES[random.nextInt(STYLES.length)].style())
.setSeed(random.nextLong(-100_000L, 100_000L))
.setZoom(0.3D + random.nextDouble() * 4D)
.setOpacity(0.05D + random.nextDouble())
.setOffsetX(random.nextInt(-20, 20) * 0.5D)
.setOffsetZ(random.nextInt(-20, 20) * 0.5D)
.setOffsetY(random.nextInt(4) == 0 ? random.nextDouble() * 0.1D : 0D)
.setNegative(random.nextInt(4) == 0)
.setExponent(random.nextInt(4) == 0 ? 0.5D + random.nextDouble() : 1D)
.setParametric(random.nextInt(6) == 0)
.setBezier(random.nextInt(6) == 0)
.setSinCentered(random.nextInt(8) == 0)
.setOctaves(random.nextInt(5) == 0 ? 2 : 1)
.setEnabled(depth == 0 || random.nextInt(8) != 0);
if (depth < 4 && random.nextInt(3) != 0) {
int children = random.nextInt(4) == 0 ? 2 + random.nextInt(2) : 1;
KList<IrisNoiseGenerator> fracture = new KList<>();
for (int child = 0; child < children; child++) {
fracture.add(layer(random, depth + 1));
}
layer.setFracture(fracture);
}
return layer;
}
}
@@ -3,9 +3,6 @@ package art.arcane.iris.generation.noise;
import art.arcane.volmlib.util.collection.KList;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
@@ -34,7 +31,7 @@ public class IrisGeneratorBatchTest {
assertTrue(batch.supportsBatch());
double[] sampled = new double[XS.length];
for (long seed : seeds) {
batch.getHeights(XS, ZS, seed, sampled, sampled.length);
batch.compileProgram(seed).heights(new TerrainBatchScratch(), XS, ZS, sampled.length, sampled);
for (int index = 0; index < sampled.length; index++) {
assertEquals(style + " detail=" + detail + " seed=" + seed + " index=" + index,
Double.doubleToRawLongBits(scalar.getHeight(XS[index], ZS[index], seed)),
@@ -54,7 +51,7 @@ public class IrisGeneratorBatchTest {
IrisGenerator batch = generator(style, detail, false).setCliffHeightMax(0D).setCellFractureHeight(0D);
double[] values = new double[XS.length];
for (long seed : new long[]{-1337L, Long.MAX_VALUE - 37L}) {
batch.getHeights(XS, ZS, seed, values, XS.length);
batch.compileProgram(seed).heights(new TerrainBatchScratch(), XS, ZS, XS.length, values);
for (int index = 0; index < XS.length; index++) {
assertEquals(Double.doubleToRawLongBits(scalar.getHeight(XS[index], ZS[index], seed)),
Double.doubleToRawLongBits(values[index]));
@@ -65,13 +62,15 @@ public class IrisGeneratorBatchTest {
}
@Test
public void resolvesLayerContextOncePerBatch() {
public void resolvesLayerContextOncePerCompile() {
IrisNoiseGenerator layer = spy(new IrisNoiseGenerator().setStyle(NoiseStyle.SIMPLEX.style()));
IrisGenerator generator = new IrisGenerator().setComposite(new KList<>(layer));
double[] sampled = new double[XS.length];
generator.getHeights(XS, ZS, 99L, sampled, XS.length);
GeneratorProgram program = generator.compileProgram(99L);
verify(layer, times(1)).getGenerator(anyLong(), isNull(), isNull());
clearInvocations(layer);
program.heights(new TerrainBatchScratch(), XS, ZS, XS.length, sampled);
verify(layer, times(0)).getGenerator(anyLong(), isNull(), isNull());
for (int index = 0; index < XS.length; index++) {
generator.getHeight(XS[index], ZS[index], 99L);
}
@@ -79,24 +78,15 @@ public class IrisGeneratorBatchTest {
}
@Test
public void customGeneratorFallbackKeepsCoordinateOrder() {
List<Double> sampled = new ArrayList<>();
public void customGeneratorsCannotCompile() {
IrisGenerator generator = new IrisGenerator() {
@Override
public double getHeight(double x, double z, long seed) {
sampled.add(x);
return sampled.size();
return x;
}
};
assertFalse(generator.supportsBatch());
double[] values = new double[XS.length];
generator.getHeights(XS, ZS, 0L, values, values.length);
for (int index = 0; index < values.length; index++) {
assertEquals(Double.valueOf(XS[index]), sampled.get(index));
assertEquals(index + 1D, values[index], 0D);
}
assertThrows(IllegalArgumentException.class,
() -> generator.getHeights(new double[4097], new double[4097], 0L, new double[4097], 4097));
assertThrows(IllegalStateException.class, () -> generator.compileProgram(0L));
}
@Test
@@ -0,0 +1,204 @@
package art.arcane.iris.generation.runtime;
import art.arcane.iris.configuration.IrisSettings;
import art.arcane.iris.generation.bake.BakePlane;
import art.arcane.iris.generation.bake.GeneratorEvaluations;
import art.arcane.iris.generation.block.B;
import art.arcane.iris.generation.image.IrisImageMapApplication;
import art.arcane.iris.generation.image.IrisImageMapRuntime;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.mockito.MockedStatic;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.SplittableRandom;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Answers.CALLS_REAL_METHODS;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
public class IrisComplexBakeParityTest {
@ClassRule public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
@Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
private IrisSettings previousSettings;
@Before
public void prepareRuntime() {
previousSettings = IrisSettings.settings;
IrisSettings.settings = new IrisSettings();
IrisServices.register(PreservationRegistry.class, mock(PreservationRegistry.class));
IrisPlatform platform = mock(IrisPlatform.class);
when(platform.platformName()).thenReturn("bukkit");
IrisPlatforms.bind(platform);
}
@After
public void releaseRuntime() {
IrisPlatforms.unbind();
IrisServices.remove(PreservationRegistry.class);
IrisSettings.settings = previousSettings;
}
@Test
public void batchedChunkBaseHeightsEqualScalarHeightsBitForBit() throws Exception {
try (Statics ignored = new Statics(); Fixture batched = new Fixture(pack()); Fixture scalar = new Fixture(pack())) {
SplittableRandom random = new SplittableRandom(0xBA5EL);
for (int chunk = 0; chunk < 48; chunk++) {
int chunkX = random.nextInt(-400, 400);
int chunkZ = random.nextInt(-400, 400);
double[] baked = (double[]) batched.complex.bakePlane(BakePlane.BASE_HEIGHT, chunkX, chunkZ);
for (int index = 0; index < 256; index++) {
int x = (chunkX << 4) + (index & 15);
int z = (chunkZ << 4) + (index >> 4);
assertEquals("chunk " + chunkX + "," + chunkZ + " column " + index,
Double.doubleToRawLongBits(scalar.complex.getBaseTerrainHeightStream().getDouble(x, z)),
Double.doubleToRawLongBits(baked[index]));
}
}
}
}
@Test
public void batchedChunksEvaluateExactlyTheScalarGeneratorSet() throws Exception {
try (Statics ignored = new Statics(); Fixture batched = new Fixture(pack()); Fixture scalar = new Fixture(pack())) {
SplittableRandom random = new SplittableRandom(0xC0DEL);
long totalBatched = 0L;
for (int chunk = 0; chunk < 32; chunk++) {
int chunkX = random.nextInt(-400, 400);
int chunkZ = random.nextInt(-400, 400);
GeneratorEvaluations.Snapshot before = GeneratorEvaluations.snapshot();
batched.complex.bakePlane(BakePlane.BASE_HEIGHT, chunkX, chunkZ);
GeneratorEvaluations.Snapshot afterBatch = GeneratorEvaluations.snapshot();
for (int index = 0; index < 256; index++) {
scalar.complex.getBaseTerrainHeightStream().getDouble((chunkX << 4) + (index & 15), (chunkZ << 4) + (index >> 4));
}
GeneratorEvaluations.Snapshot afterScalar = GeneratorEvaluations.snapshot();
long batchedEvaluations = afterBatch.minus(before).bakedEvaluations();
assertEquals(0L, afterBatch.minus(before).scalarEvaluations());
assertEquals("chunk " + chunkX + "," + chunkZ, batchedEvaluations,
afterScalar.minus(afterBatch).scalarEvaluations());
totalBatched += batchedEvaluations;
}
assertTrue(totalBatched > 0L);
}
}
private Path pack() throws Exception {
Path pack = temporaryFolder.newFolder().toPath();
Files.createDirectories(pack.resolve("dimensions"));
Files.createDirectories(pack.resolve("regions"));
Files.createDirectories(pack.resolve("biomes"));
Files.createDirectories(pack.resolve("generators"));
Files.writeString(pack.resolve("dimensions/main.json"), """
{"regions":["alpha","beta"],"landChance":0.55,"regionZoom":0.2,"biomeZoom":0.15,
"carvingEnabled":false,"hydrology":{"rivers":{"enabled":false}}}
""");
Files.writeString(pack.resolve("regions/alpha.json"),
"{\"landBiomes\":[\"peaks\",\"meadow\",\"dunes\"],\"seaBiomes\":[\"sea\"],\"rarity\":1}");
Files.writeString(pack.resolve("regions/beta.json"),
"{\"landBiomes\":[\"meadow\",\"spires\"],\"seaBiomes\":[\"sea\"],\"rarity\":2}");
generator(pack, "mountain", "BILINEAR_STARCAST_9", 52, 0.5D, """
[{"style":{"style":"FRACTAL_SMOKE","zoom":1},"seed":3356}]""", "");
generator(pack, "magnet", "BILINEAR_STARCAST_9", 52, 0.5D, """
[{"style":{"style":"FRACTAL_SMOKE","zoom":0.6},"seed":911}]""", "");
generator(pack, "plain", "BILINEAR_STARCAST_9", 12, 0.5D, """
[{"style":{"style":"IRIS_DOUBLE","zoom":0.87,"fracture":{"style":"NOWHERE","zoom":0.195,"multiplier":18}},"seed":10056,"bezier":true}]""", "");
generator(pack, "cliffs", "BILINEAR_STARCAST_9", 12, 0.5D, """
[{"style":{"style":"GLOB","zoom":0.4,"exponent":0.7,"fracture":{"style":"NOWHERE","zoom":0.1,"multiplier":5}},"negative":true,"seed":10056}]""",
",\"cliffHeightMax\":80,\"cliffHeightMin\":35,\"cliffHeightGenerator\":{\"seed\":2348,\"style\":{\"style\":\"CELLULAR_HEIGHT\"},\"zoom\":0.4}");
generator(pack, "dunes", "BILINEAR_STARCAST_9", 23, 1D, """
[{"style":{"style":"SIMPLEX","zoom":2},"seed":77,"fracture":[{"style":{"style":"PERLIN"},"seed":5,"opacity":2.5}]}]""", "");
generator(pack, "spikes", "STARCAST_12", 42, 0D, """
[{"style":{"style":"CELLULAR","zoom":0.5},"seed":4}]""", "");
biome(pack, "peaks", "[{\"generator\":\"mountain\",\"min\":40,\"max\":120},{\"generator\":\"spikes\",\"min\":0,\"max\":20}]",
",\"children\":[\"meadow\"],\"childShrinkFactor\":1.5");
biome(pack, "meadow", "[{\"generator\":\"plain\",\"min\":2,\"max\":14},{\"generator\":\"cliffs\",\"min\":0,\"max\":9}]", "");
biome(pack, "dunes", "[{\"generator\":\"dunes\",\"min\":3,\"max\":30},{\"generator\":\"magnet\",\"min\":-4,\"max\":6}]", "");
biome(pack, "spires", "[{\"generator\":\"spikes\",\"min\":10,\"max\":60},{\"generator\":\"plain\",\"min\":1,\"max\":4}]", "");
biome(pack, "sea", "[{\"generator\":\"mountain\",\"min\":-32,\"max\":-10}]", "");
return pack;
}
private static void generator(Path pack, String key, String function, double scale, double detail,
String composite, String extra) throws Exception {
Files.writeString(pack.resolve("generators/" + key + ".json"), "{\"surfaceDetail\":" + detail
+ ",\"interpolator\":{\"function\":\"" + function + "\",\"horizontalScale\":" + scale + "}"
+ ",\"seed\":" + Math.abs(key.hashCode()) + ",\"composite\":" + composite.strip() + extra + "}");
}
private static void biome(Path pack, String key, String generators, String extra) throws Exception {
Files.writeString(pack.resolve("biomes/" + key + ".json"), "{\"generators\":" + generators + extra + "}");
}
private static final class Statics implements AutoCloseable {
private final MockedStatic<B> blocks = mockStatic(B.class);
private final MockedStatic<IrisImageMapRuntime> imageRuntimes = mockStatic(IrisImageMapRuntime.class);
private Statics() {
NativeBlockState block = mock(NativeBlockState.class);
when(block.isFluid()).thenReturn(true);
blocks.when(() -> B.getState(anyString())).thenReturn(block);
blocks.when(() -> B.getStateOrNull(anyString(), eq(false))).thenReturn(block);
IrisImageMapRuntime images = mock(IrisImageMapRuntime.class);
when(images.has(IrisImageMapApplication.BIOME)).thenReturn(false);
when(images.sampleTerrainHeight(anyDouble(), anyDouble(), anyDouble()))
.thenAnswer(invocation -> invocation.<Double>getArgument(2));
imageRuntimes.when(() -> IrisImageMapRuntime.compile(any(Engine.class))).thenReturn(images);
}
@Override
public void close() {
imageRuntimes.close();
blocks.close();
}
}
private static final class Fixture implements AutoCloseable {
private final Engine engine = mock(Engine.class, CALLS_REAL_METHODS);
private final EnginePlatformHooks hooks = mock(EnginePlatformHooks.class);
private final IrisData data;
private final IrisComplex complex;
private Fixture(Path pack) {
data = IrisData.openRuntime(pack.toFile());
IrisDimension dimension = data.getDimensionLoader().load("main");
doReturn(data).when(engine).getData();
doReturn(dimension).when(engine).getDimension();
doReturn(384).when(engine).getHeight();
doReturn(-64).when(engine).getMinHeight();
doReturn(320).when(engine).getMaxHeight();
when(hooks.isMainThread()).thenReturn(false);
doReturn(hooks).when(engine).getPlatformHooks();
doReturn(new SeedManager(1337L)).when(engine).getSeedManager();
complex = new IrisComplex(engine);
doReturn(complex).when(engine).getComplex();
}
@Override
public void close() {
complex.close();
data.close();
}
}
}
@@ -44,6 +44,7 @@ import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atMost;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
@@ -89,16 +90,10 @@ public class IrisComplexHeightStreamTest {
try (Fixture fixture = new Fixture(pack())) {
IrisBiome mapped = new IrisBiome();
when(fixture.images.sampleBiome(anyDouble(), anyDouble())).thenReturn(mapped);
MeteredCache bridge = (MeteredCache) fixture.complex.getBridgeStream();
MeteredCache regions = (MeteredCache) fixture.complex.getRegionStream();
MeteredCache heights = (MeteredCache) fixture.complex.getNaturalHeightStream();
assertSame(mapped, fixture.complex.getBaseBiomeStream().get(-16, 32));
assertSame(mapped, fixture.complex.getBaseBiomeStream().get(-16.75, 10, 32.5));
assertSame(mapped, fixture.complex.getNaturalTrueBiomeStream().get(-16, 32));
assertSame(mapped, fixture.complex.getNaturalTrueBiomeStream().get(-16.75, 10, 32.5));
assertEquals(0L, bridge.getSize());
assertEquals(0L, regions.getSize());
assertEquals(0L, heights.getSize());
assertNull(fixture.complex.getSharedCornerBounds());
when(fixture.images.sampleBiome(anyDouble(), anyDouble())).thenReturn(null);
@@ -116,7 +111,7 @@ public class IrisComplexHeightStreamTest {
int[] rounded = new int[256];
clearInvocations(fixture.hooks);
cache.fillRounded(rounded);
verify(fixture.hooks, times(1)).isMainThread();
verify(fixture.hooks, atMost(2)).isMainThread();
assertEquals(256, samples.get());
for (int index = 0; index < 256; index++) {
int x = index & 15;
@@ -127,7 +122,7 @@ public class IrisComplexHeightStreamTest {
}
clearInvocations(fixture.hooks);
cache.fill();
verify(fixture.hooks, times(1)).isMainThread();
verify(fixture.hooks, atMost(2)).isMainThread();
assertEquals(256, samples.get());
}
}
@@ -2,6 +2,8 @@ package art.arcane.iris.generation.runtime;
import art.arcane.iris.configuration.IrisSettings;
import art.arcane.iris.world.pregen.PregenPerformanceProfile;
import art.arcane.iris.generation.bake.ChunkBakeCache;
import art.arcane.iris.generation.bake.PlaneSource;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.hydrology.HydrologyPlanner;
import art.arcane.iris.generation.hydrology.HydrologyPlannerSettings;
@@ -48,17 +50,14 @@ public class IrisComplexHydrologyNoiseCacheTest {
try (Fixture fixture = new Fixture(1_024)) {
ProceduralStream<Double> wrapper = fixture.complex.getRawHeightStream();
assertEquals(13D, fixture.height.getDouble(18, 5), 0D);
assertEquals(13D, fixture.rawHeight.getDouble(18, 5), 0D);
PregenPerformanceProfile.apply(fixture.engine);
assertEquals(4_096L * 256L, fixture.height.getMaxSize());
assertEquals(4_096L * 256L, fixture.rawHeight.getMaxSize());
assertTrue(fixture.bakes.capacity() >= 4_096);
assertEquals(1_024L * 256L, fixture.biomes.getMaxSize());
assertSame(wrapper, fixture.complex.getRawHeightStream());
assertEquals(47D, wrapper.getDouble(18, 5), 0D);
assertEquals(13D, fixture.height.getDouble(18, 5), 0D);
assertEquals(13D, fixture.rawHeight.getDouble(18, 5), 0D);
} finally {
PregenPerformanceProfile.restore();
IrisSettings.settings = previousSettings;
@@ -75,7 +74,7 @@ public class IrisComplexHydrologyNoiseCacheTest {
fixture.complex.ensureTerrainNoiseCacheSize(4_096);
fixture.assertCapacities(32_768);
assertEquals(4_096L * 256L, fixture.rawHeight.getMaxSize());
assertTrue(fixture.bakes.capacity() >= 4_096);
assertEquals(reservedBytes, budget.reservedBytes());
fixture.complex.close();
assertEquals(0L, budget.reservedBytes());
@@ -88,7 +87,7 @@ public class IrisComplexHydrologyNoiseCacheTest {
fixture.complex.ensureTerrainNoiseCacheSize(4_096);
fixture.assertCapacities(65_536);
assertEquals(65_536L * 256L, fixture.rawHeight.getMaxSize());
assertTrue(fixture.bakes.capacity() >= 65_536);
}
}
@@ -229,11 +228,54 @@ public class IrisComplexHydrologyNoiseCacheTest {
HydrologyNoiseCacheBudget budget = new HydrologyNoiseCacheBudget(Long.MAX_VALUE);
setField(fixture.complex, "detached", true);
fixture.complex.expandHydrologyNoiseCaches(budget);
fixture.complex.reserveHydrologyBake(budget);
fixture.assertCapacities(1_024);
assertEquals(1_024, fixture.bakes.capacity());
setField(fixture.complex, "detached", false);
fixture.complex.setHydrologyRuntime(null);
fixture.complex.expandHydrologyNoiseCaches(budget);
fixture.complex.reserveHydrologyBake(budget);
fixture.assertCapacities(1_024);
assertEquals(1_024, fixture.bakes.capacity());
assertEquals(0L, budget.reservedBytes());
}
}
@Test
public void plannerReservationGrowsTheBakeToTheHydrologyTarget() throws Exception {
try (Fixture fixture = new Fixture(1_024)) {
HydrologyNoiseCacheBudget budget = new HydrologyNoiseCacheBudget(Long.MAX_VALUE);
fixture.complex.reserveHydrologyBake(budget);
fixture.complex.reserveHydrologyBake(budget);
assertEquals(32_768, fixture.bakes.capacity());
assertEquals(2L * (32_768L - 1_024L) * 4_096L, budget.reservedBytes());
fixture.complex.ensureTerrainNoiseCacheSize(4_096);
assertEquals(32_768, fixture.bakes.capacity());
fixture.complex.close();
assertEquals(0L, budget.reservedBytes());
fixture.complex.reserveHydrologyBake(budget);
assertEquals(0L, budget.reservedBytes());
}
}
@Test
public void plannerReservationTakesOnlyTheGrantedShareOfTheBudget() throws Exception {
try (Fixture fixture = new Fixture(1_024)) {
HydrologyNoiseCacheBudget budget = new HydrologyNoiseCacheBudget(2L * 80L * 4_096L + 4_095L);
fixture.complex.reserveHydrologyBake(budget);
assertEquals(1_104, fixture.bakes.capacity());
assertEquals(2L * 80L * 4_096L, budget.reservedBytes());
fixture.complex.close();
assertEquals(0L, budget.reservedBytes());
}
}
@Test
public void plannerReservationLeavesLargerBakesUnchanged() throws Exception {
try (Fixture fixture = new Fixture(65_536)) {
HydrologyNoiseCacheBudget budget = new HydrologyNoiseCacheBudget(Long.MAX_VALUE);
fixture.complex.reserveHydrologyBake(budget);
assertEquals(65_536, fixture.bakes.capacity());
assertEquals(0L, budget.reservedBytes());
}
}
@@ -297,7 +339,7 @@ public class IrisComplexHydrologyNoiseCacheTest {
private final CachedStream2D<IrisRegion> regions;
private final CachedStream2D<InferredType> bridge;
private final CachedDoubleStream2D height;
private final CachedDoubleStream2D rawHeight;
private final ChunkBakeCache bakes;
private Fixture(int capacity) throws Exception {
services.when(() -> IrisServices.get(PreservationRegistry.class)).thenReturn(mock(PreservationRegistry.class));
@@ -305,10 +347,10 @@ public class IrisComplexHydrologyNoiseCacheTest {
regions = objectCache(capacity);
bridge = objectCache(capacity);
height = heightCache(capacity);
rawHeight = heightCache(capacity);
bakes = new ChunkBakeCache(mock(PlaneSource.class), capacity);
complex.setNaturalHeightStream(height);
complex.setHeightStream(ProceduralStream.ofDouble((x, z) -> 47D));
setField(complex, "cachedHeightStream", rawHeight);
setField(complex, "bakeCache", bakes);
when(engine.getComplex()).thenReturn(complex);
complex.setBaseBiomeStream(biomes);
complex.setRegionStream(regions);
@@ -83,7 +83,7 @@ public class IrisComplexSelfSelectionTest {
Field field = IrisComplex.class.getDeclaredField("childSelectionPlans");
field.setAccessible(true);
field.set(complex, plans);
implode = IrisComplex.class.getDeclaredMethod("implode", IrisBiome.class, Double.class, Double.class, int.class);
implode = IrisComplex.class.getDeclaredMethod("implode", IrisBiome.class, double.class, double.class, int.class);
implode.setAccessible(true);
}
@@ -106,7 +106,7 @@ public class IrisComplexSelfSelectionTest {
}
@Override
public CNG getChildrenGenerator(RNG rng, int signature, double scale) {
public CNG getChildrenGenerator(long seed, double scale) {
return noise;
}
}
@@ -97,11 +97,11 @@ public class IrisCarveModifierInferenceIsolationTest {
doReturn(0D).when(region).getShoreHeight(anyDouble(), anyDouble());
Method fixBiomeType = IrisComplex.class.getDeclaredMethod(
"fixBiomeType",
Double.class,
double.class,
IrisBiome.class,
IrisRegion.class,
Double.class,
Double.class,
double.class,
double.class,
double.class
);
fixBiomeType.setAccessible(true);
@@ -0,0 +1,52 @@
package art.arcane.iris.generation.stream;
import art.arcane.volmlib.util.math.Rarity;
import art.arcane.volmlib.util.stream.ProceduralStream;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.SplittableRandom;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
public class RarityStreamTest {
@Test
public void picksMatchRarityStreamSelectionForEveryNoiseValue() {
SplittableRandom random = new SplittableRandom(4242L);
for (int trial = 0; trial < 200; trial++) {
List<Option> options = new ArrayList<>();
int count = 1 + random.nextInt(12);
for (int index = 0; index < count; index++) {
options.add(new Option(random.nextInt(-2, 40)));
}
double[] value = new double[1];
ProceduralStream<Double> noise = ProceduralStream.ofDouble((x, z) -> value[0]);
ProceduralStream<Option> expected = noise.selectRarity(options);
RarityStream<Option> candidate = new RarityStream<>(noise, options);
for (int sample = 0; sample < 2_000; sample++) {
value[0] = switch (random.nextInt(5)) {
case 0 -> random.nextDouble();
case 1 -> random.nextDouble(-0.5D, 1.5D);
case 2 -> random.nextInt(0, 64) / 63D;
case 3 -> sample % 2 == 0 ? Double.NaN : -0D;
default -> Math.nextUp(random.nextInt(0, 16) / 15D);
};
assertSame(expected.get(sample, -sample), candidate.get(sample, -sample));
}
}
}
@Test
public void emptyOptionsSelectNothing() {
assertNull(new RarityStream<Option>(ProceduralStream.ofDouble((x, z) -> 0.5D), List.of()).get(0D, 0D));
}
private record Option(int rarity) implements Rarity {
@Override
public int getRarity() {
return rarity;
}
}
}
@@ -791,7 +791,7 @@ public class IrisObjectPlacementRunnerRegressionTest {
object.place(0, ANCHOR_Y, 0, placer, placement, new RNG(2L), data);
assertEquals(1, placer.blockReads);
assertEquals(enabled || authored ? 1 : 0, placer.blockReads);
assertEquals(1, placer.writes().size());
assertSame(submerged && (enabled || authored) ? wet : dry,
placer.writes().get(0).state());
@@ -0,0 +1,76 @@
package art.arcane.iris.structure.object;
import art.arcane.iris.generation.geometry.IrisBlockVector;
import art.arcane.iris.testsupport.KeyedBlockState;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
public class ObjectPlacementBakeTest {
@Test
public void spinsResolvingToTheSameKernelShareOneBake() {
IrisObject object = object();
IrisObjectRotation rotation = new IrisObjectRotation();
ObjectPlacementBake first = ObjectPlacementBake.of(object, new SpinKernel(rotation, 0, 10, 0), false, null);
ObjectPlacementBake second = ObjectPlacementBake.of(object, new SpinKernel(rotation, 3, 80, 9), false, null);
ObjectPlacementBake turned = ObjectPlacementBake.of(object, new SpinKernel(rotation, 0, 100, 0), false, null);
assertSame(first, second);
assertNotSame(first, turned);
assertEquals(3, first.count());
}
@Test
public void bakedOffsetsMatchTheKernelTransformInCursorOrder() {
IrisObject object = object();
IrisObjectRotation rotation = IrisObjectRotation.of(0D, 90D, 0D);
SpinKernel spin = new SpinKernel(rotation, 0, 0, 0);
IrisBlockVector translate = new IrisBlockVector(1.5D, -2D, 0.25D);
ObjectPlacementBake bake = ObjectPlacementBake.of(object, spin, true, translate);
double[] exact = bake.exact(object, spin);
int index = 0;
for (IrisBlockVector source : object.getBlocks().keys()) {
IrisBlockVector expected = source.clone();
spin.rotate(expected);
expected.setY(-expected.getBlockY());
expected.add(translate);
assertEquals((int) Math.round(expected.getX()), bake.offsetX(index));
assertEquals((int) Math.round(expected.getY()), bake.offsetY(index));
assertEquals((int) Math.round(expected.getZ()), bake.offsetZ(index));
assertEquals(expected.getBlockY(), bake.layerY(index));
assertEquals(expected.getX(), exact[index * 3], 0D);
assertEquals(expected.getY(), exact[index * 3 + 1], 0D);
assertEquals(expected.getZ(), exact[index * 3 + 2], 0D);
index++;
}
assertEquals(bake.count(), index);
}
@Test
public void editedObjectsRebake() {
IrisObject object = object();
SpinKernel spin = new SpinKernel(new IrisObjectRotation(), 0, 0, 0);
ObjectPlacementBake before = ObjectPlacementBake.of(object, spin, false, null);
object.getBlocks().put(new IrisBlockVector(1, 1, 1), new KeyedBlockState("minecraft:dirt"));
ObjectPlacementBake after = ObjectPlacementBake.of(object, spin, false, null);
assertNotSame(before, after);
assertEquals(4, after.count());
assertSame(after, ObjectPlacementBake.of(object, spin, false, null));
}
private static IrisObject object() {
IrisObject object = new IrisObject(5, 5, 5);
KeyedBlockState stone = new KeyedBlockState("minecraft:stone");
object.getBlocks().put(new IrisBlockVector(-2, 0, 1), stone);
object.getBlocks().put(new IrisBlockVector(0, -1, 2), stone);
object.getBlocks().put(new IrisBlockVector(2, 2, -2), new KeyedBlockState("minecraft:oak_log[axis=y]"));
return object;
}
}
@@ -0,0 +1,761 @@
package art.arcane.iris.structure.object;
import art.arcane.iris.generation.block.BlockDataMergeSupport;
import art.arcane.iris.generation.block.IrisBlockData;
import art.arcane.iris.generation.block.TileData;
import art.arcane.iris.generation.cave.CarvingMode;
import art.arcane.iris.generation.decoration.IrisStiltSettings;
import art.arcane.iris.generation.decoration.IrisVacuumSettings;
import art.arcane.iris.generation.decoration.tree.TreeBlockCanvas;
import art.arcane.iris.generation.decoration.tree.TreeGrounding;
import art.arcane.iris.generation.geometry.IrisBlockVector;
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
import art.arcane.iris.generation.noise.NoiseStyle;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisMaterialPalette;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.pack.loading.ResourceLoader;
import art.arcane.iris.pack.validation.ContentGate;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.iris.world.entity.IrisMarker;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.math.BlockPosition;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.matter.MatterMarker;
import art.arcane.volmlib.util.stream.ProceduralStream;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.withSettings;
public class ObjectPlacementGoldenTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
private static final int OBJECTS = 200;
private static final int SEEDS = 5;
private static final long GOLDEN = 0x408f9d4113015897L;
private static final long MARKER_GOLDEN = 0x60b4bbf1144b239cL;
private static final List<String> PALETTE = List.of(
"minecraft:stone",
"minecraft:dirt",
"minecraft:grass_block[snowy=false]",
"minecraft:oak_log[axis=y]",
"minecraft:oak_log[axis=x]",
"minecraft:oak_leaves[distance=3,persistent=false,waterlogged=false]",
"minecraft:oak_leaves[distance=1,persistent=true,waterlogged=false]",
"minecraft:oak_leaves[distance=7,persistent=false,waterlogged=true]",
"minecraft:oak_stairs[facing=north,half=bottom,waterlogged=false]",
"minecraft:oak_stairs[facing=east,half=top,waterlogged=true]",
"minecraft:vine[east=false,north=true,south=false,up=false,west=false]",
"minecraft:air",
"minecraft:cave_air",
"minecraft:chest[facing=south,waterlogged=false]",
"minecraft:lantern[hanging=false,waterlogged=false]",
"minecraft:cobblestone",
"minecraft:glass",
"minecraft:oak_slab[type=bottom,waterlogged=false]");
private final Map<String, GoldenState> states = new ConcurrentHashMap<>();
private IrisData data;
private IrisData objectData;
private Engine engine;
private IrisObjectRotation.StateRotator previousRotator;
private BlockDataMergeSupport.StateMerger previousMerger;
@Before
public void bindPlatform() {
IrisPlatforms.unbind();
PlatformRegistries registries = mock(PlatformRegistries.class, withSettings().stubOnly());
when(registries.block(anyString())).thenAnswer(invocation -> state(invocation.getArgument(0)));
when(registries.blockOrNull(anyString())).thenAnswer(invocation -> state(invocation.getArgument(0)));
when(registries.blockOrNull(anyString(), anyBoolean())).thenAnswer(invocation -> state(invocation.getArgument(0)));
when(registries.air()).thenAnswer(invocation -> state("minecraft:air"));
IrisPlatform platform = mock(IrisPlatform.class, withSettings().stubOnly());
when(platform.registries()).thenReturn(registries);
IrisPlatforms.bind(platform);
previousRotator = IrisObjectRotation.bindPlatformRotator(ObjectPlacementGoldenTest::rotateState);
previousMerger = BlockDataMergeSupport.bindPlatformMerger(this::mergeStates);
ProceduralStream<NativeBlockState> rock = mock(ProceduralStream.class, withSettings().stubOnly());
when(rock.get(anyDouble(), anyDouble())).thenAnswer(invocation -> state("minecraft:stone"));
IrisComplex complex = mock(IrisComplex.class, withSettings().stubOnly());
when(complex.getRockStream()).thenReturn(rock);
IrisDimension dimension = mock(IrisDimension.class, withSettings().stubOnly());
engine = mock(Engine.class, withSettings().stubOnly());
when(engine.getHeight()).thenReturn(256);
when(engine.getMinHeight()).thenReturn(0);
when(engine.getComplex()).thenReturn(complex);
when(engine.getDimension()).thenReturn(dimension);
ContentGate gate = mock(ContentGate.class, withSettings().stubOnly());
when(gate.ready()).thenReturn(false);
data = mock(IrisData.class, withSettings().stubOnly());
when(data.getEngine()).thenReturn(engine);
when(data.getContentGate()).thenReturn(gate);
objectData = mock(IrisData.class, withSettings().stubOnly());
when(objectData.getContentGate()).thenReturn(gate);
}
@After
public void unbindPlatform() {
IrisObjectRotation.restorePlatformRotator(previousRotator);
BlockDataMergeSupport.restorePlatformMerger(previousMerger);
IrisPlatforms.unbind();
}
@Test
public void placementsMatchTheRecordedBlockStream() throws IOException {
Random random = new Random(20261021L);
List<IrisObjectRotation> rotations = rotations();
long combined = 17L;
StringBuilder dump = new StringBuilder();
for (int index = 0; index < OBJECTS; index++) {
IrisObject object = randomObject(random, index);
object.setLoader(objectData);
IrisObjectPlacement base = randomPlacement(random);
for (int rotationIndex = 0; rotationIndex < rotations.size(); rotationIndex++) {
IrisObjectPlacement placement = base.toPlacement("golden");
placement.setRotation(rotations.get(rotationIndex));
boolean explicitY = (index + rotationIndex) % 3 == 0;
for (int seed = 0; seed < SEEDS; seed++) {
RNG rng = new RNG(index * 1_000_003L + rotationIndex * 7919L + seed);
GoldenPlacer placer = new GoldenPlacer(engine, (index & 1) == 0);
List<String> listened = new ArrayList<>();
int x = 400 + index * 3 + seed;
int z = -900 + rotationIndex * 5 - seed;
int y = explicitY ? 90 + seed : -1;
int result;
if (placement.getMode() == ObjectPlaceMode.ORGANIC_STILT && explicitY && seed % 2 == 0) {
result = object.placeOnFloor(x, 70, z, placer, placement, rng,
(position, state) -> listened.add(position.getX() + "," + position.getY() + "," + position.getZ() + "=" + state.key()),
data);
} else {
result = object.place(x, y, z, placer, placement, rng,
(position, state) -> listened.add(position.getX() + "," + position.getY() + "," + position.getZ() + "=" + state.key()),
null, data);
}
long hash = placer.hash;
hash = mix(hash, result);
hash = mix(hash, rng.nextLong());
for (String entry : listened) {
hash = mix(hash, entry.hashCode());
}
hash = mix(hash, listened.size());
combined = mix(combined, hash);
dump.append(index).append(' ').append(rotationIndex).append(' ').append(seed).append(' ')
.append(Long.toHexString(hash)).append(' ').append(placer.writes).append('\n');
}
}
}
String dumpPath = System.getenv("IRIS_OBJECT_GOLDEN_DUMP");
if (dumpPath != null && !dumpPath.isBlank()) {
Files.writeString(Path.of(dumpPath), dump.toString());
}
assertEquals(Long.toHexString(GOLDEN), Long.toHexString(combined));
}
@Test
public void markedPlacementsMatchTheRecordedBlockStream() {
ResourceLoader<IrisMarker> markers = mock(ResourceLoader.class, withSettings().stubOnly());
IrisMarker anywhere = new IrisMarker();
anywhere.setEmptyAbove(false);
when(markers.load("anywhere")).thenReturn(anywhere);
when(markers.load("open")).thenReturn(new IrisMarker());
when(objectData.getMarkerLoader()).thenReturn(markers);
Random random = new Random(77L);
List<IrisObjectRotation> rotations = rotations();
long combined = 23L;
int markerWrites = 0;
for (int index = 0; index < 40; index++) {
IrisObject object = randomObject(random, index);
object.setLoader(objectData);
IrisObjectPlacement base = new IrisObjectPlacement();
base.setMode(ObjectPlaceMode.STRUCTURE_PIECE);
base.setRequireSurfaceSupport(false);
IrisObjectMarker loose = new IrisObjectMarker();
loose.setMarker("anywhere");
loose.setMaximumMarkers(1 + random.nextInt(6));
loose.setMark(new KList<>(new IrisBlockData("minecraft:stone"), new IrisBlockData("minecraft:oak_leaves")));
IrisObjectMarker exact = new IrisObjectMarker();
exact.setMarker("open");
exact.setExact(true);
exact.setMark(new KList<>(new IrisBlockData("minecraft:oak_log[axis=y]")));
base.setMarkers(new KList<>(loose, exact));
for (int rotationIndex = 0; rotationIndex < rotations.size(); rotationIndex++) {
IrisObjectPlacement placement = base.toPlacement("marked");
placement.setRotation(rotations.get(rotationIndex));
for (int seed = 0; seed < 3; seed++) {
RNG rng = new RNG(index * 31L + rotationIndex * 7L + seed);
GoldenPlacer placer = new GoldenPlacer(engine, false);
int result = object.place(100 + index, 80 + seed, 200 - index, placer, placement, rng, null, null, data);
long hash = mix(placer.hash, result);
hash = mix(hash, rng.nextLong());
combined = mix(combined, hash);
markerWrites += placer.markerWrites;
}
}
}
assertTrue(markerWrites > 100);
assertEquals(Long.toHexString(MARKER_GOLDEN), Long.toHexString(combined));
}
private static long mix(long hash, long value) {
long mixed = (hash ^ value) * 0x9E3779B97F4A7C15L;
return mixed ^ (mixed >>> 29);
}
private List<IrisObjectRotation> rotations() {
IrisObjectRotation defaultY = new IrisObjectRotation();
IrisObjectRotation lockedQuarter = IrisObjectRotation.of(0D, 90D, 0D);
IrisObjectRotation flipped = rotation(new IrisAxisRotationClamp(true, true, 180D, 180D, 0D),
new IrisAxisRotationClamp(true, false, 0D, 0D, 90D), new IrisAxisRotationClamp());
IrisObjectRotation angled = rotation(new IrisAxisRotationClamp(true, false, 0D, 0D, 90D),
new IrisAxisRotationClamp(true, false, 0D, 0D, 45D), new IrisAxisRotationClamp(true, false, 0D, 0D, 90D));
return List.of(defaultY, lockedQuarter, flipped, angled);
}
private IrisObjectPlacement randomPlacement(Random random) {
IrisObjectPlacement placement = new IrisObjectPlacement();
List<ObjectPlaceMode> modes = List.of(ObjectPlaceMode.CENTER_HEIGHT, ObjectPlaceMode.CENTER_HEIGHT,
ObjectPlaceMode.CENTER_HEIGHT, ObjectPlaceMode.STILT, ObjectPlaceMode.FAST_STILT,
ObjectPlaceMode.CENTER_STILT, ObjectPlaceMode.ERODE_STILT, ObjectPlaceMode.ORGANIC_STILT,
ObjectPlaceMode.CEILING_HANG, ObjectPlaceMode.MAX_HEIGHT, ObjectPlaceMode.FAST_MAX_HEIGHT,
ObjectPlaceMode.MIN_HEIGHT, ObjectPlaceMode.PAINT, ObjectPlaceMode.VACUUM, ObjectPlaceMode.VACUUM_ORGANIC,
ObjectPlaceMode.STRUCTURE_PIECE, ObjectPlaceMode.FLOATING, ObjectPlaceMode.FAST_MIN_STILT);
placement.setMode(modes.get(random.nextInt(modes.size())));
if (random.nextInt(4) == 0) {
placement.setTranslate(new IrisObjectTranslate()
.setX(random.nextInt(7) - 3)
.setY(random.nextInt(5) - 2)
.setZ(random.nextInt(7) - 3)
.setYRandom(random.nextInt(3) - 1));
}
if (random.nextInt(6) == 0) {
placement.setWarp(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).setMultiplier(1D + random.nextInt(4)));
}
if (random.nextInt(3) == 0) {
placement.setEdit(randomEdits(random));
}
if (random.nextInt(4) == 0) {
placement.setStiltSettings(new IrisStiltSettings());
}
if (random.nextInt(5) == 0) {
placement.setVacuumSettings(new IrisVacuumSettings().setRadius(1 + random.nextInt(4)));
}
placement.setWaterloggable(random.nextInt(4) == 0);
placement.setUnderwater(random.nextInt(8) == 0);
placement.setOnwater(random.nextInt(10) == 0);
placement.setMeld(random.nextInt(8) == 0);
placement.setBore(random.nextInt(10) == 0);
placement.setForcePlace(random.nextInt(3) == 0);
placement.setBottom(random.nextInt(6) == 0);
placement.setSnow(random.nextInt(5) == 0 ? 0.7D : 0D);
placement.setRequireSurfaceSupport(random.nextInt(3) != 0);
placement.setSurfaceSupportBuffer(random.nextInt(3));
placement.setSurfaceSupportDepth(1 + random.nextInt(2));
placement.setCarvingSupport(switch (random.nextInt(5)) {
case 0 -> CarvingMode.ANYWHERE;
case 1 -> CarvingMode.CARVING_ONLY;
default -> CarvingMode.SURFACE_ONLY;
});
return placement;
}
private KList<IrisObjectReplace> randomEdits(Random random) {
KList<IrisObjectReplace> edits = new KList<>();
int count = 1 + random.nextInt(3);
for (int index = 0; index < count; index++) {
IrisObjectReplace replace = new IrisObjectReplace();
switch (random.nextInt(4)) {
case 0 -> {
replace.setFind(new KList<>(new IrisBlockData("minecraft:oak_leaves")));
replace.setReplace(palette("minecraft:cobblestone"));
}
case 1 -> {
replace.setFind(new KList<>(new IrisBlockData("minecraft:oak_stairs")));
replace.setReplace(palette("minecraft:oak_stairs[half=top]"));
}
case 2 -> {
replace.setExact(true);
replace.setFind(new KList<>(new IrisBlockData("minecraft:stone")));
replace.setReplace(palette("minecraft:glass"));
}
default -> {
replace.setFind(new KList<>(new IrisBlockData("minecraft:grass_block"), new IrisBlockData("minecraft:dirt")));
replace.setReplace(palette("minecraft:oak_leaves[distance=2,persistent=false,waterlogged=false]"));
}
}
replace.setChance(switch (random.nextInt(3)) {
case 0 -> 1F;
case 1 -> 0.5F;
default -> 0.15F;
});
edits.add(replace);
}
return edits;
}
private static IrisMaterialPalette palette(String key) {
IrisMaterialPalette palette = new IrisMaterialPalette();
palette.setPalette(new KList<>(new IrisBlockData(key)));
return palette;
}
private IrisObject randomObject(Random random, int index) {
int width = 1 + random.nextInt(9);
int height = 1 + random.nextInt(9);
int depth = 1 + random.nextInt(9);
IrisObject object = new IrisObject(width, height, depth);
int count = 1 + random.nextInt(Math.max(1, (width * height * depth) / 2));
List<IrisBlockVector> placed = new ArrayList<>();
for (int block = 0; block < count; block++) {
int x = random.nextInt(width) - (width / 2);
int y = random.nextInt(height) - (height / 2);
int z = random.nextInt(depth) - (depth / 2);
IrisBlockVector position = new IrisBlockVector(x, y, z);
String key = PALETTE.get(random.nextInt(PALETTE.size()));
object.getBlocks().put(position, state(key));
placed.add(position);
if (key.startsWith("minecraft:chest") && random.nextBoolean()) {
object.getStates().put(position, new TileData("minecraft:chest", new KMap<>()));
}
}
if (index % 4 == 1 && !placed.isEmpty()) {
List<TreeGrounding.Root> roots = new ArrayList<>();
Set<TreeBlockCanvas.Vec> canopy = new HashSet<>();
for (IrisBlockVector position : placed) {
if (random.nextInt(3) == 0) {
canopy.add(new TreeBlockCanvas.Vec(position.getBlockX(), position.getBlockY(), position.getBlockZ()));
}
}
IrisBlockVector root = placed.get(0);
roots.add(new TreeGrounding.Root(root.getBlockX(), -(height / 2), root.getBlockZ(), state("minecraft:oak_log[axis=y]")));
object.setTreeGrounding(TreeGrounding.create(roots, 4, canopy));
}
return object;
}
private static IrisObjectRotation rotation(IrisAxisRotationClamp x, IrisAxisRotationClamp y, IrisAxisRotationClamp z) {
IrisObjectRotation rotation = new IrisObjectRotation();
rotation.setEnabled(true);
rotation.setXAxis(x);
rotation.setYAxis(y);
rotation.setZAxis(z);
return rotation;
}
private GoldenState state(String raw) {
String key = raw.toLowerCase();
if (key.indexOf(':') < 0) {
key = "minecraft:" + key;
}
return states.computeIfAbsent(key, GoldenState::new);
}
private NativeBlockState mergeStates(NativeBlockState base, NativeBlockState update) {
GoldenState left = (GoldenState) base;
GoldenState right = (GoldenState) update;
if (!left.material.equals(right.material)) {
return update;
}
LinkedHashMap<String, String> merged = new LinkedHashMap<>(left.properties);
merged.putAll(right.properties);
return state(GoldenState.compose(left.material, merged));
}
private static NativeBlockState rotateState(IrisObjectRotation rotation, NativeBlockState state, int spinX, int spinY, int spinZ) {
if (state == null || !rotation.canRotate()) {
return state;
}
GoldenState golden = (GoldenState) state;
String facing = golden.properties.get("facing");
if (facing != null) {
IrisBlockVector vector = switch (facing) {
case "north" -> new IrisBlockVector(0, 0, -1);
case "south" -> new IrisBlockVector(0, 0, 1);
case "east" -> new IrisBlockVector(1, 0, 0);
case "west" -> new IrisBlockVector(-1, 0, 0);
case "up" -> new IrisBlockVector(0, 1, 0);
default -> new IrisBlockVector(0, -1, 0);
};
IrisBlockVector rotated = rotation.rotate(vector, spinX, spinY, spinZ);
String face = faceName(rotated);
if (face.equals("up") || face.equals("down")) {
return golden.material.endsWith("_stairs") ? null : state;
}
return face.equals(facing) ? state : golden.with("facing", face);
}
String axis = golden.properties.get("axis");
if (axis != null) {
IrisBlockVector vector = switch (axis) {
case "x" -> new IrisBlockVector(1, 0, 0);
case "y" -> new IrisBlockVector(0, 1, 0);
default -> new IrisBlockVector(0, 0, 1);
};
IrisBlockVector rotated = rotation.rotate(vector, spinX, spinY, spinZ);
double ax = Math.abs(rotated.getX());
double ay = Math.abs(rotated.getY());
double az = Math.abs(rotated.getZ());
String result = ax >= ay && ax >= az ? "x" : ay >= az ? "y" : "z";
return result.equals(axis) ? state : golden.with("axis", result);
}
return state;
}
private static String faceName(IrisBlockVector vector) {
double ax = Math.abs(vector.getX());
double ay = Math.abs(vector.getY());
double az = Math.abs(vector.getZ());
if (ay >= ax && ay >= az) {
return vector.getY() > 0 ? "up" : "down";
}
if (ax >= az) {
return vector.getX() > 0 ? "east" : "west";
}
return vector.getZ() > 0 ? "south" : "north";
}
private final class GoldenState implements NativeBlockState {
private final String key;
private final String material;
private final LinkedHashMap<String, String> properties;
private GoldenState(String key) {
this.key = key;
int bracket = key.indexOf('[');
this.material = bracket < 0 ? key : key.substring(0, bracket);
this.properties = new LinkedHashMap<>();
if (bracket >= 0) {
for (String part : key.substring(bracket + 1, key.length() - 1).split(",")) {
int split = part.indexOf('=');
properties.put(part.substring(0, split), part.substring(split + 1));
}
}
}
private static String compose(String material, Map<String, String> properties) {
if (properties.isEmpty()) {
return material;
}
StringBuilder builder = new StringBuilder(material).append('[');
boolean first = true;
for (Map.Entry<String, String> entry : properties.entrySet()) {
if (!first) {
builder.append(',');
}
builder.append(entry.getKey()).append('=').append(entry.getValue());
first = false;
}
return builder.append(']').toString();
}
private GoldenState with(String name, String value) {
LinkedHashMap<String, String> updated = new LinkedHashMap<>(properties);
updated.put(name, value);
return state(compose(material, updated));
}
@Override
public String key() {
return key;
}
@Override
public String namespace() {
return "minecraft";
}
@Override
public boolean isAir() {
return material.endsWith("air");
}
@Override
public boolean isSolid() {
return !isAir() && !isFluid() && !isVineBlock() && !material.equals("minecraft:lantern");
}
@Override
public boolean isOccluding() {
return material.equals("minecraft:stone") || material.equals("minecraft:dirt")
|| material.equals("minecraft:grass_block") || material.equals("minecraft:oak_log")
|| material.equals("minecraft:cobblestone") || material.equals("minecraft:oak_slab");
}
@Override
public boolean isCustom() {
return false;
}
@Override
public boolean isFluid() {
return material.equals("minecraft:water");
}
@Override
public boolean isWater() {
return isFluid();
}
@Override
public boolean isWaterLogged() {
return "true".equals(properties.get("waterlogged"));
}
@Override
public boolean isLit() {
return false;
}
@Override
public boolean isUpdatable() {
return material.endsWith("_stairs");
}
@Override
public boolean isFoliage() {
return false;
}
@Override
public boolean isTreeBlock() {
return material.endsWith("_log") || material.endsWith("_leaves");
}
@Override
public boolean isFoliagePlantable() {
return material.equals("minecraft:grass_block") || material.equals("minecraft:dirt");
}
@Override
public boolean isDecorant() {
return false;
}
@Override
public boolean isStorage() {
return material.equals("minecraft:chest");
}
@Override
public boolean isStorageChest() {
return isStorage();
}
@Override
public boolean isOre() {
return false;
}
@Override
public boolean isDeepSlate() {
return false;
}
@Override
public boolean isVineBlock() {
return material.equals("minecraft:vine");
}
@Override
public boolean canPlaceOnto(NativeBlockState onto) {
return onto != null && onto.isSolid();
}
@Override
public boolean matches(NativeBlockState state) {
if (!(state instanceof GoldenState other) || !other.material.equals(material)) {
return false;
}
for (Map.Entry<String, String> entry : other.properties.entrySet()) {
if (!entry.getValue().equals(properties.get(entry.getKey()))) {
return false;
}
}
return true;
}
@Override
public boolean hasTileEntity() {
return isStorage();
}
@Override
public NativeBlockState withProperty(String name, String value) {
return with(name, value);
}
@Override
public Object nativeHandle() {
return key;
}
@Override
public String toString() {
return key;
}
}
private final class GoldenPlacer implements IObjectPlacer {
private final Engine engine;
private final boolean preventDecay;
private final Map<Long, NativeBlockState> blocks = new HashMap<>();
private final Map<Long, Map<Class<?>, Object>> extra = new HashMap<>();
private long hash = 31L;
private int writes;
private int markerWrites;
private GoldenPlacer(Engine engine, boolean preventDecay) {
this.engine = engine;
this.preventDecay = preventDecay;
}
private static long key(int x, int y, int z) {
return (((long) x & 0x3FFFFFL) << 42) | (((long) y & 0xFFFFFL) << 22) | ((long) z & 0x3FFFFFL);
}
private int surface(int x, int z) {
return 64 + Math.floorMod(x * 31 + z * 17, 9) - 4;
}
private void record(String kind, int x, int y, int z, String value) {
writes++;
hash = mix(hash, kind.hashCode());
hash = mix(hash, x);
hash = mix(hash, y);
hash = mix(hash, z);
hash = mix(hash, value.hashCode());
}
@Override
public int getHighest(int x, int z, IrisData data) {
return surface(x, z);
}
@Override
public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) {
return ignoreFluid ? surface(x, z) : Math.max(surface(x, z), 62);
}
@Override
public void set(int x, int y, int z, NativeBlockState d) {
record("set", x, y, z, d == null ? "null" : d.key());
blocks.put(key(x, y, z), d);
}
@Override
public NativeBlockState get(int x, int y, int z) {
NativeBlockState written = blocks.get(key(x, y, z));
if (written != null) {
return written;
}
int surface = surface(x, z);
if (y <= surface) {
return state(y == surface ? "minecraft:grass_block[snowy=false]" : "minecraft:stone");
}
if (y <= 62) {
return state("minecraft:water");
}
return Math.floorMod(x + y + z, 13) == 0 ? state("minecraft:oak_leaves[distance=1,persistent=true,waterlogged=true]") : null;
}
@Override
public boolean isPreventingDecay() {
return preventDecay;
}
@Override
public boolean isCarved(int x, int y, int z) {
return Math.floorMod(x * 7 + y * 3 + z * 11, 29) == 0;
}
@Override
public boolean isSolid(int x, int y, int z) {
NativeBlockState state = get(x, y, z);
return state != null && state.isSolid();
}
@Override
public boolean isUnderwater(int x, int z) {
return surface(x, z) < 62;
}
@Override
public int getFluidHeight() {
return 62;
}
@Override
public int getFluidHeight(int x, int z) {
return 62;
}
@Override
public boolean isDebugSmartBore() {
return false;
}
@Override
public void setTile(int xx, int yy, int zz, TileData tile) {
record("tile", xx, yy, zz, tile == null ? "null" : "tile");
}
@Override
public <T> void setData(int xx, int yy, int zz, T value) {
record("data", xx, yy, zz, value == null ? "null" : value.getClass().getSimpleName() + ":" + value);
if (value instanceof MatterMarker) {
markerWrites++;
}
extra.computeIfAbsent(key(xx, yy, zz), ignored -> new HashMap<>()).put(value == null ? Object.class : value.getClass(), value);
}
@Override
public <T> T getData(int xx, int yy, int zz, Class<T> type) {
Map<Class<?>, Object> values = extra.get(key(xx, yy, zz));
Object value = values == null ? null : values.get(type);
return value == null ? null : type.cast(value);
}
@Override
public boolean beginTreeCanopy(BlockPosition position) {
boolean open = IObjectPlacer.super.beginTreeCanopy(position);
record("canopy", position.getX(), position.getY(), position.getZ(), Boolean.toString(open));
return open;
}
@Override
public Engine getEngine() {
return engine;
}
}
}