mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 22:10:18 +00:00
Merged wave1 into parallel waits
- Terrain bake and object bake from turbo/wave1
This commit is contained in:
commit
cb008b6428
46 files changed
+4807
-866
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) {
|
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();
|
IrisData data = getLoader();
|
||||||
Engine engine = data == null ? null : data.getEngine();
|
Engine engine = data == null ? null : data.getEngine();
|
||||||
CachedChildGenerator recent = recentChildGenerator;
|
CachedChildGenerator recent = recentChildGenerator;
|
||||||
@@ -533,6 +536,10 @@ public class IrisBiome extends IrisRegistrant implements Rarity {
|
|||||||
return random.nextParallelRNG(signature * CHILDREN_SEED_SALT);
|
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) {
|
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);
|
return IrisBiomeLayerGenerator.generateLayers(this, dim, wx, wz, random, maxDepth, height, rdata, complex);
|
||||||
}
|
}
|
||||||
|
|||||||
+102
-71
@@ -30,27 +30,41 @@ import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
|
|||||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||||
import art.arcane.volmlib.util.math.BlockPosition;
|
import art.arcane.volmlib.util.math.BlockPosition;
|
||||||
import art.arcane.iris.generation.block.B;
|
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.Long2IntOpenHashMap;
|
||||||
|
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.Arrays;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
||||||
private static final int CACHE_MISS = Integer.MIN_VALUE;
|
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 IObjectPlacer delegate;
|
||||||
private final Engine engine;
|
private final Engine engine;
|
||||||
private final int anchorY;
|
private final int anchorY;
|
||||||
private final int minDepthBelowSurface;
|
private final int minDepthBelowSurface;
|
||||||
private final int worldHeight;
|
private final int worldHeight;
|
||||||
private final KList<BufferedMutation> mutations;
|
private static final byte BLOCK = 0;
|
||||||
private final Map<PositionKey, NativeBlockState> bufferedBlocks;
|
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 surfaceHeights;
|
||||||
private final Long2IntOpenHashMap caveCeilings;
|
private final Long2IntOpenHashMap caveCeilings;
|
||||||
private int blockWrites;
|
private int blockWrites;
|
||||||
private BlockPosition canopyPosition;
|
private BlockPosition canopyPosition;
|
||||||
|
private boolean originKnown;
|
||||||
|
private int originX;
|
||||||
|
private int originZ;
|
||||||
|
|
||||||
CaveObjectPlacementTransaction(IObjectPlacer delegate, int anchorY, int minDepthBelowSurface) {
|
CaveObjectPlacementTransaction(IObjectPlacer delegate, int anchorY, int minDepthBelowSurface) {
|
||||||
this.delegate = delegate;
|
this.delegate = delegate;
|
||||||
@@ -58,8 +72,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
|||||||
this.anchorY = anchorY;
|
this.anchorY = anchorY;
|
||||||
this.minDepthBelowSurface = Math.max(0, minDepthBelowSurface);
|
this.minDepthBelowSurface = Math.max(0, minDepthBelowSurface);
|
||||||
this.worldHeight = engine == null ? 0 : engine.getHeight();
|
this.worldHeight = engine == null ? 0 : engine.getHeight();
|
||||||
this.mutations = new KList<>();
|
this.bufferedBlocks = new Long2ObjectOpenHashMap<>();
|
||||||
this.bufferedBlocks = new HashMap<>();
|
|
||||||
this.surfaceHeights = new Long2IntOpenHashMap();
|
this.surfaceHeights = new Long2IntOpenHashMap();
|
||||||
this.caveCeilings = new Long2IntOpenHashMap();
|
this.caveCeilings = new Long2IntOpenHashMap();
|
||||||
this.surfaceHeights.defaultReturnValue(CACHE_MISS);
|
this.surfaceHeights.defaultReturnValue(CACHE_MISS);
|
||||||
@@ -73,28 +86,41 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
IrisComplex complex = engine == null ? null : engine.getComplex();
|
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
|
if (complex != null
|
||||||
&& !complex.allowsNewDiscreteContentAt(mutation.x(), mutation.z())) {
|
&& !complex.allowsNewDiscreteContentAt(x, z)) {
|
||||||
discard();
|
discard();
|
||||||
return CommitResult.REJECTED_TRANSITION;
|
return CommitResult.REJECTED_TRANSITION;
|
||||||
}
|
}
|
||||||
if (!isWithinBounds(mutation.x(), mutation.y(), mutation.z())) {
|
if (!isWithinBounds(x, y, z)) {
|
||||||
discard();
|
discard();
|
||||||
return CommitResult.REJECTED_BOUNDS;
|
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)
|
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();
|
discard();
|
||||||
return CommitResult.REJECTED_SUBTERRAIN;
|
return CommitResult.REJECTED_SUBTERRAIN;
|
||||||
}
|
}
|
||||||
HydrologyCaveCell hydrology = delegate.getData(
|
HydrologyCaveCell hydrology = delegate.getData(x, y, z, HydrologyCaveCell.class);
|
||||||
mutation.x(), mutation.y(), mutation.z(), HydrologyCaveCell.class);
|
|
||||||
if (hydrology != null && hydrology.protectsPlacement()) {
|
if (hydrology != null && hydrology.protectsPlacement()) {
|
||||||
discard();
|
discard();
|
||||||
return CommitResult.REJECTED_HYDROLOGY;
|
return CommitResult.REJECTED_HYDROLOGY;
|
||||||
}
|
}
|
||||||
|
previousChecked = true;
|
||||||
|
previousX = x;
|
||||||
|
previousY = y;
|
||||||
|
previousZ = z;
|
||||||
}
|
}
|
||||||
|
|
||||||
applyMutations();
|
applyMutations();
|
||||||
@@ -106,18 +132,26 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
|||||||
boolean canopyOpen = false;
|
boolean canopyOpen = false;
|
||||||
boolean skipCanopy = false;
|
boolean skipCanopy = false;
|
||||||
try {
|
try {
|
||||||
for (BufferedMutation mutation : mutations) {
|
for (int index = 0; index < mutationCount; index++) {
|
||||||
if (mutation.value() instanceof NativeBlockState) {
|
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) {
|
if (canopyOpen) {
|
||||||
delegate.endTreeCanopy();
|
delegate.endTreeCanopy();
|
||||||
canopyOpen = false;
|
canopyOpen = false;
|
||||||
}
|
}
|
||||||
skipCanopy = mutation.canopy()
|
skipCanopy = canopy && !delegate.beginTreeCanopy(new BlockPosition(x, y, z));
|
||||||
&& !delegate.beginTreeCanopy(new BlockPosition(mutation.x(), mutation.y(), mutation.z()));
|
canopyOpen = canopy && !skipCanopy;
|
||||||
canopyOpen = mutation.canopy() && !skipCanopy;
|
}
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
if (!mutation.canopy() || !skipCanopy) {
|
|
||||||
mutation.apply(delegate);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -128,11 +162,31 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void discard() {
|
void discard() {
|
||||||
mutations.clear();
|
Arrays.fill(mutationValues, 0, mutationCount, null);
|
||||||
|
mutationCount = 0;
|
||||||
bufferedBlocks.clear();
|
bufferedBlocks.clear();
|
||||||
blockWrites = 0;
|
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) {
|
int getCaveCeiling(int x, int z) {
|
||||||
long key = Cache.key(x, z);
|
long key = Cache.key(x, z);
|
||||||
int cached = caveCeilings.get(key);
|
int cached = caveCeilings.get(key);
|
||||||
@@ -164,8 +218,8 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
|||||||
if (state == null) {
|
if (state == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
mutations.add(new BlockMutation(x, y, z, state, isCanopy(x, y, z)));
|
buffer(BLOCK, x, y, z, state);
|
||||||
bufferedBlocks.put(new PositionKey(x, y, z), state);
|
bufferedBlocks.put(bufferKey(x, y, z), state);
|
||||||
blockWrites++;
|
blockWrites++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,7 +244,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public NativeBlockState get(int x, int y, int z) {
|
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;
|
return buffered == null ? delegate.get(x, y, z) : buffered;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,7 +265,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isSolid(int x, int y, int z) {
|
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);
|
return buffered == null ? delegate.isSolid(x, y, z) : B.isSolid(buffered);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,7 +296,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
|||||||
@Override
|
@Override
|
||||||
public void setTile(int x, int y, int z, TileData tile) {
|
public void setTile(int x, int y, int z, TileData tile) {
|
||||||
if (tile != null) {
|
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) {
|
if (data == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
mutations.add(new DataMutation(x, y, z, data, isCanopy(x, y, z)));
|
buffer(DATA, x, y, z, data);
|
||||||
if (data instanceof NativeBlockState state) {
|
if (data instanceof NativeBlockState state) {
|
||||||
bufferedBlocks.put(new PositionKey(x, y, z), state);
|
bufferedBlocks.put(bufferKey(x, y, z), state);
|
||||||
blockWrites++;
|
blockWrites++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <T> @Nullable T getData(int x, int y, int z, Class<T> type) {
|
public <T> @Nullable T getData(int x, int y, int z, Class<T> type) {
|
||||||
for (int i = mutations.size() - 1; i >= 0; i--) {
|
for (int index = mutationCount - 1; index >= 0; index--) {
|
||||||
BufferedMutation mutation = mutations.get(i);
|
if (mutationX[index] != x || mutationY[index] != y || mutationZ[index] != z) {
|
||||||
if (mutation.x() != x || mutation.y() != y || mutation.z() != z) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Object value = mutation.value();
|
Object value = mutationValues[index];
|
||||||
if (type.isInstance(value)) {
|
if (type.isInstance(value)) {
|
||||||
return type.cast(value);
|
return type.cast(value);
|
||||||
}
|
}
|
||||||
@@ -278,6 +331,22 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
|||||||
return engine;
|
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) {
|
private boolean isWithinBounds(int x, int y, int z) {
|
||||||
if (engine == null || y < 0 || y >= worldHeight) {
|
if (engine == null || y < 0 || y >= worldHeight) {
|
||||||
return false;
|
return false;
|
||||||
@@ -325,42 +394,4 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
|||||||
REJECTED_HYDROLOGY,
|
REJECTED_HYDROLOGY,
|
||||||
REJECTED_SUBTERRAIN
|
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 Map<String, CaveRejectLogState> CAVE_REJECT_LOG_STATE = new ConcurrentHashMap<>();
|
||||||
private static final Set<String> MISSING_LOAD_KEY_WARNED = ConcurrentHashMap.newKeySet();
|
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 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 Object collisionRuleLock;
|
||||||
private final ObjectSourcePlanCache sourcePlans;
|
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) {
|
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
|
@Override
|
||||||
@@ -157,19 +166,23 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
|||||||
ObjectDestinationTransaction transaction = new ObjectDestinationTransaction(writer, x, z);
|
ObjectDestinationTransaction transaction = new ObjectDestinationTransaction(writer, x, z);
|
||||||
int radius = getRadius();
|
int radius = getRadius();
|
||||||
int sourceChunkRadius = sourceChunkRadius(radius);
|
int sourceChunkRadius = sourceChunkRadius(radius);
|
||||||
replaySourceChunks(
|
int side = (sourceChunkRadius * 2) + 1;
|
||||||
x,
|
ObjectSourcePlan[] plans = new ObjectSourcePlan[side * side];
|
||||||
z,
|
int[] slot = new int[1];
|
||||||
radius,
|
replaySourceChunks(x, z, radius, (sourceX, sourceZ) -> plans[slot[0]++] = sourcePlans.tryAcquire(
|
||||||
(sourceX, sourceZ) -> {
|
sourceX, sourceZ, x, z, sourceChunkRadius, () -> buildSourcePlan(writer, sourceX, sourceZ, context)));
|
||||||
ObjectSourcePlan plan = sourcePlans.acquire(sourceX, sourceZ, x, z, sourceChunkRadius,
|
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));
|
() -> buildSourcePlan(writer, sourceX, sourceZ, context));
|
||||||
|
}
|
||||||
if (plan != null) {
|
if (plan != null) {
|
||||||
transaction.apply(plan);
|
transaction.apply(plan);
|
||||||
plan.persistContinuations(writer, sourceX, sourceZ, x, z);
|
plan.persistContinuations(writer, sourceX, sourceZ, x, z);
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
);
|
|
||||||
transaction.commit();
|
transaction.commit();
|
||||||
logSourcePlanStats();
|
logSourcePlanStats();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -244,7 +244,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
|
|||||||
synchronized (chunk) {
|
synchronized (chunk) {
|
||||||
Matter matter = chunk.getOrCreate(y >> 4);
|
Matter matter = chunk.getOrCreate(y >> 4);
|
||||||
if ((t instanceof NativeBlockState || t instanceof MatterCavern)
|
if ((t instanceof NativeBlockState || t instanceof MatterCavern)
|
||||||
&& (hasProtectedHydrology(matter, x, y, z) || protectsSubterrainPlacement(x, y, z))) {
|
&& hasProtectedHydrology(matter, x, y, z)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (t instanceof NativeBlockState) {
|
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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
+110
-75
@@ -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.hydrology.cave.HydrologyCaveCell;
|
||||||
import art.arcane.iris.generation.subterrain.SubterrainRasterizer;
|
import art.arcane.iris.generation.subterrain.SubterrainRasterizer;
|
||||||
import art.arcane.iris.generation.block.TileData;
|
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.nativelib.terrain.NativeBlockState;
|
||||||
import art.arcane.volmlib.util.math.BlockPosition;
|
import art.arcane.volmlib.util.math.BlockPosition;
|
||||||
import art.arcane.iris.generation.block.B;
|
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.Int2ObjectMap;
|
||||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
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.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
|||||||
private final int destinationChunkX;
|
private final int destinationChunkX;
|
||||||
private final int destinationChunkZ;
|
private final int destinationChunkZ;
|
||||||
private final int worldHeight;
|
private final int worldHeight;
|
||||||
private final List<Mutation> mutations;
|
private final MutationLog log = new MutationLog();
|
||||||
private final List<PlacementRange> placements = new ArrayList<>();
|
private final List<PlacementRange> placements = new ArrayList<>();
|
||||||
private final Long2ObjectOpenHashMap<Int2ObjectOpenHashMap<OverlayCell>> overlay;
|
private final Long2ObjectOpenHashMap<Int2ObjectOpenHashMap<OverlayCell>> overlay;
|
||||||
private BlockPosition canopyPosition;
|
private BlockPosition canopyPosition;
|
||||||
@@ -40,13 +40,17 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
|||||||
private int hydrologyGateChunkX;
|
private int hydrologyGateChunkX;
|
||||||
private int hydrologyGateChunkZ;
|
private int hydrologyGateChunkZ;
|
||||||
private boolean hydrologyGateOpen;
|
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) {
|
ObjectDestinationTransaction(MantleWriter writer, int destinationChunkX, int destinationChunkZ) {
|
||||||
this.writer = writer;
|
this.writer = writer;
|
||||||
this.destinationChunkX = destinationChunkX;
|
this.destinationChunkX = destinationChunkX;
|
||||||
this.destinationChunkZ = destinationChunkZ;
|
this.destinationChunkZ = destinationChunkZ;
|
||||||
this.worldHeight = writer.getMantle().getWorldHeight();
|
this.worldHeight = writer.getMantle().getWorldHeight();
|
||||||
this.mutations = new ArrayList<>();
|
|
||||||
this.overlay = new Long2ObjectOpenHashMap<>();
|
this.overlay = new Long2ObjectOpenHashMap<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,11 +59,11 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int mutationCheckpoint() {
|
int mutationCheckpoint() {
|
||||||
return mutations.size();
|
return log.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
ObjectSourcePlan sourcePlanSince(int checkpoint) {
|
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");
|
throw new IllegalArgumentException("Mutation checkpoint is outside the transaction");
|
||||||
}
|
}
|
||||||
ArrayList<PlacementRange> accepted = new ArrayList<>();
|
ArrayList<PlacementRange> accepted = new ArrayList<>();
|
||||||
@@ -68,65 +72,91 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
|||||||
accepted.add(new PlacementRange(placement.start() - checkpoint, placement.end() - checkpoint));
|
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
|
@Override
|
||||||
public int beginObjectPlacement() {
|
public int beginObjectPlacement() {
|
||||||
return mutations.size();
|
return log.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void endObjectPlacement(int checkpoint) {
|
public void endObjectPlacement(int checkpoint) {
|
||||||
if (checkpoint >= 0 && checkpoint < mutations.size()) {
|
if (checkpoint >= 0 && checkpoint < log.size()) {
|
||||||
placements.add(new PlacementRange(checkpoint, mutations.size()));
|
placements.add(new PlacementRange(checkpoint, log.size()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void apply(ObjectSourcePlan plan) {
|
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;
|
boolean skipCanopy = false;
|
||||||
for (Mutation mutation : plan.mutationsFor(destinationChunkX, destinationChunkZ)) {
|
int end = plan.slotEnd(slot);
|
||||||
if (mutation.key().type() == NativeBlockState.class) {
|
for (int index = plan.slotStart(slot); index < end; index++) {
|
||||||
NativeBlockState existing = mutation.canopy() ? get(mutation.key().x(), mutation.key().y(), mutation.key().z()) : null;
|
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();
|
skipCanopy = existing != null && existing.isSolid();
|
||||||
}
|
}
|
||||||
if (mutation.canopy() && skipCanopy) {
|
if (canopy && skipCanopy) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
switch (mutation) {
|
if (custom) {
|
||||||
case SetMutation set -> setData(set.key().x(), set.key().y(), set.key().z(), set.value(), set);
|
set(x, y, z, (NativeBlockState) value, true, canopy);
|
||||||
case CustomBlockMutation custom -> set(custom.key().x(), custom.key().y(), custom.key().z(), custom.state(), custom);
|
} else {
|
||||||
|
setData(x, y, z, value, true, canopy);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void commitFenced() {
|
private void commitFenced() {
|
||||||
LinkedHashMap<DataKey, Object> originals = new LinkedHashMap<>();
|
CommitJournal journal = new CommitJournal();
|
||||||
try {
|
try {
|
||||||
for (Mutation mutation : mutations) {
|
for (int position = 0; position < log.size(); position++) {
|
||||||
if (!isDestination(mutation.x(), mutation.z())) {
|
int x = log.x(position);
|
||||||
|
int z = log.z(position);
|
||||||
|
if (!isDestination(x, z)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
captureOriginal(originals, mutation.key());
|
int y = log.y(position);
|
||||||
if (mutation.key().type() == NativeBlockState.class) {
|
Object value = log.value(position);
|
||||||
TileWrapper tile = writer.getDataIfPresent(
|
boolean custom = log.custom(position);
|
||||||
mutation.key().x(), mutation.key().y(), mutation.key().z(), TileWrapper.class);
|
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) {
|
if (tile != null) {
|
||||||
originals.putIfAbsent(new DataKey(
|
journal.capture(x, y, z, TileWrapper.class, tile);
|
||||||
mutation.key().x(), mutation.key().y(), mutation.key().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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mutation.apply(writer);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (custom) {
|
||||||
|
writer.set(x, y, z, (NativeBlockState) value);
|
||||||
|
} else {
|
||||||
|
writer.setData(x, y, z, value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (Throwable failure) {
|
} catch (Throwable failure) {
|
||||||
rollback(originals, failure);
|
journal.rollback(failure);
|
||||||
if (failure instanceof RuntimeException runtimeException) {
|
if (failure instanceof RuntimeException runtimeException) {
|
||||||
throw runtimeException;
|
throw runtimeException;
|
||||||
}
|
}
|
||||||
@@ -149,10 +179,10 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void set(int x, int y, int z, NativeBlockState state) {
|
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) {
|
if (state == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -167,8 +197,7 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
|||||||
cell.block = baseState;
|
cell.block = baseState;
|
||||||
cell.tile = CLEARED;
|
cell.tile = CLEARED;
|
||||||
cell.identifier = identifier;
|
cell.identifier = identifier;
|
||||||
mutations.add(replayed != null
|
log.add(x, y, z, state, true, replayed ? replayedCanopy : isCanopy(x, y, z));
|
||||||
? replayed : new CustomBlockMutation(new DataKey(x, y, z, NativeBlockState.class), state, isCanopy(x, y, z)));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setData(x, y, z, state);
|
setData(x, y, z, state);
|
||||||
@@ -262,10 +291,10 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <T> void setData(int x, int y, int z, T data) {
|
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
|
if (data == null || y < 0 || y >= worldHeight
|
||||||
|| protectsSubterrain(x, y, z)) {
|
|| protectsSubterrain(x, y, z)) {
|
||||||
return;
|
return;
|
||||||
@@ -285,8 +314,7 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
|||||||
} else {
|
} else {
|
||||||
cell.put(type, data);
|
cell.put(type, data);
|
||||||
}
|
}
|
||||||
mutations.add(replayed != null
|
log.add(x, y, z, data, false, replayed ? replayedCanopy : isCanopy(x, y, z));
|
||||||
? replayed : new SetMutation(new DataKey(x, y, z, type), data, isCanopy(x, y, z)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -368,18 +396,6 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
|||||||
return value == null ? CLEARED : value;
|
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) {
|
private boolean isDestination(int x, int z) {
|
||||||
return (x >> 4) == destinationChunkX && (z >> 4) == destinationChunkZ;
|
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) {
|
private boolean protectsSubterrain(int x, int y, int z) {
|
||||||
|
if (subterrainKnown && x == subterrainX && y == subterrainY && z == subterrainZ) {
|
||||||
|
return subterrainProtects;
|
||||||
|
}
|
||||||
Engine engine = getEngine();
|
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) {
|
private boolean hasProtectedHydrology(int x, int y, int z) {
|
||||||
@@ -419,14 +444,32 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
|||||||
return hydrologyGateOpen;
|
return hydrologyGateOpen;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void rollback(LinkedHashMap<DataKey, Object> originals, Throwable failure) {
|
private final class CommitJournal {
|
||||||
ArrayList<Map.Entry<DataKey, Object>> entries = new ArrayList<>(originals.entrySet());
|
private final Reference2IntOpenHashMap<Class<?>> typeIds = new Reference2IntOpenHashMap<>();
|
||||||
for (int index = entries.size() - 1; index >= 0; index--) {
|
private final LongOpenHashSet keys = new LongOpenHashSet();
|
||||||
Map.Entry<DataKey, Object> entry = entries.get(index);
|
private final ArrayList<Original> originals = new ArrayList<>();
|
||||||
DataKey key = entry.getKey();
|
|
||||||
|
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 {
|
try {
|
||||||
writer.restoreData(key.x(), key.y(), key.z(), key.type(),
|
writer.restoreData(original.x(), original.y(), original.z(), original.type(),
|
||||||
entry.getValue() == CLEARED ? null : entry.getValue());
|
original.value() == CLEARED ? null : original.value());
|
||||||
} catch (Throwable rollbackFailure) {
|
} catch (Throwable rollbackFailure) {
|
||||||
if (rollbackFailure != failure) {
|
if (rollbackFailure != failure) {
|
||||||
failure.addSuppressed(rollbackFailure);
|
failure.addSuppressed(rollbackFailure);
|
||||||
@@ -434,6 +477,10 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Original(int x, int y, int z, Class<?> type, Object value) {
|
||||||
|
}
|
||||||
|
|
||||||
private static final class OverlayCell {
|
private static final class OverlayCell {
|
||||||
private Object block;
|
private Object block;
|
||||||
@@ -506,8 +553,6 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
|||||||
DataKey key();
|
DataKey key();
|
||||||
|
|
||||||
boolean canopy();
|
boolean canopy();
|
||||||
|
|
||||||
void apply(IObjectPlacer placer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
record SetMutation(DataKey key, Object value, boolean canopy) implements Mutation {
|
record SetMutation(DataKey key, Object value, boolean canopy) implements Mutation {
|
||||||
@@ -520,11 +565,6 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
|||||||
public int z() {
|
public int z() {
|
||||||
return key.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 {
|
record CustomBlockMutation(DataKey key, NativeBlockState state, boolean canopy) implements Mutation {
|
||||||
@@ -537,10 +577,5 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
|||||||
public int z() {
|
public int z() {
|
||||||
return key.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.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
final class ObjectSourcePlan {
|
final class ObjectSourcePlan {
|
||||||
static final ObjectSourcePlan EMPTY = new ObjectSourcePlan(List.of());
|
static final ObjectSourcePlan EMPTY = new ObjectSourcePlan(List.of());
|
||||||
@@ -45,18 +44,21 @@ final class ObjectSourcePlan {
|
|||||||
private final int estimatedRetainedBytes;
|
private final int estimatedRetainedBytes;
|
||||||
|
|
||||||
ObjectSourcePlan(List<ObjectDestinationTransaction.Mutation> mutations) {
|
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) {
|
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];
|
long[] mutationDestinations = new long[count];
|
||||||
Long2IntOpenHashMap destinationSlots = new Long2IntOpenHashMap();
|
Long2IntOpenHashMap destinationSlots = new Long2IntOpenHashMap();
|
||||||
destinationSlots.defaultReturnValue(-1);
|
destinationSlots.defaultReturnValue(-1);
|
||||||
LongArrayList firstSeen = new LongArrayList();
|
LongArrayList firstSeen = new LongArrayList();
|
||||||
for (int index = 0; index < count; index++) {
|
for (int index = 0; index < count; index++) {
|
||||||
ObjectDestinationTransaction.Mutation mutation = Objects.requireNonNull(mutations.get(index), "mutation");
|
long destination = destinationKey(log.x(from + index) >> 4, log.z(from + index) >> 4);
|
||||||
long destination = destinationKey(mutation.x() >> 4, mutation.z() >> 4);
|
|
||||||
mutationDestinations[index] = destination;
|
mutationDestinations[index] = destination;
|
||||||
if (destinationSlots.putIfAbsent(destination, firstSeen.size()) == -1) {
|
if (destinationSlots.putIfAbsent(destination, firstSeen.size()) == -1) {
|
||||||
firstSeen.add(destination);
|
firstSeen.add(destination);
|
||||||
@@ -80,38 +82,23 @@ final class ObjectSourcePlan {
|
|||||||
valueIndexes = new int[count];
|
valueIndexes = new int[count];
|
||||||
ValueTable table = new ValueTable();
|
ValueTable table = new ValueTable();
|
||||||
for (int index = 0; index < count; index++) {
|
for (int index = 0; index < count; index++) {
|
||||||
ObjectDestinationTransaction.Mutation mutation = mutations.get(index);
|
int logged = from + index;
|
||||||
int position = cursor[destinationSlots.get(mutationDestinations[index])]++;
|
int position = cursor[destinationSlots.get(mutationDestinations[index])]++;
|
||||||
positions[index] = position;
|
positions[index] = position;
|
||||||
ObjectDestinationTransaction.DataKey key = mutation.key();
|
int y = log.y(logged);
|
||||||
if (key.y() < 0 || key.y() > MAXIMUM_Y) {
|
if (y < 0 || y > MAXIMUM_Y) {
|
||||||
throw new IllegalArgumentException("Object mutation height is outside the plan range: " + key.y());
|
throw new IllegalArgumentException("Object mutation height is outside the plan range: " + y);
|
||||||
}
|
}
|
||||||
Object value;
|
cells[position] = (y << Y_SHIFT) | (log.custom(logged) ? CUSTOM_BIT : 0) | (log.canopy(logged) ? CANOPY_BIT : 0)
|
||||||
int custom;
|
| ((log.z(logged) & 15) << 4) | (log.x(logged) & 15);
|
||||||
switch (mutation) {
|
valueIndexes[position] = table.indexOf(log.value(logged));
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
values = table.values.toArray();
|
values = table.values.toArray();
|
||||||
|
|
||||||
ArrayList<Placement> crossing = new ArrayList<>();
|
ArrayList<Placement> crossing = new ArrayList<>();
|
||||||
for (int ordinal = 0; ordinal < accepted.size(); ordinal++) {
|
for (int ordinal = 0; ordinal < accepted.size(); ordinal++) {
|
||||||
ObjectDestinationTransaction.PlacementRange range = accepted.get(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) {
|
if (placement != null) {
|
||||||
crossing.add(placement);
|
crossing.add(placement);
|
||||||
}
|
}
|
||||||
@@ -147,12 +134,13 @@ final class ObjectSourcePlan {
|
|||||||
int ordinal,
|
int ordinal,
|
||||||
int start,
|
int start,
|
||||||
int end,
|
int end,
|
||||||
List<ObjectDestinationTransaction.Mutation> mutations,
|
MutationLog log,
|
||||||
|
int from,
|
||||||
long[] mutationDestinations,
|
long[] mutationDestinations,
|
||||||
Long2IntOpenHashMap destinationSlots,
|
Long2IntOpenHashMap destinationSlots,
|
||||||
int[] positions
|
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");
|
throw new IllegalArgumentException("Object placement range is outside the plan");
|
||||||
}
|
}
|
||||||
long first = mutationDestinations[start];
|
long first = mutationDestinations[start];
|
||||||
@@ -172,11 +160,12 @@ final class ObjectSourcePlan {
|
|||||||
int maximumX = Integer.MIN_VALUE;
|
int maximumX = Integer.MIN_VALUE;
|
||||||
int maximumZ = Integer.MIN_VALUE;
|
int maximumZ = Integer.MIN_VALUE;
|
||||||
for (int mutationIndex = start; mutationIndex < end; mutationIndex++) {
|
for (int mutationIndex = start; mutationIndex < end; mutationIndex++) {
|
||||||
ObjectDestinationTransaction.Mutation mutation = mutations.get(mutationIndex);
|
int mutationX = log.x(from + mutationIndex);
|
||||||
minimumX = Math.min(minimumX, mutation.x());
|
int mutationZ = log.z(from + mutationIndex);
|
||||||
minimumZ = Math.min(minimumZ, mutation.z());
|
minimumX = Math.min(minimumX, mutationX);
|
||||||
maximumX = Math.max(maximumX, mutation.x());
|
minimumZ = Math.min(minimumZ, mutationZ);
|
||||||
maximumZ = Math.max(maximumZ, mutation.z());
|
maximumX = Math.max(maximumX, mutationX);
|
||||||
|
maximumZ = Math.max(maximumZ, mutationZ);
|
||||||
long destination = mutationDestinations[mutationIndex];
|
long destination = mutationDestinations[mutationIndex];
|
||||||
int touchedIndex = touchedIndexes.get(destination);
|
int touchedIndex = touchedIndexes.get(destination);
|
||||||
if (touchedIndex < 0) {
|
if (touchedIndex < 0) {
|
||||||
@@ -216,6 +205,55 @@ final class ObjectSourcePlan {
|
|||||||
return destinations.length == 0;
|
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) {
|
List<ObjectDestinationTransaction.Mutation> mutationsFor(int chunkX, int chunkZ) {
|
||||||
int slot = destinationSlot(chunkX, chunkZ);
|
int slot = destinationSlot(chunkX, chunkZ);
|
||||||
return slot < 0 ? List.of() : decode(chunkX, chunkZ, offsets[slot], offsets[slot + 1] - offsets[slot]);
|
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) {
|
private List<ObjectDestinationTransaction.Mutation> decode(int chunkX, int chunkZ, int start, int length) {
|
||||||
ObjectDestinationTransaction.Mutation[] decoded = new ObjectDestinationTransaction.Mutation[length];
|
ObjectDestinationTransaction.Mutation[] decoded = new ObjectDestinationTransaction.Mutation[length];
|
||||||
int baseX = chunkX << 4;
|
|
||||||
int baseZ = chunkZ << 4;
|
|
||||||
for (int index = 0; index < length; index++) {
|
for (int index = 0; index < length; index++) {
|
||||||
int cell = cells[start + index];
|
decoded[index] = mutationAt(chunkX, chunkZ, 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);
|
|
||||||
}
|
}
|
||||||
return List.of(decoded);
|
return List.of(decoded);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import com.github.benmanes.caffeine.cache.Cache;
|
|||||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||||
import art.arcane.volmlib.util.cache.CacheKey;
|
import art.arcane.volmlib.util.cache.CacheKey;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.CompletionException;
|
import java.util.concurrent.CompletionException;
|
||||||
@@ -14,6 +15,7 @@ import java.util.function.Supplier;
|
|||||||
import static art.arcane.iris.generation.cache.Cache.key;
|
import static art.arcane.iris.generation.cache.Cache.key;
|
||||||
|
|
||||||
final class ObjectSourcePlanCache {
|
final class ObjectSourcePlanCache {
|
||||||
|
static final ObjectSourcePlan BUSY = new ObjectSourcePlan(List.of());
|
||||||
private static final long MAXIMUM_RETAINED_BYTES = 512L * 1024L * 1024L;
|
private static final long MAXIMUM_RETAINED_BYTES = 512L * 1024L * 1024L;
|
||||||
private static final int ENTRY_BYTES = 64;
|
private static final int ENTRY_BYTES = 64;
|
||||||
|
|
||||||
@@ -68,6 +70,19 @@ final class ObjectSourcePlanCache {
|
|||||||
return plan;
|
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,
|
private ObjectSourcePlan claim(State current, long source, Entry entry, int destinationChunkX, int destinationChunkZ,
|
||||||
Supplier<ObjectSourcePlan> builder) {
|
Supplier<ObjectSourcePlan> builder) {
|
||||||
int slot = entry.slot(destinationChunkX, destinationChunkZ);
|
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.Setter;
|
||||||
import lombok.experimental.Accessors;
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.function.DoubleBinaryOperator;
|
|
||||||
|
|
||||||
@Accessors(chain = true)
|
@Accessors(chain = true)
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@@ -211,7 +208,7 @@ public class IrisGenerator extends IrisRegistrant {
|
|||||||
if (surfaceDetail == 1D) {
|
if (surfaceDetail == 1D) {
|
||||||
return sampleHeight(rx, rz, superSeed, data, engine);
|
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() {
|
public boolean supportsBatch() {
|
||||||
@@ -227,90 +224,84 @@ public class IrisGenerator extends IrisRegistrant {
|
|||||||
return !hasCliffs() || cliffHeightGenerator.supportsBatch();
|
return !hasCliffs() || cliffHeightGenerator.supportsBatch();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void getHeights(double[] xs, double[] zs, long superSeed, double[] target, int count) {
|
public GeneratorProgram compileProgram(long superSeed) {
|
||||||
Objects.checkFromIndexSize(0, count, xs.length);
|
if (!Double.isFinite(surfaceDetail) || surfaceDetail < 0D || surfaceDetail > 1D) {
|
||||||
Objects.checkFromIndexSize(0, count, zs.length);
|
throw new IllegalArgumentException("Generator surfaceDetail must be finite and between 0 and 1");
|
||||||
Objects.checkFromIndexSize(0, count, target.length);
|
|
||||||
if (count > 4096) {
|
|
||||||
throw new IllegalArgumentException("Generator batch exceeds 4096 samples");
|
|
||||||
}
|
}
|
||||||
if (count < 8 || !supportsBatch()) {
|
if (!supportsBatch()) {
|
||||||
for (int index = 0; index < count; index++) {
|
throw new IllegalStateException("Generator " + getLoadKey() + " cannot be compiled into a batch program");
|
||||||
target[index] = getHeight(xs[index], zs[index], superSeed);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (composite.isEmpty()) {
|
|
||||||
Arrays.fill(target, 0, count, 0D);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
IrisData data = getLoader();
|
IrisData data = getLoader();
|
||||||
Engine engine = data == null ? null : data.getEngine();
|
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);
|
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;
|
double totalOpacity = 0D;
|
||||||
for (int index = 0; index < layers.length; index++) {
|
for (int index = 0; index < layers.length; index++) {
|
||||||
IrisNoiseGenerator layer = composite.get(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) {
|
if (!multiplicitive) {
|
||||||
totalOpacity += layer.getOpacity();
|
totalOpacity += layer.getOpacity();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
double sumOpacity = totalOpacity;
|
|
||||||
double cliffSeed = (double) (superSeed + 294596 + hc) - 34857;
|
double cliffSeed = (double) (superSeed + 294596 + hc) - 34857;
|
||||||
DoubleBinaryOperator cliffs = hasCliffs() ? cliffHeightGenerator.prepareBatch(
|
NoiseLayerProgram cliffs = hasCliffs()
|
||||||
(long) (getSeed() + cliffSeed + hc), data, engine) : null;
|
? cliffHeightGenerator.compileProgram((long) (getSeed() + cliffSeed + hc), data, engine)
|
||||||
return (rx, rz) -> {
|
: null;
|
||||||
double sampleX = (rx + offsetX) / zoom;
|
return new GeneratorProgram(this, superSeed, layers, totalOpacity, cliffs);
|
||||||
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;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private double surfaceHeight(double rx, double rz, long superSeed, IrisData data, Engine engine,
|
SurfaceCacheStripe[] surfaceStripes() {
|
||||||
DoubleBinaryOperator prepared) {
|
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();
|
SurfaceCacheStripe[] stripes = surfaceCache(data, engine).stripes();
|
||||||
double lowerX = Math.floor(rx / SURFACE_GRID) * SURFACE_GRID;
|
double lowerX = Math.floor(rx / SURFACE_GRID) * SURFACE_GRID;
|
||||||
double lowerZ = Math.floor(rz / SURFACE_GRID) * SURFACE_GRID;
|
double lowerZ = Math.floor(rz / SURFACE_GRID) * SURFACE_GRID;
|
||||||
double dx = (rx - lowerX) / SURFACE_GRID;
|
double dx = (rx - lowerX) / SURFACE_GRID;
|
||||||
double dz = (rz - lowerZ) / 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) {
|
if (dx == 0D && dz == 0D) {
|
||||||
return northWest;
|
return northWest;
|
||||||
}
|
}
|
||||||
double north = dx == 0D ? northWest : IrisInterpolation.lerp(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;
|
double smooth = north;
|
||||||
if (dz != 0D) {
|
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,
|
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);
|
smooth = IrisInterpolation.lerp(north, south, dz);
|
||||||
}
|
}
|
||||||
return surfaceDetail == 0D ? smooth
|
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) {
|
private double sampleHeight(double rx, double rz, long superSeed, IrisData data, Engine engine) {
|
||||||
@@ -350,21 +341,19 @@ public class IrisGenerator extends IrisRegistrant {
|
|||||||
return v;
|
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) {
|
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 key = ((long) (int) x << 32) | ((int) z & 0xffffffffL);
|
||||||
long mixed = key ^ (key >>> 33);
|
long mixed = surfaceMix(key);
|
||||||
mixed *= 0xff51afd7ed558ccdL;
|
|
||||||
mixed ^= mixed >>> 33;
|
|
||||||
SurfaceCacheStripe stripe = stripes[(int) mixed & (SURFACE_CACHE_STRIPES - 1)];
|
SurfaceCacheStripe stripe = stripes[(int) mixed & (SURFACE_CACHE_STRIPES - 1)];
|
||||||
int slot = (int) (mixed >>> 4) & (SURFACE_CACHE_STRIPE_SIZE - 1);
|
int slot = (int) (mixed >>> 4) & (SURFACE_CACHE_STRIPE_SIZE - 1);
|
||||||
SurfaceSample cached = stripe.get(key, superSeed, slot);
|
SurfaceSample cached = stripe.get(key, superSeed, slot);
|
||||||
if (cached != null) {
|
if (cached != null) {
|
||||||
return cached.height();
|
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));
|
stripe.put(slot, new SurfaceSample(key, superSeed, height));
|
||||||
return height;
|
return height;
|
||||||
}
|
}
|
||||||
@@ -457,7 +446,7 @@ public class IrisGenerator extends IrisRegistrant {
|
|||||||
private record SurfaceSample(long key, long seed, double height) {
|
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 final AtomicReferenceArray<SurfaceSample> entries = new AtomicReferenceArray<>(SURFACE_CACHE_STRIPE_SIZE);
|
||||||
|
|
||||||
private SurfaceSample get(long key, long seed, int slot) {
|
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.OffsetNoiseGenerator;
|
||||||
import art.arcane.volmlib.util.noise.PerlinNoise;
|
import art.arcane.volmlib.util.noise.PerlinNoise;
|
||||||
import art.arcane.volmlib.util.noise.SimplexNoise;
|
import art.arcane.volmlib.util.noise.SimplexNoise;
|
||||||
|
import art.arcane.volmlib.util.noise.program.NoiseProgram;
|
||||||
import lombok.AccessLevel;
|
import lombok.AccessLevel;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@@ -47,8 +48,8 @@ import lombok.NoArgsConstructor;
|
|||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import lombok.experimental.Accessors;
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.function.DoubleBinaryOperator;
|
|
||||||
|
|
||||||
@Snippet("generator")
|
@Snippet("generator")
|
||||||
@Accessors(chain = true)
|
@Accessors(chain = true)
|
||||||
@@ -198,44 +199,29 @@ public class IrisNoiseGenerator {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
DoubleBinaryOperator prepareBatch(long superSeed, IrisData data, Engine engine) {
|
NoiseLayerProgram compileProgram(long superSeed, IrisData data, Engine engine) {
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
double value = offsetY;
|
return NoiseLayerProgram.disabled(this);
|
||||||
return (x, z) -> value;
|
|
||||||
}
|
}
|
||||||
if (hasDeepFracture()) {
|
int enabledChildren = 0;
|
||||||
FractureSamples samples = new FractureSamples();
|
for (IrisNoiseGenerator child : fracture) {
|
||||||
if (samples.prepare(this, superSeed, data, engine)) {
|
|
||||||
return samples::sample;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DoubleBinaryOperator[] children = new DoubleBinaryOperator[fracture.size()];
|
|
||||||
double[] offsets = new double[fracture.size()];
|
|
||||||
int salt = 33;
|
|
||||||
for (int index = 0; index < children.length; index++) {
|
|
||||||
IrisNoiseGenerator child = fracture.get(index);
|
|
||||||
if (child.isEnabled()) {
|
if (child.isEnabled()) {
|
||||||
children[index] = child.prepareBatch(superSeed + seed + salt, data, engine);
|
enabledChildren++;
|
||||||
offsets[index] = child.getOpacity() / 2D;
|
}
|
||||||
|
}
|
||||||
|
NoiseLayerProgram[] children = new NoiseLayerProgram[enabledChildren];
|
||||||
|
double[] offsets = new double[enabledChildren];
|
||||||
|
int salt = 33;
|
||||||
|
int compiled = 0;
|
||||||
|
for (IrisNoiseGenerator child : fracture) {
|
||||||
|
if (child.isEnabled()) {
|
||||||
|
children[compiled] = child.compileProgram(superSeed + seed + salt, data, engine);
|
||||||
|
offsets[compiled++] = child.getOpacity() / 2D;
|
||||||
}
|
}
|
||||||
salt += 819;
|
salt += 819;
|
||||||
}
|
}
|
||||||
CNG generator = getGenerator(superSeed, data, engine);
|
CNG generator = Objects.requireNonNull(getGenerator(superSeed, data, engine), "Noise layer generator");
|
||||||
return (x, z) -> samplePrepared(generator, children, offsets, x, z);
|
return NoiseLayerProgram.of(this, NoiseProgram.compile2D(generator), children, offsets);
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public KList<IrisNoiseGenerator> getAllComposites() {
|
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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,7 +18,21 @@
|
|||||||
|
|
||||||
package art.arcane.iris.generation.runtime;
|
package art.arcane.iris.generation.runtime;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.bake.BakePlane;
|
||||||
|
import art.arcane.iris.generation.bake.BakeRegistry;
|
||||||
|
import art.arcane.iris.generation.bake.ChunkBakeCache;
|
||||||
|
import art.arcane.iris.generation.bake.ChunkPlanes;
|
||||||
|
import art.arcane.iris.generation.bake.ColumnBake;
|
||||||
|
import art.arcane.iris.generation.bake.GeneratorEvaluations;
|
||||||
|
import art.arcane.iris.generation.bake.PlaneSource;
|
||||||
|
import art.arcane.iris.generation.bake.TerrainProgram;
|
||||||
|
import art.arcane.iris.generation.noise.TerrainBatchScratch;
|
||||||
|
import art.arcane.iris.generation.simd.SimdSupport;
|
||||||
|
import art.arcane.iris.generation.stream.BakedDoubleStream;
|
||||||
|
import art.arcane.iris.generation.stream.BakedHeightStream;
|
||||||
|
import art.arcane.iris.generation.stream.BakedStream;
|
||||||
import art.arcane.iris.generation.stream.GenerationStreams;
|
import art.arcane.iris.generation.stream.GenerationStreams;
|
||||||
|
import art.arcane.iris.generation.stream.RarityStream;
|
||||||
import art.arcane.iris.generation.stream.CachedDoubleStream2D;
|
import art.arcane.iris.generation.stream.CachedDoubleStream2D;
|
||||||
import art.arcane.iris.generation.stream.CachedStream2D;
|
import art.arcane.iris.generation.stream.CachedStream2D;
|
||||||
import art.arcane.iris.generation.stream.ProvisionalSampling;
|
import art.arcane.iris.generation.stream.ProvisionalSampling;
|
||||||
@@ -118,14 +132,16 @@ import java.nio.file.Path;
|
|||||||
import java.util.function.BiFunction;
|
import java.util.function.BiFunction;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "hydrologyRuntime", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyBanks3D"})
|
@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "terrainProgram", "bakeCache", "biomeRegistry", "regionRegistry", "blockRegistry", "regionSource", "baseBiomeSource", "caveSource", "rockSource", "fluidSource", "childStyleSeed", "inferredByType", "naturalHeightSource", "naturalTrueBiomeSource", "trueBiomeSource", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "hydrologyRuntime", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyBanks3D"})
|
||||||
@ToString(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "hydrologyRuntime", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyBanks3D"})
|
@ToString(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "terrainProgram", "bakeCache", "biomeRegistry", "regionRegistry", "blockRegistry", "regionSource", "baseBiomeSource", "caveSource", "rockSource", "fluidSource", "childStyleSeed", "inferredByType", "naturalHeightSource", "naturalTrueBiomeSource", "trueBiomeSource", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "hydrologyRuntime", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyBanks3D"})
|
||||||
public class IrisComplex implements DataProvider {
|
public class IrisComplex implements DataProvider {
|
||||||
private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D);
|
private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D);
|
||||||
private static final AtomicLong lastBoundsFailureLog = new AtomicLong(0L);
|
private static final AtomicLong lastBoundsFailureLog = new AtomicLong(0L);
|
||||||
private static final int GRID_BOUNDS_CACHE_SIZE = 8192;
|
private static final int GRID_BOUNDS_CACHE_SIZE = 8192;
|
||||||
private static final int STUDIO_NOISE_CACHE_SIZE = 32_768;
|
private static final int STUDIO_NOISE_CACHE_SIZE = 32_768;
|
||||||
private static final int TERRAIN_COLUMN_CACHE_SIZE = 524_288;
|
private static final int TERRAIN_COLUMN_CACHE_SIZE = 524_288;
|
||||||
|
private static final int BAKE_CHUNKS_PER_NOISE_CHUNK = 8;
|
||||||
|
private static final int HYDROLOGY_BAKE_BUDGET_SHARES = 2;
|
||||||
/** One million corners: about 20 MB, roughly a 4000 by 4000 block area at the 4-block grid. */
|
/** One million corners: about 20 MB, roughly a 4000 by 4000 block area at the 4-block grid. */
|
||||||
private static final int SHARED_CORNER_BOUNDS_CAPACITY = 1 << 20;
|
private static final int SHARED_CORNER_BOUNDS_CAPACITY = 1 << 20;
|
||||||
/** The slope streams measure the rise across a run of this many blocks, so gradient is slope over run. */
|
/** The slope streams measure the rise across a run of this many blocks, so gradient is slope over run. */
|
||||||
@@ -141,6 +157,7 @@ public class IrisComplex implements DataProvider {
|
|||||||
IrisGenerator::getLoadKey,
|
IrisGenerator::getLoadKey,
|
||||||
Comparator.nullsFirst(Comparator.naturalOrder())
|
Comparator.nullsFirst(Comparator.naturalOrder())
|
||||||
);
|
);
|
||||||
|
private static final ThreadLocal<TerrainBatchScratch> BATCH_SCRATCH = ThreadLocal.withInitial(TerrainBatchScratch::new);
|
||||||
private static final InferredType[] INFERRED_BIOME_PREPARATION_ORDER = {
|
private static final InferredType[] INFERRED_BIOME_PREPARATION_ORDER = {
|
||||||
InferredType.LAND,
|
InferredType.LAND,
|
||||||
InferredType.CAVE,
|
InferredType.CAVE,
|
||||||
@@ -153,6 +170,37 @@ public class IrisComplex implements DataProvider {
|
|||||||
private final int biomeBoundsSamplingStep;
|
private final int biomeBoundsSamplingStep;
|
||||||
private transient volatile SharedCornerBounds sharedCornerBounds;
|
private transient volatile SharedCornerBounds sharedCornerBounds;
|
||||||
@Getter(AccessLevel.NONE)
|
@Getter(AccessLevel.NONE)
|
||||||
|
@Setter(AccessLevel.NONE)
|
||||||
|
private transient volatile TerrainProgram terrainProgram;
|
||||||
|
@Getter(AccessLevel.NONE)
|
||||||
|
private final transient ChunkBakeCache bakeCache;
|
||||||
|
@Getter(AccessLevel.NONE)
|
||||||
|
private final transient BakeRegistry<IrisBiome> biomeRegistry = new BakeRegistry<>();
|
||||||
|
@Getter(AccessLevel.NONE)
|
||||||
|
private final transient BakeRegistry<IrisRegion> regionRegistry = new BakeRegistry<>();
|
||||||
|
@Getter(AccessLevel.NONE)
|
||||||
|
private final transient BakeRegistry<NativeBlockState> blockRegistry = new BakeRegistry<>();
|
||||||
|
@Getter(AccessLevel.NONE)
|
||||||
|
private final transient long childStyleSeed;
|
||||||
|
@Getter(AccessLevel.NONE)
|
||||||
|
private final transient Map<InferredType, ProceduralStream<IrisBiome>> inferredByType;
|
||||||
|
@Getter(AccessLevel.NONE)
|
||||||
|
private final transient ProceduralStream<IrisRegion> regionSource;
|
||||||
|
@Getter(AccessLevel.NONE)
|
||||||
|
private final transient ProceduralStream<IrisBiome> baseBiomeSource;
|
||||||
|
@Getter(AccessLevel.NONE)
|
||||||
|
private final transient ProceduralStream<IrisBiome> caveSource;
|
||||||
|
@Getter(AccessLevel.NONE)
|
||||||
|
private final transient ProceduralStream<NativeBlockState> rockSource;
|
||||||
|
@Getter(AccessLevel.NONE)
|
||||||
|
private final transient ProceduralStream<NativeBlockState> fluidSource;
|
||||||
|
@Getter(AccessLevel.NONE)
|
||||||
|
private final transient ProceduralStream<Double> naturalHeightSource;
|
||||||
|
@Getter(AccessLevel.NONE)
|
||||||
|
private final transient ProceduralStream<IrisBiome> naturalTrueBiomeSource;
|
||||||
|
@Getter(AccessLevel.NONE)
|
||||||
|
private final transient ProceduralStream<IrisBiome> trueBiomeSource;
|
||||||
|
@Getter(AccessLevel.NONE)
|
||||||
private final transient IrisInterpolator[] frozenInterpolators;
|
private final transient IrisInterpolator[] frozenInterpolators;
|
||||||
@Getter(AccessLevel.NONE)
|
@Getter(AccessLevel.NONE)
|
||||||
private final transient IrisGenerator[][] frozenGenerators;
|
private final transient IrisGenerator[][] frozenGenerators;
|
||||||
@@ -170,6 +218,9 @@ public class IrisComplex implements DataProvider {
|
|||||||
@Getter(AccessLevel.NONE)
|
@Getter(AccessLevel.NONE)
|
||||||
@Setter(AccessLevel.NONE)
|
@Setter(AccessLevel.NONE)
|
||||||
private transient boolean hydrologyNoiseCachesClosed;
|
private transient boolean hydrologyNoiseCachesClosed;
|
||||||
|
@Getter(AccessLevel.NONE)
|
||||||
|
@Setter(AccessLevel.NONE)
|
||||||
|
private transient HydrologyNoiseCacheBudget.Reservation hydrologyBakeReservation;
|
||||||
private RNG rng;
|
private RNG rng;
|
||||||
private double fluidHeight;
|
private double fluidHeight;
|
||||||
private IrisData data;
|
private IrisData data;
|
||||||
@@ -213,9 +264,6 @@ public class IrisComplex implements DataProvider {
|
|||||||
private ProceduralStream<Double> unblendedNaturalHeightStream;
|
private ProceduralStream<Double> unblendedNaturalHeightStream;
|
||||||
private final TransitionDisplacementField transitionDisplacement;
|
private final TransitionDisplacementField transitionDisplacement;
|
||||||
private ProceduralStream<Double> heightStream;
|
private ProceduralStream<Double> heightStream;
|
||||||
@Getter(AccessLevel.NONE)
|
|
||||||
@Setter(AccessLevel.NONE)
|
|
||||||
private CachedDoubleStream2D cachedHeightStream;
|
|
||||||
private ProceduralStream<Integer> roundedHeighteightStream;
|
private ProceduralStream<Integer> roundedHeighteightStream;
|
||||||
private ProceduralStream<Double> maxHeightStream;
|
private ProceduralStream<Double> maxHeightStream;
|
||||||
private ProceduralStream<Double> overlayStream;
|
private ProceduralStream<Double> overlayStream;
|
||||||
@@ -279,6 +327,7 @@ public class IrisComplex implements DataProvider {
|
|||||||
IrisBiome emptyBiome = new IrisBiome().setInferredType(InferredType.CAVE);
|
IrisBiome emptyBiome = new IrisBiome().setInferredType(InferredType.CAVE);
|
||||||
UUID focusUUID = UUID.nameUUIDFromBytes("focus".getBytes());
|
UUID focusUUID = UUID.nameUUIDFromBytes("focus".getBytes());
|
||||||
this.rng = new RNG(engine.getSeedManager().getComplex());
|
this.rng = new RNG(engine.getSeedManager().getComplex());
|
||||||
|
childStyleSeed = IrisBiome.childStyleSeed(rng, 123);
|
||||||
this.data = engine.getData();
|
this.data = engine.getData();
|
||||||
imageMapRuntime = IrisImageMapRuntime.compile(engine);
|
imageMapRuntime = IrisImageMapRuntime.compile(engine);
|
||||||
double height = engine.getMaxHeight();
|
double height = engine.getMaxHeight();
|
||||||
@@ -292,7 +341,8 @@ public class IrisComplex implements DataProvider {
|
|||||||
boolean buffetActive = biomeBuffet != null && !biomeBuffet.cells().isEmpty();
|
boolean buffetActive = biomeBuffet != null && !biomeBuffet.cells().isEmpty();
|
||||||
focusBiome = buffetActive ? null : compatUsable(engine.getFocus());
|
focusBiome = buffetActive ? null : compatUsable(engine.getFocus());
|
||||||
focusRegion = buffetActive ? null : compatUsable(engine.getFocusRegion());
|
focusRegion = buffetActive ? null : compatUsable(engine.getFocusRegion());
|
||||||
Map<InferredType, ProceduralStream<IrisBiome>> inferredStreams = new HashMap<>();
|
Map<InferredType, ProceduralStream<IrisBiome>> inferredStreams = new EnumMap<>(InferredType.class);
|
||||||
|
inferredByType = inferredStreams;
|
||||||
KList<IrisRegion> preparedRegions = new KList<>();
|
KList<IrisRegion> preparedRegions = new KList<>();
|
||||||
|
|
||||||
if (focusBiome != null) {
|
if (focusBiome != null) {
|
||||||
@@ -365,54 +415,59 @@ public class IrisComplex implements DataProvider {
|
|||||||
|
|
||||||
return value;
|
return value;
|
||||||
});
|
});
|
||||||
rockStream = engine.getDimension().getRockPalette().getLayerGenerator(rng.nextParallelRNG(45), data).stream()
|
bakeCache = new ChunkBakeCache(new BakeSource(), cacheSize * BAKE_CHUNKS_PER_NOISE_CHUNK);
|
||||||
|
rockSource = engine.getDimension().getRockPalette().getLayerGenerator(rng.nextParallelRNG(45), data).stream()
|
||||||
.select(engine.getDimension().getRockPalette().getBlockData(data));
|
.select(engine.getDimension().getRockPalette().getBlockData(data));
|
||||||
fluidStream = engine.getDimension().getFluidPalette().getLayerGenerator(rng.nextParallelRNG(78), data).stream()
|
rockStream = new BakedStream<>(bakeCache, BakePlane.ROCK, rockSource);
|
||||||
|
fluidSource = engine.getDimension().getFluidPalette().getLayerGenerator(rng.nextParallelRNG(78), data).stream()
|
||||||
.select(engine.getDimension().getFluidPalette().getBlockData(data));
|
.select(engine.getDimension().getFluidPalette().getBlockData(data));
|
||||||
|
fluidStream = new BakedStream<>(bakeCache, BakePlane.FLUID, fluidSource);
|
||||||
hydrologyFluidStreams = createHydrologyFluidStreams(engine.getDimension().getHydrology());
|
hydrologyFluidStreams = createHydrologyFluidStreams(engine.getDimension().getHydrology());
|
||||||
regionStyleStream = engine.getDimension().getRegionStyle().create(rng.nextParallelRNG(883), getData()).stream()
|
regionStyleStream = engine.getDimension().getRegionStyle().create(rng.nextParallelRNG(883), getData()).stream()
|
||||||
.zoom(engine.getDimension().getRegionZoom());
|
.zoom(engine.getDimension().getRegionZoom());
|
||||||
regionIdentityStream = regionStyleStream.fit(Integer.MIN_VALUE, Integer.MAX_VALUE);
|
regionIdentityStream = regionStyleStream.fit(Integer.MIN_VALUE, Integer.MAX_VALUE);
|
||||||
regionStream = focusedRegions != null ? focusedRegions
|
regionSource = focusedRegions != null ? focusedRegions
|
||||||
: GenerationStreams.cache2D(RegionSelection.select(new RegionSelection.Options(
|
: RegionSelection.select(new RegionSelection.Options(
|
||||||
engine.getDimension(), data, regionStyleStream, imageMapRuntime, null)),
|
engine.getDimension(), data, regionStyleStream, imageMapRuntime, null));
|
||||||
"regionStream", engine, cacheSize);
|
regionStream = focusedRegions != null ? focusedRegions : new BakedStream<>(bakeCache, BakePlane.REGION, regionSource);
|
||||||
caveBiomeStream = GenerationStreams.cache2D(GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z))
|
caveSource = GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z))
|
||||||
.convert((r) -> createInferredBiomeStream(r, InferredType.CAVE))
|
.convert((r) -> createInferredBiomeStream(r, InferredType.CAVE))
|
||||||
.convertAware2D(ProceduralStream::get), "caveBiomeStream", engine, cacheSize);
|
.convertAware2D(ProceduralStream::get);
|
||||||
|
caveBiomeStream = new BakedStream<>(bakeCache, BakePlane.CAVE_BIOME, caveSource);
|
||||||
inferredStreams.put(InferredType.CAVE, caveBiomeStream);
|
inferredStreams.put(InferredType.CAVE, caveBiomeStream);
|
||||||
landBiomeStream = GenerationStreams.cache2D(GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z))
|
landBiomeStream = GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z))
|
||||||
.convert((r) -> createInferredBiomeStream(r, InferredType.LAND))
|
.convert((r) -> createInferredBiomeStream(r, InferredType.LAND))
|
||||||
.convertAware2D(ProceduralStream::get), "landBiomeStream", engine, cacheSize);
|
.convertAware2D(ProceduralStream::get);
|
||||||
inferredStreams.put(InferredType.LAND, landBiomeStream);
|
inferredStreams.put(InferredType.LAND, landBiomeStream);
|
||||||
seaBiomeStream = GenerationStreams.cache2D(GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z))
|
seaBiomeStream = GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z))
|
||||||
.convert((r) -> createInferredBiomeStream(r, InferredType.SEA))
|
.convert((r) -> createInferredBiomeStream(r, InferredType.SEA))
|
||||||
.convertAware2D(ProceduralStream::get), "seaBiomeStream", engine, cacheSize);
|
.convertAware2D(ProceduralStream::get);
|
||||||
inferredStreams.put(InferredType.SEA, seaBiomeStream);
|
inferredStreams.put(InferredType.SEA, seaBiomeStream);
|
||||||
shoreBiomeStream = GenerationStreams.cache2D(GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z))
|
shoreBiomeStream = GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z))
|
||||||
.convert((r) -> createInferredBiomeStream(r, InferredType.SHORE))
|
.convert((r) -> createInferredBiomeStream(r, InferredType.SHORE))
|
||||||
.convertAware2D(ProceduralStream::get), "shoreBiomeStream", engine, cacheSize);
|
.convertAware2D(ProceduralStream::get);
|
||||||
inferredStreams.put(InferredType.SHORE, shoreBiomeStream);
|
inferredStreams.put(InferredType.SHORE, shoreBiomeStream);
|
||||||
bridgeStream = focusedBiomes != null ? focusedBiomes.convert(IrisBiome::getInferredType) :
|
bridgeStream = focusedBiomes != null ? focusedBiomes.convert(IrisBiome::getInferredType) :
|
||||||
GenerationStreams.cache2D(ContinentGenerator.create(engine.getDimension(), rng, getData()).stream()
|
ContinentGenerator.create(engine.getDimension(), rng, getData()).stream()
|
||||||
.convert((v) -> v >= engine.getDimension().getLandChance() ? InferredType.SEA : InferredType.LAND), "bridgeStream", engine, cacheSize);
|
.convert((v) -> v >= engine.getDimension().getLandChance() ? InferredType.SEA : InferredType.LAND);
|
||||||
ProceduralStream<IrisBiome> proceduralBaseBiomeStream = focusedBiomes != null ? focusedBiomes :
|
ProceduralStream<IrisBiome> proceduralBaseBiomeStream = focusedBiomes != null ? focusedBiomes
|
||||||
GenerationStreams.cache2D(bridgeStream.convertAware2D((t, x, z) -> inferredStreams.get(t).get(x, z))
|
: ProceduralStream.of(this::proceduralBaseBiome,
|
||||||
.convertAware2D(this::implode), "baseBiomeStream", engine, cacheSize);
|
(x, y, z) -> implode(inferredByType.get(bridgeStream.get(x, y, z)).get(x, z), x, z),
|
||||||
baseBiomeStream = focusedBiomes != null || !imageMapRuntime.has(IrisImageMapApplication.BIOME)
|
Interpolated.of(value -> 0D, value -> null));
|
||||||
? proceduralBaseBiomeStream : GenerationStreams.cache2D(ProceduralStream.of((x, z) -> {
|
baseBiomeSource = focusedBiomes != null || !imageMapRuntime.has(IrisImageMapApplication.BIOME)
|
||||||
|
? proceduralBaseBiomeStream : ProceduralStream.of((x, z) -> {
|
||||||
IrisBiome mapped = imageMapRuntime.sampleBiome(x, z);
|
IrisBiome mapped = imageMapRuntime.sampleBiome(x, z);
|
||||||
return mapped == null ? proceduralBaseBiomeStream.get(x, z) : mapped;
|
return mapped == null ? proceduralBaseBiomeStream.get(x, z) : mapped;
|
||||||
}, (x, y, z) -> {
|
}, (x, y, z) -> {
|
||||||
IrisBiome mapped = imageMapRuntime.sampleBiome(x, z);
|
IrisBiome mapped = imageMapRuntime.sampleBiome(x, z);
|
||||||
return mapped == null ? proceduralBaseBiomeStream.get(x, y, z) : mapped;
|
return mapped == null ? proceduralBaseBiomeStream.get(x, y, z) : mapped;
|
||||||
}, proceduralBaseBiomeStream), "imageMappedBaseBiomeStream", engine, cacheSize);
|
}, proceduralBaseBiomeStream);
|
||||||
|
baseBiomeStream = focusedBiomes != null ? focusedBiomes : new BakedStream<>(bakeCache, BakePlane.BASE_BIOME, baseBiomeSource);
|
||||||
proceduralTerrainHeight = new ProceduralTerrainHeightSampler(
|
proceduralTerrainHeight = new ProceduralTerrainHeightSampler(
|
||||||
(x, z) -> getHeight(engine, x, z, engine.getSeedManager().getHeight()),
|
(x, z) -> getHeight(engine, x, z, engine.getSeedManager().getHeight()),
|
||||||
engine.getDimension().getTerrainSamplingStep());
|
engine.getDimension().getTerrainSamplingStep());
|
||||||
baseTerrainHeightStream = GenerationStreams.cache2DDouble(
|
baseTerrainHeightStream = new BakedDoubleStream(bakeCache, BakePlane.BASE_HEIGHT,
|
||||||
ProceduralStream.ofDouble(this::sampleUnblendedNaturalTerrainHeight),
|
ProceduralStream.ofDouble(this::sampleUnblendedNaturalTerrainHeight));
|
||||||
"baseTerrainHeightStream", engine, cacheSize);
|
|
||||||
boolean terrain3DEnabled = false;
|
boolean terrain3DEnabled = false;
|
||||||
for (IrisBiome biome : generatorBiomes) {
|
for (IrisBiome biome : generatorBiomes) {
|
||||||
if (biome.getTerrain3D() != null) {
|
if (biome.getTerrain3D() != null) {
|
||||||
@@ -427,18 +482,20 @@ public class IrisComplex implements DataProvider {
|
|||||||
new Terrain3DRuntime.Options(engine.getSeedManager().getTerrain(), engine.getHeight(),
|
new Terrain3DRuntime.Options(engine.getSeedManager().getTerrain(), engine.getHeight(),
|
||||||
fluidHeight, data, terrain3DEnabled, TERRAIN_COLUMN_CACHE_SIZE));
|
fluidHeight, data, terrain3DEnabled, TERRAIN_COLUMN_CACHE_SIZE));
|
||||||
unblendedNaturalHeightStream = terrain3DEnabled
|
unblendedNaturalHeightStream = terrain3DEnabled
|
||||||
? GenerationStreams.cache2DDouble(ProceduralStream.ofDouble(this::sampleTerrain3DHeight), "unblendedNaturalHeightStream", engine, cacheSize)
|
? ProceduralStream.ofDouble(this::sampleTerrain3DHeight)
|
||||||
: baseTerrainHeightStream;
|
: baseTerrainHeightStream;
|
||||||
transitionDisplacement = transitionGenerationPlan == null ? null : new TransitionDisplacementField(
|
transitionDisplacement = transitionGenerationPlan == null ? null : new TransitionDisplacementField(
|
||||||
transitionGenerationPlan, new TransitionDisplacementField.Sources(
|
transitionGenerationPlan, new TransitionDisplacementField.Sources(
|
||||||
(x, z) -> unblendedNaturalHeightStream.getDouble(x, z), engine.getHeight()));
|
(x, z) -> unblendedNaturalHeightStream.getDouble(x, z), engine.getHeight()));
|
||||||
naturalHeightStream = transitionDisplacement == null ? unblendedNaturalHeightStream
|
naturalHeightSource = transitionDisplacement == null
|
||||||
: GenerationStreams.cache2DDouble(ProceduralStream.ofDouble((x, z) -> transitionDisplacement.height(
|
? unblendedNaturalHeightStream
|
||||||
blockCoordinate(x), blockCoordinate(z), unblendedNaturalHeightStream.getDouble(x, z))),
|
: ProceduralStream.ofDouble((x, z) -> transitionDisplacement.height(
|
||||||
"naturalHeightStream", engine, cacheSize);
|
blockCoordinate(x), blockCoordinate(z), unblendedNaturalHeightStream.getDouble(x, z)));
|
||||||
naturalTrueBiomeStream = focusedBiomes != null ? GenerationStreams.cache2D(focusedBiomes, "naturalTrueBiomeStream-focus", engine, cacheSize)
|
naturalHeightStream = new BakedDoubleStream(bakeCache, BakePlane.NATURAL_HEIGHT, naturalHeightSource);
|
||||||
: GenerationStreams.cache2D(ProceduralStream.of(this::sampleNaturalTrueBiome, this::sampleNaturalTrueBiome,
|
naturalTrueBiomeSource = focusedBiomes != null ? focusedBiomes
|
||||||
Interpolated.of(value -> 0D, value -> null)), "naturalTrueBiomeStream", engine, cacheSize);
|
: ProceduralStream.of(this::sampleNaturalTrueBiome, this::sampleNaturalTrueBiome,
|
||||||
|
Interpolated.of(value -> 0D, value -> null));
|
||||||
|
naturalTrueBiomeStream = new BakedStream<>(bakeCache, BakePlane.NATURAL_TRUE_BIOME, naturalTrueBiomeSource);
|
||||||
IrisHydrology configuredHydrology = engine.getDimension().getHydrology();
|
IrisHydrology configuredHydrology = engine.getDimension().getHydrology();
|
||||||
if (hydrologyActive(configuredHydrology)) {
|
if (hydrologyActive(configuredHydrology)) {
|
||||||
hydrologyRuntime = new IrisHydrologyRuntime(new IrisHydrologyRuntimeContext(
|
hydrologyRuntime = new IrisHydrologyRuntime(new IrisHydrologyRuntimeContext(
|
||||||
@@ -467,8 +524,8 @@ public class IrisComplex implements DataProvider {
|
|||||||
Math.max(4096, cacheSize), data, banks3D));
|
Math.max(4096, cacheSize), data, banks3D));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cachedHeightStream = new TerrainHeightStream(engine, cacheSize);
|
heightStream = new BakedHeightStream(bakeCache, () -> terrainEngine.getPlatformHooks().isMainThread(),
|
||||||
heightStream = cachedHeightStream;
|
this::nonblockingTerrainHeight, this::resolveHydrologyTerrainHeight);
|
||||||
roundedHeighteightStream = GenerationStreams.contextInjecting(heightStream, engine, (c, x, z) -> c.getHeight().getDouble(x, z))
|
roundedHeighteightStream = GenerationStreams.contextInjecting(heightStream, engine, (c, x, z) -> c.getHeight().getDouble(x, z))
|
||||||
.round();
|
.round();
|
||||||
slopeStream = GenerationStreams.contextInjecting(heightStream, engine, (c, x, z) -> c.getHeight().getDouble(x, z))
|
slopeStream = GenerationStreams.contextInjecting(heightStream, engine, (c, x, z) -> c.getHeight().getDouble(x, z))
|
||||||
@@ -476,8 +533,9 @@ public class IrisComplex implements DataProvider {
|
|||||||
naturalSlopeStream = GenerationStreams.cache2DDouble(naturalHeightStream.slope(3), "naturalSlopeStream", engine, cacheSize);
|
naturalSlopeStream = GenerationStreams.cache2DDouble(naturalHeightStream.slope(3), "naturalSlopeStream", engine, cacheSize);
|
||||||
// Terrain3D column heights resolve the surface biome, so the shore band reads the pre-terrain3D slope or it recurses.
|
// Terrain3D column heights resolve the surface biome, so the shore band reads the pre-terrain3D slope or it recurses.
|
||||||
shoreSlopeStream = terrain3DEnabled ? baseTerrainHeightStream.slope(3) : naturalSlopeStream;
|
shoreSlopeStream = terrain3DEnabled ? baseTerrainHeightStream.slope(3) : naturalSlopeStream;
|
||||||
trueBiomeStream = focusedBiomes != null ? GenerationStreams.cache2D(focusedBiomes, "trueBiomeStream-focus", engine, cacheSize) : GenerationStreams.cache2D(heightStream
|
trueBiomeSource = focusedBiomes != null ? focusedBiomes
|
||||||
.convertAware2D((terrainHeight, x, z) -> resolveHydrologySurfaceBiome(terrainHeight, x, z)), "trueBiomeStream", engine, cacheSize);
|
: heightStream.convertAware2D((terrainHeight, x, z) -> resolveHydrologySurfaceBiome(terrainHeight, x, z));
|
||||||
|
trueBiomeStream = new BakedStream<>(bakeCache, BakePlane.TRUE_BIOME, trueBiomeSource);
|
||||||
riverDistanceStream = GenerationStreams.cache2DDouble(ProceduralStream.ofDouble(this::resolveHydrologyDistance), "riverDistanceStream", engine, cacheSize);
|
riverDistanceStream = GenerationStreams.cache2DDouble(ProceduralStream.ofDouble(this::resolveHydrologyDistance), "riverDistanceStream", engine, cacheSize);
|
||||||
riverFlowStream = GenerationStreams.cache2DDouble(ProceduralStream.ofDouble(this::resolveHydrologyFlow), "riverFlowStream", engine, cacheSize);
|
riverFlowStream = GenerationStreams.cache2DDouble(ProceduralStream.ofDouble(this::resolveHydrologyFlow), "riverFlowStream", engine, cacheSize);
|
||||||
riverCarveWeightStream = GenerationStreams.cache2DDouble(ProceduralStream.ofDouble(this::resolveHydrologyCarveWeight), "riverCarveWeightStream", engine, cacheSize);
|
riverCarveWeightStream = GenerationStreams.cache2DDouble(ProceduralStream.ofDouble(this::resolveHydrologyCarveWeight), "riverCarveWeightStream", engine, cacheSize);
|
||||||
@@ -518,17 +576,11 @@ public class IrisComplex implements DataProvider {
|
|||||||
return engine.isStudio() && !detached ? Math.max(configuredSize, STUDIO_NOISE_CACHE_SIZE) : configuredSize;
|
return engine.isStudio() && !detached ? Math.max(configuredSize, STUDIO_NOISE_CACHE_SIZE) : configuredSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void ensureTerrainNoiseCacheSize(int minimumChunks) {
|
public void ensureTerrainNoiseCacheSize(int minimumChunks) {
|
||||||
if (minimumChunks <= 0) {
|
if (minimumChunks <= 0) {
|
||||||
throw new IllegalArgumentException("Terrain noise cache size must be positive");
|
throw new IllegalArgumentException("Terrain noise cache size must be positive");
|
||||||
}
|
}
|
||||||
CachedDoubleStream2D naturalCache = (CachedDoubleStream2D) naturalHeightStream;
|
bakeCache.ensureCapacity(minimumChunks * BAKE_CHUNKS_PER_NOISE_CHUNK);
|
||||||
if (naturalCache.getMaxSize() < minimumChunks * 256L) {
|
|
||||||
naturalCache.setMaximumChunks(minimumChunks);
|
|
||||||
}
|
|
||||||
if (cachedHeightStream.getMaxSize() < minimumChunks * 256L) {
|
|
||||||
cachedHeightStream.setMaximumChunks(minimumChunks);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void expandHydrologyNoiseCaches() {
|
void expandHydrologyNoiseCaches() {
|
||||||
@@ -578,6 +630,27 @@ public class IrisComplex implements DataProvider {
|
|||||||
hydrologyNoiseCaches = List.of();
|
hydrologyNoiseCaches = List.of();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
synchronized void reserveHydrologyBake(HydrologyNoiseCacheBudget budget) {
|
||||||
|
if (detached || hydrologyRuntime == null || hydrologyNoiseCachesClosed || hydrologyBakeReservation != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int capacity = bakeCache.capacity();
|
||||||
|
int requested = HydrologyNoiseCacheBudget.MAXIMUM_CHUNKS - capacity;
|
||||||
|
if (requested <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
hydrologyBakeReservation = budget.reserve(HYDROLOGY_BAKE_BUDGET_SHARES, requested);
|
||||||
|
bakeCache.ensureCapacity(capacity + hydrologyBakeReservation.additionalChunks());
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void releaseHydrologyBake() {
|
||||||
|
if (hydrologyBakeReservation == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
hydrologyBakeReservation.close();
|
||||||
|
hydrologyBakeReservation = null;
|
||||||
|
}
|
||||||
|
|
||||||
void enablePreparedHydrologyCache(String runtimeIdentity, Path persistentRoot) {
|
void enablePreparedHydrologyCache(String runtimeIdentity, Path persistentRoot) {
|
||||||
if (hydrologyRuntime != null) {
|
if (hydrologyRuntime != null) {
|
||||||
hydrologyRuntime.enableSharedCache(runtimeIdentity, persistentRoot);
|
hydrologyRuntime.enableSharedCache(runtimeIdentity, persistentRoot);
|
||||||
@@ -787,106 +860,261 @@ public class IrisComplex implements DataProvider {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sampleBaseHeightBatch(long[] coordinates, int count, double[] target) {
|
|
||||||
((CachedDoubleStream2D) baseTerrainHeightStream).getBatch(coordinates, count, target, this::resolveBaseHeightBatch);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void resolveBaseHeightBatch(long[] missing, int size, double[] values) {
|
private void resolveBaseHeightBatch(long[] missing, int size, double[] values) {
|
||||||
double[] xs = new double[size];
|
TerrainBatchScratch scratch = BATCH_SCRATCH.get();
|
||||||
double[] zs = new double[size];
|
int mark = scratch.mark();
|
||||||
double[] lows = new double[size];
|
int indexMark = scratch.indexMark();
|
||||||
double[] highs = new double[size];
|
try {
|
||||||
double[] activeXs = new double[size];
|
double[] xs = scratch.buffer(size);
|
||||||
double[] activeZs = new double[size];
|
double[] zs = scratch.buffer(size);
|
||||||
double[] noise = new double[size];
|
double[] lows = scratch.buffer(size);
|
||||||
double[] groupHeights = new double[size];
|
double[] highs = scratch.buffer(size);
|
||||||
int[] active = new int[size];
|
Arrays.fill(values, 0, size, 0D);
|
||||||
for (int index = 0; index < size; index++) {
|
for (int index = 0; index < size; index++) {
|
||||||
xs[index] = RiverFootprint.unpackX(missing[index]);
|
xs[index] = RiverFootprint.unpackX(missing[index]);
|
||||||
zs[index] = RiverFootprint.unpackZ(missing[index]);
|
zs[index] = RiverFootprint.unpackZ(missing[index]);
|
||||||
}
|
}
|
||||||
long seed = terrainEngine.getSeedManager().getHeight() + 239945;
|
TerrainProgram program = terrainProgram();
|
||||||
|
long evaluations = 0L;
|
||||||
for (int group = 0; group < frozenInterpolators.length; group++) {
|
for (int group = 0; group < frozenInterpolators.length; group++) {
|
||||||
IrisGenerator[] generators = frozenGenerators[group];
|
if (frozenGenerators[group].length == 0) {
|
||||||
if (generators.length == 0) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
int activeCount = 0;
|
|
||||||
Arrays.fill(groupHeights, 0D);
|
|
||||||
for (int index = 0; index < size; index++) {
|
for (int index = 0; index < size; index++) {
|
||||||
NoiseBounds bounds = gridSampleBounds(terrainEngine, frozenInterpolators[group], group,
|
NoiseBounds bounds = gridSampleBounds(terrainEngine, frozenInterpolators[group], group,
|
||||||
generators, xs[index], zs[index]);
|
frozenGenerators[group], xs[index], zs[index]);
|
||||||
lows[index] = bounds.min();
|
lows[index] = bounds.min();
|
||||||
highs[index] = bounds.max();
|
highs[index] = bounds.max();
|
||||||
if (!Double.isFinite(lows[index]) || !Double.isFinite(highs[index])) {
|
|
||||||
averageGeneratorHeights(generators, lows[index], highs[index], xs[index], zs[index], seed);
|
|
||||||
}
|
|
||||||
if (lows[index] != highs[index]) {
|
|
||||||
active[activeCount] = index;
|
|
||||||
activeXs[activeCount] = xs[index];
|
|
||||||
activeZs[activeCount++] = zs[index];
|
|
||||||
} else {
|
|
||||||
for (int generator = 0; generator < generators.length; generator++) {
|
|
||||||
groupHeights[index] += lows[index];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (IrisGenerator generator : generators) {
|
|
||||||
if (activeCount == 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
generator.getHeights(activeXs, activeZs, seed, noise, activeCount);
|
|
||||||
if (GenerationCounter.ENABLED) {
|
|
||||||
GenerationCounter.GENERATOR_EVALUATIONS.add(activeCount);
|
|
||||||
}
|
|
||||||
for (int sample = 0; sample < activeCount; sample++) {
|
|
||||||
int index = active[sample];
|
|
||||||
if (!Double.isFinite(noise[sample])) {
|
|
||||||
throw new IllegalStateException("Terrain generator noise was not finite at " + xs[index] + "," + zs[index]
|
|
||||||
+ " (generator=" + generator.getLoadKey() + ", noise=" + noise[sample]
|
|
||||||
+ ", bounds=" + lows[index] + ".." + highs[index] + ").");
|
|
||||||
}
|
|
||||||
groupHeights[index] += M.lerp(lows[index], highs[index], noise[sample]);
|
|
||||||
}
|
}
|
||||||
|
evaluations += program.accumulate(group, scratch, xs, zs, size, lows, highs, values);
|
||||||
}
|
}
|
||||||
|
GeneratorEvaluations.batched(evaluations);
|
||||||
for (int index = 0; index < size; index++) {
|
for (int index = 0; index < size; index++) {
|
||||||
values[index] += groupHeights[index] / generators.length;
|
values[index] = finishBaseHeight(xs[index], zs[index], values[index]);
|
||||||
}
|
}
|
||||||
}
|
} finally {
|
||||||
for (int index = 0; index < size; index++) {
|
scratch.reset(mark);
|
||||||
double height = Math.max(Math.min(values[index] + fluidHeight + overlayStream.get(xs[index], zs[index]),
|
scratch.resetIndices(indexMark);
|
||||||
terrainEngine.getHeight()), 0);
|
|
||||||
values[index] = imageMapRuntime.sampleTerrainHeight(xs[index], zs[index], height);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sampleUnblendedHeightBatch(long[] coordinates, int count, double[] target) {
|
private void bakeBaseHeights(int chunkX, int chunkZ, long[] missing, double[] values) {
|
||||||
if (terrain3D == null) {
|
int originX = chunkX << 4;
|
||||||
sampleBaseHeightBatch(coordinates, count, target);
|
int originZ = chunkZ << 4;
|
||||||
|
if (!supportsChunkHeightBatch()) {
|
||||||
|
for (int index = 0; index < 256; index++) {
|
||||||
|
if ((missing[index >>> 6] & (1L << (index & 63))) != 0L) {
|
||||||
|
values[index] = sampleUnblendedNaturalTerrainHeight(originX + (index & 15), originZ + (index >> 4));
|
||||||
|
}
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
((CachedDoubleStream2D) unblendedNaturalHeightStream).getBatch(coordinates, count, target, (missing, size, values) -> {
|
TerrainBatchScratch scratch = BATCH_SCRATCH.get();
|
||||||
long[] required = new long[size];
|
int mark = scratch.mark();
|
||||||
int requiredCount = 0;
|
int indexMark = scratch.indexMark();
|
||||||
for (int index = 0; index < size; index++) {
|
try {
|
||||||
if (terrain3D.requiresBaseHeight(RiverFootprint.unpackX(missing[index]), RiverFootprint.unpackZ(missing[index]))) {
|
double[] xs = scratch.buffer(256);
|
||||||
required[requiredCount++] = missing[index];
|
double[] zs = scratch.buffer(256);
|
||||||
|
double[] lows = scratch.buffer(256);
|
||||||
|
double[] highs = scratch.buffer(256);
|
||||||
|
double[] heights = scratch.buffer(256);
|
||||||
|
int[] columns = scratch.indices(256);
|
||||||
|
int count = 0;
|
||||||
|
for (int index = 0; index < 256; index++) {
|
||||||
|
if ((missing[index >>> 6] & (1L << (index & 63))) != 0L) {
|
||||||
|
xs[count] = originX + (index & 15);
|
||||||
|
zs[count] = originZ + (index >> 4);
|
||||||
|
heights[count] = 0D;
|
||||||
|
columns[count++] = index;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (count == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
TerrainProgram program = terrainProgram();
|
||||||
|
GridBoundsCache cache = gridBoundsCache.get();
|
||||||
|
int grid = biomeBoundsSamplingStep;
|
||||||
|
boolean corners = ChunkCorners.supports(grid);
|
||||||
|
long evaluations = 0L;
|
||||||
|
for (int group = 0; group < frozenInterpolators.length; group++) {
|
||||||
|
if (frozenGenerators[group].length == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (corners) {
|
||||||
|
if (!chunkCornerBounds(cache, group, grid, originX, originZ, xs, zs, count, lows, highs)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (int index = 0; index < count; index++) {
|
||||||
|
NoiseBounds bounds = gridSampleBounds(terrainEngine, frozenInterpolators[group], group,
|
||||||
|
frozenGenerators[group], xs[index], zs[index]);
|
||||||
|
lows[index] = bounds.min();
|
||||||
|
highs[index] = bounds.max();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
evaluations += program.accumulate(group, scratch, xs, zs, count, lows, highs, heights);
|
||||||
|
}
|
||||||
|
for (int index = 0; index < count; index++) {
|
||||||
|
values[columns[index]] = finishBaseHeight(xs[index], zs[index], heights[index]);
|
||||||
|
}
|
||||||
|
GeneratorEvaluations.bakedChunk(evaluations);
|
||||||
|
} finally {
|
||||||
|
scratch.reset(mark);
|
||||||
|
scratch.resetIndices(indexMark);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean chunkCornerBounds(GridBoundsCache cache, int group, int grid, int originX, int originZ,
|
||||||
|
double[] xs, double[] zs, int count, double[] lows, double[] highs) {
|
||||||
|
int cells = 16 / grid;
|
||||||
|
int stride = cells + 1;
|
||||||
|
long[] packed = new long[stride * stride];
|
||||||
|
boolean silent = true;
|
||||||
|
for (int row = 0; row < stride; row++) {
|
||||||
|
for (int column = 0; column < stride; column++) {
|
||||||
|
long corner = cornerBounds(cache, terrainEngine, frozenInterpolators[group], group,
|
||||||
|
frozenGenerators[group], originX + column * grid, originZ + row * grid);
|
||||||
|
packed[row * stride + column] = corner;
|
||||||
|
silent &= silentBounds(corner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (silent) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (int index = 0; index < count; index++) {
|
||||||
|
double x = xs[index];
|
||||||
|
double z = zs[index];
|
||||||
|
int gx = ((int) Math.floor(x)) & ~(grid - 1);
|
||||||
|
int gz = ((int) Math.floor(z)) & ~(grid - 1);
|
||||||
|
double fx = (x - gx) / grid;
|
||||||
|
double fz = (z - gz) / grid;
|
||||||
|
int cornerIndex = ((gz - originZ) / grid) * stride + (gx - originX) / grid;
|
||||||
|
long b00 = packed[cornerIndex];
|
||||||
|
if (fx == 0D && fz == 0D) {
|
||||||
|
lows[index] = boundsLow(b00);
|
||||||
|
highs[index] = boundsHigh(b00);
|
||||||
|
} else if (fz == 0D) {
|
||||||
|
long b10 = packed[cornerIndex + 1];
|
||||||
|
lows[index] = biLerp(boundsLow(b00), boundsLow(b10), boundsLow(b00), boundsLow(b10), fx, fz);
|
||||||
|
highs[index] = biLerp(boundsHigh(b00), boundsHigh(b10), boundsHigh(b00), boundsHigh(b10), fx, fz);
|
||||||
|
} else if (fx == 0D) {
|
||||||
|
long b01 = packed[cornerIndex + stride];
|
||||||
|
lows[index] = biLerp(boundsLow(b00), boundsLow(b00), boundsLow(b01), boundsLow(b01), fx, fz);
|
||||||
|
highs[index] = biLerp(boundsHigh(b00), boundsHigh(b00), boundsHigh(b01), boundsHigh(b01), fx, fz);
|
||||||
|
} else {
|
||||||
|
long b10 = packed[cornerIndex + 1];
|
||||||
|
long b01 = packed[cornerIndex + stride];
|
||||||
|
long b11 = packed[cornerIndex + stride + 1];
|
||||||
|
lows[index] = biLerp(boundsLow(b00), boundsLow(b10), boundsLow(b01), boundsLow(b11), fx, fz);
|
||||||
|
highs[index] = biLerp(boundsHigh(b00), boundsHigh(b10), boundsHigh(b01), boundsHigh(b11), fx, fz);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void prefetchBaseHeights(int chunkX, int chunkZ, long[] naturalMissing) {
|
||||||
|
ChunkPlanes.Plane base = bakeCache.plane(BakePlane.BASE_HEIGHT, chunkX, chunkZ);
|
||||||
|
int originX = chunkX << 4;
|
||||||
|
int originZ = chunkZ << 4;
|
||||||
|
long[] compute = new long[4];
|
||||||
|
boolean any = false;
|
||||||
|
for (int index = 0; index < 256; index++) {
|
||||||
|
if ((naturalMissing[index >>> 6] & (1L << (index & 63))) == 0L || base.ready(index)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int x = originX + (index & 15);
|
||||||
|
int z = originZ + (index >> 4);
|
||||||
|
if (terrain3D != null && !terrain3D.requiresBaseHeight(x, z)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
compute[index >>> 6] |= 1L << (index & 63);
|
||||||
|
any = true;
|
||||||
|
}
|
||||||
|
if (!any) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
double[] values = new double[256];
|
||||||
|
long revision = ProvisionalSampling.revision();
|
||||||
|
bakeBaseHeights(chunkX, chunkZ, compute, values);
|
||||||
|
if (revision != ProvisionalSampling.revision()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int index = 0; index < 256; index++) {
|
||||||
|
if ((compute[index >>> 6] & (1L << (index & 63))) != 0L) {
|
||||||
|
base.storeDouble(index, values[index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean supportsChunkHeightBatch() {
|
||||||
|
return terrainEngine.getDimension().getTerrainSamplingStep() == 1 && biomeBuffetGenerators.isEmpty()
|
||||||
|
&& (biomeBuffet == null || biomeBuffet.cells().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
private double finishBaseHeight(double x, double z, double interpolated) {
|
||||||
|
double height = Math.max(Math.min(interpolated + fluidHeight + overlayStream.get(x, z), terrainEngine.getHeight()), 0);
|
||||||
|
return imageMapRuntime.sampleTerrainHeight(x, z, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TerrainProgram terrainProgram() {
|
||||||
|
TerrainProgram program = terrainProgram;
|
||||||
|
if (program == null) {
|
||||||
|
synchronized (this) {
|
||||||
|
program = terrainProgram;
|
||||||
|
if (program == null) {
|
||||||
|
program = TerrainProgram.compile(frozenGenerators, terrainEngine.getSeedManager().getHeight() + 239945);
|
||||||
|
terrainProgram = program;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sampleNaturalBatch(long[] coordinates, int count, double[] heights) {
|
||||||
|
long[] required = new long[count];
|
||||||
|
int requiredCount = 0;
|
||||||
|
for (int index = 0; index < count; index++) {
|
||||||
|
int x = RiverFootprint.unpackX(coordinates[index]);
|
||||||
|
int z = RiverFootprint.unpackZ(coordinates[index]);
|
||||||
|
if (bakedColumn(BakePlane.NATURAL_HEIGHT, x, z)
|
||||||
|
|| terrain3D != null && !terrain3D.requiresBaseHeight(x, z)
|
||||||
|
|| bakedColumn(BakePlane.BASE_HEIGHT, x, z)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
required[requiredCount++] = coordinates[index];
|
||||||
|
}
|
||||||
if (requiredCount > 0) {
|
if (requiredCount > 0) {
|
||||||
sampleBaseHeightBatch(required, requiredCount, new double[requiredCount]);
|
double[] baseHeights = new double[requiredCount];
|
||||||
|
long revision = ProvisionalSampling.revision();
|
||||||
|
resolveBaseHeightBatch(required, requiredCount, baseHeights);
|
||||||
|
if (bakeCache.memoAllowed()) {
|
||||||
|
for (int index = 0; index < requiredCount; index++) {
|
||||||
|
bakeCache.storeDouble(BakePlane.BASE_HEIGHT, RiverFootprint.unpackX(required[index]),
|
||||||
|
RiverFootprint.unpackZ(required[index]), baseHeights[index], revision);
|
||||||
}
|
}
|
||||||
for (int index = 0; index < size; index++) {
|
|
||||||
values[index] = sampleTerrain3DHeight(RiverFootprint.unpackX(missing[index]), RiverFootprint.unpackZ(missing[index]));
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
for (int index = 0; index < count; index++) {
|
||||||
|
heights[index] = naturalHeightStream.getDouble(RiverFootprint.unpackX(coordinates[index]), RiverFootprint.unpackZ(coordinates[index]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IrisBiome proceduralBaseBiome(double x, double z) {
|
||||||
|
return implode(inferredByType.get(bridgeStream.get(x, z)).get(x, z), x, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean bakedColumn(BakePlane plane, int blockX, int blockZ) {
|
||||||
|
ChunkPlanes.Plane values = bakeCache.planeIfPresent(plane, blockX >> 4, blockZ >> 4);
|
||||||
|
return values != null && values.ready(((blockZ & 15) << 4) | (blockX & 15));
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class HydrologyHeightProvider implements IrisHydrologyNaturalHeightProvider {
|
private final class HydrologyHeightProvider implements IrisHydrologyNaturalHeightProvider {
|
||||||
private final boolean batch = supportsHeightBatch();
|
private final boolean batch = supportsHeightBatch();
|
||||||
|
private volatile boolean bakeReserved;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public double sample(int blockX, int blockZ) {
|
public double sample(int blockX, int blockZ) {
|
||||||
|
reserveBake();
|
||||||
return naturalHeightStream.getDouble(blockX, blockZ);
|
return naturalHeightStream.getDouble(blockX, blockZ);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -899,18 +1127,165 @@ public class IrisComplex implements DataProvider {
|
|||||||
public void sampleBatch(long[] coordinates, int count, double[] heights) {
|
public void sampleBatch(long[] coordinates, int count, double[] heights) {
|
||||||
if (!batch) {
|
if (!batch) {
|
||||||
IrisHydrologyNaturalHeightProvider.super.sampleBatch(coordinates, count, heights);
|
IrisHydrologyNaturalHeightProvider.super.sampleBatch(coordinates, count, heights);
|
||||||
} else if (transitionDisplacement == null) {
|
return;
|
||||||
sampleUnblendedHeightBatch(coordinates, count, heights);
|
|
||||||
} else {
|
|
||||||
((CachedDoubleStream2D) naturalHeightStream).getBatch(coordinates, count, heights, (missing, size, values) -> {
|
|
||||||
sampleUnblendedHeightBatch(missing, size, values);
|
|
||||||
for (int index = 0; index < size; index++) {
|
|
||||||
values[index] = transitionDisplacement.height(RiverFootprint.unpackX(missing[index]),
|
|
||||||
RiverFootprint.unpackZ(missing[index]), values[index]);
|
|
||||||
}
|
}
|
||||||
});
|
reserveBake();
|
||||||
|
sampleNaturalBatch(coordinates, count, heights);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reserveBake() {
|
||||||
|
if (bakeReserved) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bakeReserved = true;
|
||||||
|
reserveHydrologyBake(HydrologyNoiseCacheBudget.SHARED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final class BakeSource implements PlaneSource {
|
||||||
|
@Override
|
||||||
|
public void fill(BakePlane plane, int chunkX, int chunkZ, long[] missing, Object values) {
|
||||||
|
switch (plane) {
|
||||||
|
case REGION -> objectPlane(regionSource, chunkX, chunkZ, missing, (Object[]) values);
|
||||||
|
case BASE_BIOME -> objectPlane(baseBiomeSource, chunkX, chunkZ, missing, (Object[]) values);
|
||||||
|
case BASE_HEIGHT -> bakeBaseHeights(chunkX, chunkZ, missing, (double[]) values);
|
||||||
|
case NATURAL_HEIGHT -> naturalPlane(chunkX, chunkZ, missing, (double[]) values);
|
||||||
|
case NATURAL_TRUE_BIOME -> objectPlane(naturalTrueBiomeSource, chunkX, chunkZ, missing, (Object[]) values);
|
||||||
|
case HEIGHT -> heightPlane(chunkX, chunkZ, missing, (double[]) values);
|
||||||
|
case TRUE_BIOME -> objectPlane(trueBiomeSource, chunkX, chunkZ, missing, (Object[]) values);
|
||||||
|
case ROCK -> objectPlane(rockSource, chunkX, chunkZ, missing, (Object[]) values);
|
||||||
|
case FLUID -> objectPlane(fluidSource, chunkX, chunkZ, missing, (Object[]) values);
|
||||||
|
case CAVE_BIOME -> objectPlane(caveSource, chunkX, chunkZ, missing, (Object[]) values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ColumnBake assemble(ChunkPlanes planes) {
|
||||||
|
return assembleColumnBake(planes.chunkX(), planes.chunkZ());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean mainThread() {
|
||||||
|
return terrainEngine.getPlatformHooks().isMainThread();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void objectPlane(ProceduralStream<?> source, int chunkX, int chunkZ, long[] missing, Object[] values) {
|
||||||
|
int originX = chunkX << 4;
|
||||||
|
int originZ = chunkZ << 4;
|
||||||
|
for (int index = 0; index < 256; index++) {
|
||||||
|
if ((missing[index >>> 6] & (1L << (index & 63))) != 0L) {
|
||||||
|
values[index] = source.get(originX + (index & 15), originZ + (index >> 4));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void naturalPlane(int chunkX, int chunkZ, long[] missing, double[] values) {
|
||||||
|
prefetchBaseHeights(chunkX, chunkZ, missing);
|
||||||
|
int originX = chunkX << 4;
|
||||||
|
int originZ = chunkZ << 4;
|
||||||
|
for (int index = 0; index < 256; index++) {
|
||||||
|
if ((missing[index >>> 6] & (1L << (index & 63))) != 0L) {
|
||||||
|
values[index] = naturalHeightSource.getDouble(originX + (index & 15), originZ + (index >> 4));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void heightPlane(int chunkX, int chunkZ, long[] missing, double[] values) {
|
||||||
|
int originX = chunkX << 4;
|
||||||
|
int originZ = chunkZ << 4;
|
||||||
|
boolean main = terrainEngine.getPlatformHooks().isMainThread();
|
||||||
|
if (!main && !ChunkBakeCache.inSparseScope()) {
|
||||||
|
bakeCache.complete(BakePlane.NATURAL_HEIGHT, chunkX, chunkZ);
|
||||||
|
}
|
||||||
|
for (int index = 0; index < 256; index++) {
|
||||||
|
if ((missing[index >>> 6] & (1L << (index & 63))) != 0L) {
|
||||||
|
int x = originX + (index & 15);
|
||||||
|
int z = originZ + (index >> 4);
|
||||||
|
values[index] = main ? nonblockingTerrainHeight(x, z) : resolveHydrologyTerrainHeight(x, z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Object bakePlane(BakePlane plane, int chunkX, int chunkZ) {
|
||||||
|
return bakeCache.complete(plane, chunkX, chunkZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long[] bakeCensus() {
|
||||||
|
return bakeCache.census();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ColumnBake columnBake(int chunkX, int chunkZ) {
|
||||||
|
return bakeCache.get(chunkX, chunkZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ColumnBake columnBakeIfPresent(int chunkX, int chunkZ) {
|
||||||
|
return bakeCache.getIfPresent(chunkX, chunkZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IrisBiome biome(int id) {
|
||||||
|
return id < 0 ? null : biomeRegistry.value(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public short biomeId(IrisBiome biome) {
|
||||||
|
return biome == null ? -1 : biomeRegistry.id(biome);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IrisRegion region(int id) {
|
||||||
|
return id < 0 ? null : regionRegistry.value(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public short regionId(IrisRegion region) {
|
||||||
|
return region == null ? -1 : regionRegistry.id(region);
|
||||||
|
}
|
||||||
|
|
||||||
|
public NativeBlockState blockState(int id) {
|
||||||
|
return id < 0 ? null : blockRegistry.value(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ColumnBake assembleColumnBake(int chunkX, int chunkZ) {
|
||||||
|
double[] height = (double[]) bakeCache.complete(BakePlane.HEIGHT, chunkX, chunkZ);
|
||||||
|
int[] rounded = new int[256];
|
||||||
|
SimdSupport.kernels().roundToInt(height, rounded, 256);
|
||||||
|
int originX = chunkX << 4;
|
||||||
|
int originZ = chunkZ << 4;
|
||||||
|
float[] fluidSurface = new float[256];
|
||||||
|
Terrain3DColumn[] terrainColumns = new Terrain3DColumn[256];
|
||||||
|
for (int index = 0; index < 256; index++) {
|
||||||
|
int x = originX + (index & 15);
|
||||||
|
int z = originZ + (index >> 4);
|
||||||
|
fluidSurface[index] = (float) riverWaterSurfaceStream.getDouble(x, z);
|
||||||
|
terrainColumns[index] = terrainColumn(x, z);
|
||||||
|
}
|
||||||
|
double[] apron = new double[ColumnBake.APRON_WIDTH * ColumnBake.APRON_WIDTH];
|
||||||
|
for (int row = 0; row < ColumnBake.APRON_WIDTH; row++) {
|
||||||
|
for (int column = 0; column < ColumnBake.APRON_WIDTH; column++) {
|
||||||
|
apron[row * ColumnBake.APRON_WIDTH + column] = heightStream.getDouble(originX - 1 + column, originZ - 1 + row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new ColumnBake(new ColumnBake.Planes(chunkX, chunkZ,
|
||||||
|
ids((Object[]) bakeCache.complete(BakePlane.REGION, chunkX, chunkZ), regionRegistry),
|
||||||
|
ids((Object[]) bakeCache.complete(BakePlane.BASE_BIOME, chunkX, chunkZ), biomeRegistry),
|
||||||
|
ids((Object[]) bakeCache.complete(BakePlane.TRUE_BIOME, chunkX, chunkZ), biomeRegistry),
|
||||||
|
ids((Object[]) bakeCache.complete(BakePlane.CAVE_BIOME, chunkX, chunkZ), biomeRegistry),
|
||||||
|
(double[]) bakeCache.complete(BakePlane.BASE_HEIGHT, chunkX, chunkZ),
|
||||||
|
(double[]) bakeCache.complete(BakePlane.NATURAL_HEIGHT, chunkX, chunkZ),
|
||||||
|
height,
|
||||||
|
rounded,
|
||||||
|
fluidSurface,
|
||||||
|
ids((Object[]) bakeCache.complete(BakePlane.ROCK, chunkX, chunkZ), blockRegistry),
|
||||||
|
ids((Object[]) bakeCache.complete(BakePlane.FLUID, chunkX, chunkZ), blockRegistry),
|
||||||
|
terrainColumns,
|
||||||
|
apron));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static <T> short[] ids(Object[] values, BakeRegistry<T> registry) {
|
||||||
|
short[] ids = new short[values.length];
|
||||||
|
for (int index = 0; index < values.length; index++) {
|
||||||
|
ids[index] = values[index] == null ? -1 : registry.id((T) values[index]);
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
}
|
}
|
||||||
|
|
||||||
private double sampleUnblendedNaturalTerrainHeight(double x, double z) {
|
private double sampleUnblendedNaturalTerrainHeight(double x, double z) {
|
||||||
@@ -993,29 +1368,29 @@ public class IrisComplex implements DataProvider {
|
|||||||
IrisBiome emptyBiome
|
IrisBiome emptyBiome
|
||||||
) {
|
) {
|
||||||
return switch (inferredType) {
|
return switch (inferredType) {
|
||||||
case CAVE -> engine.getDimension().getCaveBiomeStyle()
|
case CAVE -> new RarityStream<>(engine.getDimension().getCaveBiomeStyle()
|
||||||
.create(rng.nextParallelRNG(InferredType.CAVE.ordinal()), getData()).stream()
|
.create(rng.nextParallelRNG(InferredType.CAVE.ordinal()), getData()).stream()
|
||||||
.zoom(engine.getDimension().getBiomeZoom())
|
.zoom(engine.getDimension().getBiomeZoom())
|
||||||
.zoom(region.getCaveBiomeZoom())
|
.zoom(region.getCaveBiomeZoom()),
|
||||||
.selectRarity(loadInferredBiomes(region.getCaveBiomes(), InferredType.CAVE))
|
loadInferredBiomes(region.getCaveBiomes(), InferredType.CAVE))
|
||||||
.onNull(emptyBiome);
|
.onNull(emptyBiome);
|
||||||
case LAND -> engine.getDimension().getLandBiomeStyle()
|
case LAND -> new RarityStream<>(engine.getDimension().getLandBiomeStyle()
|
||||||
.create(rng.nextParallelRNG(InferredType.LAND.ordinal()), getData()).stream()
|
.create(rng.nextParallelRNG(InferredType.LAND.ordinal()), getData()).stream()
|
||||||
.zoom(engine.getDimension().getBiomeZoom())
|
.zoom(engine.getDimension().getBiomeZoom())
|
||||||
.zoom(engine.getDimension().getLandZoom())
|
.zoom(engine.getDimension().getLandZoom())
|
||||||
.zoom(region.getLandBiomeZoom())
|
.zoom(region.getLandBiomeZoom()),
|
||||||
.selectRarity(loadInferredBiomes(region.getLandBiomes(), InferredType.LAND));
|
loadInferredBiomes(region.getLandBiomes(), InferredType.LAND));
|
||||||
case SEA -> engine.getDimension().getSeaBiomeStyle()
|
case SEA -> new RarityStream<>(engine.getDimension().getSeaBiomeStyle()
|
||||||
.create(rng.nextParallelRNG(InferredType.SEA.ordinal()), getData()).stream()
|
.create(rng.nextParallelRNG(InferredType.SEA.ordinal()), getData()).stream()
|
||||||
.zoom(engine.getDimension().getBiomeZoom())
|
.zoom(engine.getDimension().getBiomeZoom())
|
||||||
.zoom(engine.getDimension().getSeaZoom())
|
.zoom(engine.getDimension().getSeaZoom())
|
||||||
.zoom(region.getSeaBiomeZoom())
|
.zoom(region.getSeaBiomeZoom()),
|
||||||
.selectRarity(loadInferredBiomes(region.getSeaBiomes(), InferredType.SEA));
|
loadInferredBiomes(region.getSeaBiomes(), InferredType.SEA));
|
||||||
case SHORE -> engine.getDimension().getShoreBiomeStyle()
|
case SHORE -> new RarityStream<>(engine.getDimension().getShoreBiomeStyle()
|
||||||
.create(rng.nextParallelRNG(InferredType.SHORE.ordinal()), getData()).stream()
|
.create(rng.nextParallelRNG(InferredType.SHORE.ordinal()), getData()).stream()
|
||||||
.zoom(engine.getDimension().getBiomeZoom())
|
.zoom(engine.getDimension().getBiomeZoom())
|
||||||
.zoom(region.getShoreBiomeZoom())
|
.zoom(region.getShoreBiomeZoom()),
|
||||||
.selectRarity(loadInferredBiomes(region.getShoreBiomes(), InferredType.SHORE));
|
loadInferredBiomes(region.getShoreBiomes(), InferredType.SHORE));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1646,7 +2021,7 @@ public class IrisComplex implements DataProvider {
|
|||||||
return biomeKey == null ? sample.parentBiomeKey() : biomeKey;
|
return biomeKey == null ? sample.parentBiomeKey() : biomeKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
private IrisBiome fixBiomeType(Double height, IrisBiome biome, IrisRegion region, Double x, Double z, double fluidHeight) {
|
private IrisBiome fixBiomeType(double height, IrisBiome biome, IrisRegion region, double x, double z, double fluidHeight) {
|
||||||
IrisBiome resolved = resolveSurfaceBiome(
|
IrisBiome resolved = resolveSurfaceBiome(
|
||||||
height,
|
height,
|
||||||
biome,
|
biome,
|
||||||
@@ -1762,7 +2137,19 @@ public class IrisComplex implements DataProvider {
|
|||||||
double hi = sampledBounds.max();
|
double hi = sampledBounds.max();
|
||||||
double lo = sampledBounds.min();
|
double lo = sampledBounds.min();
|
||||||
|
|
||||||
return averageGeneratorHeights(generators, lo, hi, x, z, seed + 239945);
|
return groupHeight(interpolatorIndex, generators, lo, hi, x, z, seed + 239945);
|
||||||
|
}
|
||||||
|
|
||||||
|
private double groupHeight(int group, IrisGenerator[] generators, double low, double high, double x, double z, long heightSeed) {
|
||||||
|
TerrainProgram program = terrainProgram;
|
||||||
|
if (program == null && terrainEngine != null) {
|
||||||
|
program = terrainProgram();
|
||||||
|
}
|
||||||
|
if (program == null || program.heightSeed() != heightSeed || group >= program.groups()
|
||||||
|
|| program.groupSize(group) != generators.length || generators != frozenGenerators[group]) {
|
||||||
|
return averageGeneratorHeights(generators, low, high, x, z, heightSeed);
|
||||||
|
}
|
||||||
|
return program.average(group, BATCH_SCRATCH.get(), low, high, x, z);
|
||||||
}
|
}
|
||||||
|
|
||||||
private NoiseBounds gridSampleBounds(Engine engine, IrisInterpolator interpolator, int interpolatorIndex, IrisGenerator[] generators, double x, double z) {
|
private NoiseBounds gridSampleBounds(Engine engine, IrisInterpolator interpolator, int interpolatorIndex, IrisGenerator[] generators, double x, double z) {
|
||||||
@@ -1965,7 +2352,7 @@ public class IrisComplex implements DataProvider {
|
|||||||
lo = biLerp(boundsLow(b00), boundsLow(b10), boundsLow(b01), boundsLow(b11), fx, fz);
|
lo = biLerp(boundsLow(b00), boundsLow(b10), boundsLow(b01), boundsLow(b11), fx, fz);
|
||||||
hi = biLerp(boundsHigh(b00), boundsHigh(b10), boundsHigh(b01), boundsHigh(b11), fx, fz);
|
hi = biLerp(boundsHigh(b00), boundsHigh(b10), boundsHigh(b01), boundsHigh(b11), fx, fz);
|
||||||
}
|
}
|
||||||
h += averageGeneratorHeights(generators, lo, hi, x, z, heightSeed);
|
h += groupHeight(interpolatorIndex, generators, lo, hi, x, z, heightSeed);
|
||||||
}
|
}
|
||||||
|
|
||||||
return h;
|
return h;
|
||||||
@@ -2191,6 +2578,7 @@ public class IrisComplex implements DataProvider {
|
|||||||
}
|
}
|
||||||
return height / generators.length;
|
return height / generators.length;
|
||||||
}
|
}
|
||||||
|
GeneratorEvaluations.scalar(generators.length);
|
||||||
if (GenerationCounter.ENABLED) {
|
if (GenerationCounter.ENABLED) {
|
||||||
GenerationCounter.GENERATOR_EVALUATIONS.add(generators.length);
|
GenerationCounter.GENERATOR_EVALUATIONS.add(generators.length);
|
||||||
}
|
}
|
||||||
@@ -2264,7 +2652,7 @@ public class IrisComplex implements DataProvider {
|
|||||||
return computed;
|
return computed;
|
||||||
}
|
}
|
||||||
|
|
||||||
private IrisBiome implode(IrisBiome b, Double x, Double z) {
|
private IrisBiome implode(IrisBiome b, double x, double z) {
|
||||||
if (b.getChildren().isEmpty()) {
|
if (b.getChildren().isEmpty()) {
|
||||||
return b;
|
return b;
|
||||||
}
|
}
|
||||||
@@ -2272,7 +2660,7 @@ public class IrisComplex implements DataProvider {
|
|||||||
return implode(b, x, z, 3);
|
return implode(b, x, z, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
private IrisBiome implode(IrisBiome b, Double x, Double z, int max) {
|
private IrisBiome implode(IrisBiome b, double x, double z, int max) {
|
||||||
if (max < 0) {
|
if (max < 0) {
|
||||||
return b;
|
return b;
|
||||||
}
|
}
|
||||||
@@ -2281,7 +2669,7 @@ public class IrisComplex implements DataProvider {
|
|||||||
return b;
|
return b;
|
||||||
}
|
}
|
||||||
|
|
||||||
CNG childCell = b.getChildrenGenerator(rng, 123, b.getChildShrinkFactor());
|
CNG childCell = b.getChildrenGenerator(childStyleSeed, b.getChildShrinkFactor());
|
||||||
ChildSelectionPlan childSelectionPlan = resolveChildSelectionPlan(b);
|
ChildSelectionPlan childSelectionPlan = resolveChildSelectionPlan(b);
|
||||||
IrisBiome biome = childSelectionPlan.select(childCell, x, z).withInferredType(b.getInferredType());
|
IrisBiome biome = childSelectionPlan.select(childCell, x, z).withInferredType(b.getInferredType());
|
||||||
if (biome == b && hasFixedNoise(b.getChildStyle())) {
|
if (biome == b && hasFixedNoise(b.getChildStyle())) {
|
||||||
@@ -2504,7 +2892,8 @@ public class IrisComplex implements DataProvider {
|
|||||||
this.engine = engine;
|
this.engine = engine;
|
||||||
this.generators = generators;
|
this.generators = generators;
|
||||||
this.cachedBounds = cachedBounds;
|
this.cachedBounds = cachedBounds;
|
||||||
this.integerCoordinates = complex.baseBiomeStream instanceof CachedStream2D<?>;
|
this.integerCoordinates = complex.baseBiomeStream instanceof BakedStream<?>
|
||||||
|
|| complex.baseBiomeStream instanceof CachedStream2D<?>;
|
||||||
this.inUse = true;
|
this.inUse = true;
|
||||||
if (sampleSource.get() != complex.baseBiomeStream) {
|
if (sampleSource.get() != complex.baseBiomeStream) {
|
||||||
sampleCache.clear();
|
sampleCache.clear();
|
||||||
@@ -2531,7 +2920,12 @@ public class IrisComplex implements DataProvider {
|
|||||||
double sampleZ = integerCoordinates ? (int) zz : zz;
|
double sampleZ = integerCoordinates ? (int) zz : zz;
|
||||||
IrisBiome bx = sampleCache.get(sampleX, sampleZ);
|
IrisBiome bx = sampleCache.get(sampleX, sampleZ);
|
||||||
if (bx == null) {
|
if (bx == null) {
|
||||||
|
ChunkBakeCache.enterSparse();
|
||||||
|
try {
|
||||||
bx = complex.baseBiomeStream.get(xx, zz);
|
bx = complex.baseBiomeStream.get(xx, zz);
|
||||||
|
} finally {
|
||||||
|
ChunkBakeCache.exitSparse();
|
||||||
|
}
|
||||||
sampleCache.put(sampleX, sampleZ, bx);
|
sampleCache.put(sampleX, sampleZ, bx);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2601,62 +2995,6 @@ public class IrisComplex implements DataProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class TerrainHeightStream extends CachedDoubleStream2D {
|
|
||||||
private TerrainHeightStream(Engine engine, int cacheSize) {
|
|
||||||
super("heightStream", engine, ProceduralStream.ofDouble(IrisComplex.this::resolveHydrologyTerrainHeight), cacheSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public double getDouble(double x, double z) {
|
|
||||||
return terrainEngine.getPlatformHooks().isMainThread()
|
|
||||||
? nonblockingTerrainHeight(x, z) : super.getDouble(x, z);
|
|
||||||
}
|
|
||||||
|
|
||||||
@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 void fillChunkDoubles(int worldX, int worldZ, double[] target) {
|
|
||||||
boolean mainThread = terrainEngine.getPlatformHooks().isMainThread();
|
|
||||||
if (!mainThread && (worldX & 15) == 0 && (worldZ & 15) == 0) {
|
|
||||||
super.fillChunkDoubles(worldX, worldZ, target);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (target == null || target.length != 256) {
|
|
||||||
throw new IllegalArgumentException("Expected a 16x16 target array.");
|
|
||||||
}
|
|
||||||
for (int index = 0; index < 256; index++) {
|
|
||||||
int x = worldX + (index & 15);
|
|
||||||
int z = worldZ + (index >> 4);
|
|
||||||
target[index] = mainThread ? nonblockingTerrainHeight(x, z) : super.getDouble(x, z);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void fillChunkRaw(int worldX, int worldZ, Object[] target) {
|
|
||||||
boolean mainThread = terrainEngine.getPlatformHooks().isMainThread();
|
|
||||||
if (!mainThread && (worldX & 15) == 0 && (worldZ & 15) == 0) {
|
|
||||||
super.fillChunkRaw(worldX, worldZ, target);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (target == null || target.length != 256) {
|
|
||||||
throw new IllegalArgumentException("Expected a 16x16 target array.");
|
|
||||||
}
|
|
||||||
for (int index = 0; index < 256; index++) {
|
|
||||||
int x = worldX + (index & 15);
|
|
||||||
int z = worldZ + (index >> 4);
|
|
||||||
target[index] = mainThread ? nonblockingTerrainHeight(x, z) : super.getDouble(x, z);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static final class ChildSelectionPlan {
|
static final class ChildSelectionPlan {
|
||||||
private final IrisRaritySelection<IrisBiome> selection;
|
private final IrisRaritySelection<IrisBiome> selection;
|
||||||
|
|
||||||
@@ -2707,6 +3045,8 @@ public class IrisComplex implements DataProvider {
|
|||||||
hydrologyRuntime.close();
|
hydrologyRuntime.close();
|
||||||
}
|
}
|
||||||
releaseHydrologyNoiseCaches();
|
releaseHydrologyNoiseCaches();
|
||||||
|
releaseHydrologyBake();
|
||||||
|
bakeCache.clear();
|
||||||
if (transitionDisplacement != null) {
|
if (transitionDisplacement != null) {
|
||||||
transitionDisplacement.clear();
|
transitionDisplacement.clear();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.biome.IrisBiome;
|
||||||
import art.arcane.iris.generation.image.IrisImageMapRuntime;
|
import art.arcane.iris.generation.image.IrisImageMapRuntime;
|
||||||
import art.arcane.iris.generation.image.IrisImageMapApplication;
|
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.pack.loading.IrisData;
|
||||||
import art.arcane.iris.studio.generation.BiomeBuffetLayout;
|
import art.arcane.iris.studio.generation.BiomeBuffetLayout;
|
||||||
import art.arcane.volmlib.util.collection.KList;
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
@@ -24,7 +25,7 @@ public final class RegionSelection {
|
|||||||
regions.add(region);
|
regions.add(region);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ProceduralStream<IrisRegion> selected = options.style().selectRarity(regions);
|
ProceduralStream<IrisRegion> selected = new RarityStream<>(options.style(), regions);
|
||||||
if (!options.imageMaps().has(IrisImageMapApplication.REGION)) {
|
if (!options.imageMaps().has(IrisImageMapApplication.REGION)) {
|
||||||
return selected;
|
return selected;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ import java.io.InputStream;
|
|||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.locks.Lock;
|
import java.util.concurrent.locks.Lock;
|
||||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||||
import java.util.function.BiConsumer;
|
import java.util.function.BiConsumer;
|
||||||
@@ -144,6 +145,7 @@ public class IrisObject extends IrisRegistrant {
|
|||||||
protected transient AtomicCache<AxisAlignedBB> aabb = new AtomicCache<>();
|
protected transient AtomicCache<AxisAlignedBB> aabb = new AtomicCache<>();
|
||||||
transient final AtomicCache<KList<IrisBlockVector>> surfaceSupportOffsets = new AtomicCache<>();
|
transient final AtomicCache<KList<IrisBlockVector>> surfaceSupportOffsets = new AtomicCache<>();
|
||||||
transient final AtomicCache<FloatingObjectFootprint> floatingFootprint = new AtomicCache<>();
|
transient final AtomicCache<FloatingObjectFootprint> floatingFootprint = new AtomicCache<>();
|
||||||
|
transient final ConcurrentHashMap<ObjectPlacementBake.Key, ObjectPlacementBake> placementBakes = new ConcurrentHashMap<>();
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
private transient TreeGrounding treeGrounding;
|
private transient TreeGrounding treeGrounding;
|
||||||
|
|||||||
+274
-316
@@ -30,7 +30,6 @@ import art.arcane.iris.generation.decoration.IrisVacuumSettings;
|
|||||||
import art.arcane.iris.world.entity.IrisMarker;
|
import art.arcane.iris.world.entity.IrisMarker;
|
||||||
|
|
||||||
import art.arcane.iris.pack.loading.IrisData;
|
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.IrisComplex;
|
||||||
import art.arcane.iris.generation.runtime.Engine;
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
import art.arcane.volmlib.nativelib.terrain.structure.NativeStructureVolume;
|
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 it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap;
|
||||||
|
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
import java.util.IdentityHashMap;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@@ -475,127 +475,53 @@ final class IrisObjectPlacementRunner {
|
|||||||
int vacuumLowest = Integer.MAX_VALUE;
|
int vacuumLowest = Integer.MAX_VALUE;
|
||||||
int vacuumHighest = Integer.MIN_VALUE;
|
int vacuumHighest = Integer.MIN_VALUE;
|
||||||
self.readLock.lock();
|
self.readLock.lock();
|
||||||
|
|
||||||
KMap<IrisBlockVector, String> markers = null;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
VectorMap<NativeBlockState> blocks = self.blocks;
|
ObjectPlacementBake bake = ObjectPlacementBake.of(self, spin, ceilingHang, translating ? translateOffset : null);
|
||||||
VectorMap<TileData> states = self.states;
|
PlacementStates palette = new PlacementStates(bake.palette(), config.getRotation(), spinx, spiny, spinz,
|
||||||
TreeGrounding tree = self.getTreeGrounding();
|
preventingDecay, placer.isDebugSmartBore(), excludedStiltMaterials);
|
||||||
// Zero-tile objects are the common case: skip the two Key allocations VectorMap#get needs to prove null.
|
double[] exact = warped || hasEdits || eroding ? bake.exact(self, spin) : null;
|
||||||
boolean hasStates = !states.isEmpty();
|
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) {
|
for (int index = 0; index < count; index++) {
|
||||||
markers = new KMap<>();
|
int entry = bake.paletteIndex(index);
|
||||||
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;
|
|
||||||
TileData tile = null;
|
TileData tile = null;
|
||||||
|
if (tileBlocks != null && tileCursor < tileBlocks.length && tileBlocks[tileCursor] == index) {
|
||||||
try {
|
tile = tileValues[tileCursor++];
|
||||||
d = cursor.value();
|
|
||||||
if (hasStates) {
|
|
||||||
tile = states.get(g);
|
|
||||||
}
|
|
||||||
} 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) {
|
if (stilting && palette.stiltLayer(entry)) {
|
||||||
IrisLogging.warn("Failed to read block node " + g.getBlockX() + "," + g.getBlockY() + "," + g.getBlockZ() + " in object " + self.getLoadKey() + " (null)");
|
int layer = bake.layerY(index);
|
||||||
d = IrisObject.States.air();
|
if (layer < lowest) {
|
||||||
|
lowest = layer;
|
||||||
}
|
}
|
||||||
|
if (layer > topLayer) {
|
||||||
if (placer.isDebugSmartBore() && IrisObject.States.vair().equals(d)) {
|
topLayer = layer;
|
||||||
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 (preventingDecay && IrisProceduralBlocks.hasProperty(data, "distance") && "false".equals(IrisProceduralBlocks.propertyValue(data, "persistent"))) {
|
NativeBlockState data = palette.decayed(entry);
|
||||||
data = data.withProperty("persistent", "true");
|
boolean edited = false;
|
||||||
}
|
|
||||||
|
|
||||||
if (hasEdits) {
|
if (hasEdits) {
|
||||||
|
double exactX = exact[index * 3];
|
||||||
|
double exactY = exact[index * 3 + 1];
|
||||||
|
double exactZ = exact[index * 3 + 2];
|
||||||
for (IrisObjectReplace j : config.getEdit()) {
|
for (IrisObjectReplace j : config.getEdit()) {
|
||||||
if (rng.chance(j.getChance())) {
|
if (rng.chance(j.getChance())) {
|
||||||
for (NativeBlockState k : j.getFind(rdata)) {
|
for (NativeBlockState k : j.getFind(rdata)) {
|
||||||
if (j.isExact() ? k.matches(data) : IrisObjectShaping.materialKey(k).equals(IrisObjectShaping.materialKey(data))) {
|
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));
|
boolean sameMaterial = IrisObjectShaping.materialKey(newData).equals(IrisObjectShaping.materialKey(data));
|
||||||
if (sameMaterial && !(newData.isCustom() || data.isCustom()))
|
if (sameMaterial && !(newData.isCustom() || data.isCustom()))
|
||||||
data = BlockDataMergeSupport.merge(data, newData);
|
data = BlockDataMergeSupport.merge(data, newData);
|
||||||
else
|
else
|
||||||
data = newData;
|
data = newData;
|
||||||
|
edited = true;
|
||||||
|
|
||||||
Optional<TileData> t = j.getReplace().getTile(rng, x, y, z, rdata);
|
Optional<TileData> t = j.getReplace().getTile(rng, x, y, z, rdata);
|
||||||
if (t.isPresent()) {
|
if (t.isPresent()) {
|
||||||
@@ -609,18 +535,21 @@ final class IrisObjectPlacementRunner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data = config.getRotation().rotate(data, spinx, spiny, spinz);
|
StateFacts facts = edited ? palette.factsOf(data) : palette.facts(entry);
|
||||||
if (data == null) {
|
NativeBlockState rotated = facts.rotated();
|
||||||
|
if (rotated == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
xx = x + (int) Math.round(i.getX());
|
xx = x + bake.offsetX(index);
|
||||||
|
int yy = y + bake.offsetY(index);
|
||||||
int yy = y + (int) Math.round(i.getY());
|
zz = z + bake.offsetZ(index);
|
||||||
zz = z + (int) Math.round(i.getZ());
|
|
||||||
|
|
||||||
if (warped) {
|
if (warped) {
|
||||||
xx += surfaceWarp.fitDouble(-warpHalf, warpHalf, i.getX() + x, i.getY() + y, i.getZ() + z);
|
double exactX = exact[index * 3];
|
||||||
zz += surfaceWarp.fitDouble(-warpHalf, warpHalf, i.getZ() + z, i.getY() + y, i.getX() + x);
|
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) {
|
if (paint) {
|
||||||
@@ -628,8 +557,8 @@ final class IrisObjectPlacementRunner {
|
|||||||
if (surface == IrisPaintSurfaceProjection.MISSING) {
|
if (surface == IrisPaintSurfaceProjection.MISSING) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!B.isVineBlock(data)) {
|
if (!B.isVineBlock(rotated)) {
|
||||||
yy = (int) Math.round(i.getY()) + Math.floorDiv(self.h, 2) + surface + yrand;
|
yy = bake.offsetY(index) + Math.floorDiv(self.h, 2) + surface + yrand;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -637,13 +566,18 @@ final class IrisObjectPlacementRunner {
|
|||||||
continue;
|
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))) {
|
if (canopy && !placer.beginTreeCanopy(new BlockPosition(xx, yy, zz))) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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)) {
|
if (!rawStructurePiece && B.isVineBlock(data)) {
|
||||||
data = attachVineFaces(placer, data, xx, yy, zz);
|
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
|
// 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.
|
// 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));
|
boolean wouldReplace = !rawStructurePiece && B.isVineBlock(data) && B.isSolid(placer.get(xx, yy, zz));
|
||||||
String material = IrisObjectShaping.materialKey(data);
|
boolean place = shouldPlaceObjectBlock(rawStructurePiece, facts.air(), wouldReplace);
|
||||||
boolean air = material.equals("minecraft:air") || material.equals("minecraft:cave_air");
|
|
||||||
boolean place = shouldPlaceObjectBlock(rawStructurePiece, air, wouldReplace);
|
|
||||||
|
|
||||||
if (data.isCustom() || place) {
|
if (data.isCustom() || place) {
|
||||||
placer.set(xx, yy, zz, data);
|
placer.set(xx, yy, zz, data);
|
||||||
@@ -668,8 +600,8 @@ final class IrisObjectPlacementRunner {
|
|||||||
if (tile != null) {
|
if (tile != null) {
|
||||||
placer.setTile(xx, yy, zz, tile);
|
placer.setTile(xx, yy, zz, tile);
|
||||||
}
|
}
|
||||||
if (markers != null && markers.containsKey(g)) {
|
if (markers != null && markers[index] != null) {
|
||||||
placer.setData(xx, yy, zz, new MatterMarker(markers.get(g)));
|
placer.setData(xx, yy, zz, new MatterMarker(markers[index]));
|
||||||
}
|
}
|
||||||
if (listener != null) {
|
if (listener != null) {
|
||||||
listener.accept(new BlockPosition(xx, yy, zz), data);
|
listener.accept(new BlockPosition(xx, yy, zz), data);
|
||||||
@@ -687,9 +619,6 @@ final class IrisObjectPlacementRunner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
self.readLock.unlock();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (grounding != null) {
|
if (grounding != null) {
|
||||||
for (TreeGrounding.Extension extension : grounding.extensions()) {
|
for (TreeGrounding.Extension extension : grounding.extensions()) {
|
||||||
@@ -705,9 +634,6 @@ final class IrisObjectPlacementRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (stilting) {
|
if (stilting) {
|
||||||
self.readLock.lock();
|
|
||||||
try {
|
|
||||||
VectorMap<NativeBlockState> blocks = self.blocks;
|
|
||||||
IrisStiltSettings settings = stiltSettings;
|
IrisStiltSettings settings = stiltSettings;
|
||||||
|
|
||||||
double erodeCentroidX = 0;
|
double erodeCentroidX = 0;
|
||||||
@@ -715,38 +641,21 @@ final class IrisObjectPlacementRunner {
|
|||||||
double erodeMaxDist = 1;
|
double erodeMaxDist = 1;
|
||||||
if (eroding) {
|
if (eroding) {
|
||||||
int centroidCount = 0;
|
int centroidCount = 0;
|
||||||
VectorMap<NativeBlockState>.Cursor centroidCursor = blocks.cursor();
|
for (int index = 0; index < count; index++) {
|
||||||
while (centroidCursor.next()) {
|
if (bake.layerY(index) == lowest && palette.shouldStilt(bake.paletteIndex(index))) {
|
||||||
IrisBlockVector rot = centroidCursor.key().clone();
|
erodeCentroidX += exact[index * 3];
|
||||||
spin.rotate(rot);
|
erodeCentroidZ += exact[index * 3 + 2];
|
||||||
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++;
|
centroidCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (centroidCount > 0) {
|
if (centroidCount > 0) {
|
||||||
erodeCentroidX /= centroidCount;
|
erodeCentroidX /= centroidCount;
|
||||||
erodeCentroidZ /= centroidCount;
|
erodeCentroidZ /= centroidCount;
|
||||||
}
|
}
|
||||||
VectorMap<NativeBlockState>.Cursor spreadCursor = blocks.cursor();
|
for (int index = 0; index < count; index++) {
|
||||||
while (spreadCursor.next()) {
|
if (bake.layerY(index) == lowest && palette.shouldStilt(bake.paletteIndex(index))) {
|
||||||
IrisBlockVector rot = spreadCursor.key().clone();
|
double dx = exact[index * 3] - erodeCentroidX;
|
||||||
spin.rotate(rot);
|
double dz = exact[index * 3 + 2] - erodeCentroidZ;
|
||||||
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);
|
double dist = Math.sqrt(dx * dx + dz * dz);
|
||||||
if (dist > erodeMaxDist) {
|
if (dist > erodeMaxDist) {
|
||||||
erodeMaxDist = dist;
|
erodeMaxDist = dist;
|
||||||
@@ -754,54 +663,29 @@ final class IrisObjectPlacementRunner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
VectorMap<NativeBlockState>.Cursor stiltCursor = blocks.cursor();
|
int targetLayer = ceilingHang ? topLayer : lowest;
|
||||||
while (stiltCursor.next()) {
|
for (int index = 0; index < count; index++) {
|
||||||
IrisBlockVector g = stiltCursor.key();
|
int entry = bake.paletteIndex(index);
|
||||||
NativeBlockState sourceData;
|
if (!palette.shouldStilt(entry)) {
|
||||||
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)) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
NativeBlockState d = sourceData;
|
NativeBlockState d;
|
||||||
if (settings != null && settings.getPalette() != null) {
|
if (settings != null && settings.getPalette() != null) {
|
||||||
d = config.getStiltSettings().getPalette().get(rng, x, y, z, rdata);
|
d = config.getStiltSettings().getPalette().get(rng, x, y, z, rdata);
|
||||||
} else {
|
} else {
|
||||||
String mat = IrisObjectShaping.materialKey(d);
|
d = palette.stiltDefault(entry);
|
||||||
if (mat.equals("minecraft:grass_block") || mat.equals("minecraft:mycelium") || mat.equals("minecraft:podzol") || mat.equals("minecraft:dirt_path")) {
|
|
||||||
d = IrisObject.States.dirt();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
IrisBlockVector i = g.clone();
|
if (bake.layerY(index) != targetLayer)
|
||||||
spin.rotate(i);
|
|
||||||
if (ceilingHang) {
|
|
||||||
i.setY(-i.getBlockY());
|
|
||||||
}
|
|
||||||
if (translating) {
|
|
||||||
i.add(translateOffset);
|
|
||||||
}
|
|
||||||
int targetLayer = ceilingHang ? topLayer : lowest;
|
|
||||||
if (i.getBlockY() != targetLayer)
|
|
||||||
continue;
|
continue;
|
||||||
d = config.getRotation().rotate(d, spinx, spiny, spinz);
|
d = palette.rotate(d);
|
||||||
|
|
||||||
if (hasEdits) {
|
if (hasEdits) {
|
||||||
|
double exactX = exact[index * 3];
|
||||||
|
double exactY = exact[index * 3 + 1];
|
||||||
|
double exactZ = exact[index * 3 + 2];
|
||||||
for (IrisObjectReplace j : config.getEdit()) {
|
for (IrisObjectReplace j : config.getEdit()) {
|
||||||
if (rng.chance(j.getChance())) {
|
if (rng.chance(j.getChance())) {
|
||||||
for (NativeBlockState k : j.getFind(rdata)) {
|
for (NativeBlockState k : j.getFind(rdata)) {
|
||||||
@@ -809,7 +693,7 @@ final class IrisObjectPlacementRunner {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (j.isExact() ? k.matches(d) : IrisObjectShaping.materialKey(k).equals(IrisObjectShaping.materialKey(d))) {
|
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))) {
|
if (IrisObjectShaping.materialKey(newData).equals(IrisObjectShaping.materialKey(d))) {
|
||||||
d = BlockDataMergeSupport.merge(d, newData);
|
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;
|
continue;
|
||||||
|
|
||||||
xx = x + (int) Math.round(i.getX());
|
xx = x + bake.offsetX(index);
|
||||||
zz = z + (int) Math.round(i.getZ());
|
zz = z + bake.offsetZ(index);
|
||||||
|
|
||||||
if (warped) {
|
if (warped) {
|
||||||
xx += surfaceWarp.fitDouble(-warpHalf, warpHalf, i.getX() + x, i.getY() + y, i.getZ() + z);
|
double exactX = exact[index * 3];
|
||||||
zz += surfaceWarp.fitDouble(-warpHalf, warpHalf, i.getZ() + z, i.getY() + y, i.getX() + x);
|
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) {
|
if (organic) {
|
||||||
@@ -891,8 +778,6 @@ final class IrisObjectPlacementRunner {
|
|||||||
|
|
||||||
int highest = placer.getHighest(xx, zz, self.getLoader(), true);
|
int highest = placer.getHighest(xx, zz, self.getLoader(), true);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
int lowerBound = highest - 1;
|
int lowerBound = highest - 1;
|
||||||
if (settings != null) {
|
if (settings != null) {
|
||||||
lowerBound -= config.getStiltSettings().getOverStilt() - rng.i(0, config.getStiltSettings().getYRand());
|
lowerBound -= config.getStiltSettings().getOverStilt() - rng.i(0, config.getStiltSettings().getYRand());
|
||||||
@@ -901,8 +786,8 @@ final class IrisObjectPlacementRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (eroding) {
|
if (eroding) {
|
||||||
double dx = i.getX() - erodeCentroidX;
|
double dx = exact[index * 3] - erodeCentroidX;
|
||||||
double dz = i.getZ() - erodeCentroidZ;
|
double dz = exact[index * 3 + 2] - erodeCentroidZ;
|
||||||
double normalizedDist = Math.sqrt(dx * dx + dz * dz) / erodeMaxDist;
|
double normalizedDist = Math.sqrt(dx * dx + dz * dz) / erodeMaxDist;
|
||||||
normalizedDist = Math.min(normalizedDist, 1.0);
|
normalizedDist = Math.min(normalizedDist, 1.0);
|
||||||
int totalDepth = (lowest + y) - lowerBound;
|
int totalDepth = (lowest + y) - lowerBound;
|
||||||
@@ -933,12 +818,11 @@ final class IrisObjectPlacementRunner {
|
|||||||
}
|
}
|
||||||
placer.set(xx, j, zz, normalizePlacedWaterlogging(d, placer, xx, j, zz, waterlogCandidate));
|
placer.set(xx, j, zz, normalizePlacedWaterlogging(d, placer, xx, j, zz, waterlogCandidate));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
self.readLock.unlock();
|
self.readLock.unlock();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (vacuuming && vacuumLowest != Integer.MAX_VALUE && placer.getEngine() != null) {
|
if (vacuuming && vacuumLowest != Integer.MAX_VALUE && placer.getEngine() != null) {
|
||||||
vacuumTerrain(placer, config, x, z, placementBounds.minX(), placementBounds.maxX(),
|
vacuumTerrain(placer, config, x, z, placementBounds.minX(), placementBounds.maxX(),
|
||||||
@@ -974,9 +858,76 @@ final class IrisObjectPlacementRunner {
|
|||||||
if (state == null || !IrisProceduralBlocks.hasProperty(state, "waterlogged")) {
|
if (state == null || !IrisProceduralBlocks.hasProperty(state, "waterlogged")) {
|
||||||
return state;
|
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);
|
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,
|
private KList<IrisBlockVector> transformedPaintSupport(SpinKernel spin, IrisBlockVector translateOffset,
|
||||||
CNG warp, double half, int x, int y, int z) {
|
CNG warp, double half, int x, int y, int z) {
|
||||||
KList<IrisBlockVector> offsets = new KList<>();
|
KList<IrisBlockVector> offsets = new KList<>();
|
||||||
@@ -1337,152 +1288,159 @@ final class IrisObjectPlacementRunner {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private record StateFacts(NativeBlockState rotated, boolean waterloggable, boolean authoredWaterlogged,
|
||||||
* Precomputed vector rotation for a single placement.
|
NativeBlockState dry, boolean air) {
|
||||||
* <p>
|
private static final StateFacts ROTATED_AWAY = new StateFacts(null, false, false, null, false);
|
||||||
* 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 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 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) {
|
private static final class PlacementStates {
|
||||||
if (!clamp.isLocked()) {
|
private static final Object ROTATED_AWAY = new Object();
|
||||||
return MODE_ANGLE;
|
private static final byte UNKNOWN = 0;
|
||||||
|
private static final byte NO = 1;
|
||||||
|
private static final byte YES = 2;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Math.abs(clamp.getMax()) % 360D == 180D) {
|
private NativeBlockState source(int entry) {
|
||||||
return MODE_FLIP;
|
NativeBlockState state = palette[entry];
|
||||||
|
return debugSmartBore && IrisObject.States.vair().equals(state) ? IrisObject.States.vairDebug() : state;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (clamp.getMax() % 360D == 90D || clamp.getMax() % 360D == -270D) {
|
private boolean stiltLayer(int entry) {
|
||||||
return MODE_QUARTER;
|
byte known = stiltLayers[entry];
|
||||||
|
if (known == UNKNOWN) {
|
||||||
|
known = IrisObjectShaping.isStiltLayerBlock(source(entry)) ? YES : NO;
|
||||||
|
stiltLayers[entry] = known;
|
||||||
|
}
|
||||||
|
return known == YES;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (clamp.getMax() == -90D || clamp.getMax() % 360D == 270D) {
|
private NativeBlockState decayed(int entry) {
|
||||||
return MODE_THREE_QUARTER;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
return MODE_ANGLE;
|
private StateFacts facts(int entry) {
|
||||||
|
StateFacts resolved = facts[entry];
|
||||||
|
if (resolved == null) {
|
||||||
|
resolved = computeFacts(decayed(entry));
|
||||||
|
facts[entry] = resolved;
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private StateFacts factsOf(NativeBlockState state) {
|
||||||
* Rotates in place. Axis order (X, then Z, then Y) is load bearing.
|
if (editedFacts == null) {
|
||||||
*/
|
editedFacts = new IdentityHashMap<>();
|
||||||
private void rotate(IrisBlockVector v) {
|
}
|
||||||
if (!rotates) {
|
StateFacts resolved = editedFacts.get(state);
|
||||||
return;
|
if (resolved == null) {
|
||||||
|
resolved = computeFacts(state);
|
||||||
|
editedFacts.put(state, resolved);
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (xMode) {
|
private StateFacts computeFacts(NativeBlockState state) {
|
||||||
case MODE_FLIP -> {
|
NativeBlockState rotated = rotate(state);
|
||||||
v.setZ(-v.getZ());
|
if (rotated == null) {
|
||||||
v.setY(-v.getY());
|
return StateFacts.ROTATED_AWAY;
|
||||||
}
|
|
||||||
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 -> {
|
|
||||||
}
|
}
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (zMode) {
|
private NativeBlockState rotate(NativeBlockState state) {
|
||||||
case MODE_FLIP -> {
|
if (state == null) {
|
||||||
v.setY(-v.getY());
|
return null;
|
||||||
v.setX(-v.getX());
|
|
||||||
}
|
}
|
||||||
case MODE_QUARTER -> {
|
if (rotations == null) {
|
||||||
double y = v.getY();
|
rotations = new IdentityHashMap<>();
|
||||||
v.setY(v.getX());
|
|
||||||
v.setX(-y);
|
|
||||||
}
|
}
|
||||||
case MODE_THREE_QUARTER -> {
|
Object memo = rotations.get(state);
|
||||||
double y = v.getY();
|
if (memo == null) {
|
||||||
v.setY(-v.getX());
|
NativeBlockState rotated = rotation.rotate(state, spinx, spiny, spinz);
|
||||||
v.setX(y);
|
memo = rotated == null ? ROTATED_AWAY : rotated;
|
||||||
}
|
rotations.put(state, memo);
|
||||||
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 -> {
|
|
||||||
}
|
}
|
||||||
|
return memo == ROTATED_AWAY ? null : (NativeBlockState) memo;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (yMode) {
|
private boolean shouldStilt(int entry) {
|
||||||
case MODE_FLIP -> {
|
byte known = stilts[entry];
|
||||||
v.setX(-v.getX());
|
if (known == UNKNOWN) {
|
||||||
v.setZ(-v.getZ());
|
known = IrisObjectShaping.shouldStilt(palette[entry], excludedStiltMaterials) ? YES : NO;
|
||||||
|
stilts[entry] = known;
|
||||||
}
|
}
|
||||||
case MODE_QUARTER -> {
|
return known == YES;
|
||||||
double x = v.getX();
|
|
||||||
v.setX(v.getZ());
|
|
||||||
v.setZ(-x);
|
|
||||||
}
|
}
|
||||||
case MODE_THREE_QUARTER -> {
|
|
||||||
double x = v.getX();
|
private NativeBlockState stiltDefault(int entry) {
|
||||||
v.setX(-v.getZ());
|
NativeBlockState resolved = stiltDefaults[entry];
|
||||||
v.setZ(x);
|
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;
|
||||||
}
|
}
|
||||||
case MODE_ANGLE -> {
|
return resolved;
|
||||||
double x = yCos * v.getX() + ySin * v.getZ();
|
|
||||||
double z = -ySin * v.getX() + yCos * v.getZ();
|
|
||||||
v.setX(x);
|
|
||||||
v.setZ(z);
|
|
||||||
}
|
}
|
||||||
default -> {
|
|
||||||
|
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
|
@Test
|
||||||
public void concurrentDestinationsShareOneSourceBuild() throws Exception {
|
public void concurrentDestinationsShareOneSourceBuild() throws Exception {
|
||||||
ObjectSourcePlanCache cache = new ObjectSourcePlanCache(1024L * 1024L);
|
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 art.arcane.volmlib.util.collection.KList;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertThrows;
|
import static org.junit.Assert.assertThrows;
|
||||||
@@ -34,7 +31,7 @@ public class IrisGeneratorBatchTest {
|
|||||||
assertTrue(batch.supportsBatch());
|
assertTrue(batch.supportsBatch());
|
||||||
double[] sampled = new double[XS.length];
|
double[] sampled = new double[XS.length];
|
||||||
for (long seed : seeds) {
|
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++) {
|
for (int index = 0; index < sampled.length; index++) {
|
||||||
assertEquals(style + " detail=" + detail + " seed=" + seed + " index=" + index,
|
assertEquals(style + " detail=" + detail + " seed=" + seed + " index=" + index,
|
||||||
Double.doubleToRawLongBits(scalar.getHeight(XS[index], ZS[index], seed)),
|
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);
|
IrisGenerator batch = generator(style, detail, false).setCliffHeightMax(0D).setCellFractureHeight(0D);
|
||||||
double[] values = new double[XS.length];
|
double[] values = new double[XS.length];
|
||||||
for (long seed : new long[]{-1337L, Long.MAX_VALUE - 37L}) {
|
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++) {
|
for (int index = 0; index < XS.length; index++) {
|
||||||
assertEquals(Double.doubleToRawLongBits(scalar.getHeight(XS[index], ZS[index], seed)),
|
assertEquals(Double.doubleToRawLongBits(scalar.getHeight(XS[index], ZS[index], seed)),
|
||||||
Double.doubleToRawLongBits(values[index]));
|
Double.doubleToRawLongBits(values[index]));
|
||||||
@@ -65,13 +62,15 @@ public class IrisGeneratorBatchTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void resolvesLayerContextOncePerBatch() {
|
public void resolvesLayerContextOncePerCompile() {
|
||||||
IrisNoiseGenerator layer = spy(new IrisNoiseGenerator().setStyle(NoiseStyle.SIMPLEX.style()));
|
IrisNoiseGenerator layer = spy(new IrisNoiseGenerator().setStyle(NoiseStyle.SIMPLEX.style()));
|
||||||
IrisGenerator generator = new IrisGenerator().setComposite(new KList<>(layer));
|
IrisGenerator generator = new IrisGenerator().setComposite(new KList<>(layer));
|
||||||
double[] sampled = new double[XS.length];
|
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());
|
verify(layer, times(1)).getGenerator(anyLong(), isNull(), isNull());
|
||||||
clearInvocations(layer);
|
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++) {
|
for (int index = 0; index < XS.length; index++) {
|
||||||
generator.getHeight(XS[index], ZS[index], 99L);
|
generator.getHeight(XS[index], ZS[index], 99L);
|
||||||
}
|
}
|
||||||
@@ -79,24 +78,15 @@ public class IrisGeneratorBatchTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void customGeneratorFallbackKeepsCoordinateOrder() {
|
public void customGeneratorsCannotCompile() {
|
||||||
List<Double> sampled = new ArrayList<>();
|
|
||||||
IrisGenerator generator = new IrisGenerator() {
|
IrisGenerator generator = new IrisGenerator() {
|
||||||
@Override
|
@Override
|
||||||
public double getHeight(double x, double z, long seed) {
|
public double getHeight(double x, double z, long seed) {
|
||||||
sampled.add(x);
|
return x;
|
||||||
return sampled.size();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
assertFalse(generator.supportsBatch());
|
assertFalse(generator.supportsBatch());
|
||||||
double[] values = new double[XS.length];
|
assertThrows(IllegalStateException.class, () -> generator.compileProgram(0L));
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -0,0 +1,204 @@
|
|||||||
|
package art.arcane.iris.generation.runtime;
|
||||||
|
|
||||||
|
import art.arcane.iris.configuration.IrisSettings;
|
||||||
|
import art.arcane.iris.generation.bake.BakePlane;
|
||||||
|
import art.arcane.iris.generation.bake.GeneratorEvaluations;
|
||||||
|
import art.arcane.iris.generation.block.B;
|
||||||
|
import art.arcane.iris.generation.image.IrisImageMapApplication;
|
||||||
|
import art.arcane.iris.generation.image.IrisImageMapRuntime;
|
||||||
|
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||||
|
import art.arcane.iris.pack.loading.IrisData;
|
||||||
|
import art.arcane.iris.spi.IrisPlatform;
|
||||||
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
|
import art.arcane.iris.spi.IrisServices;
|
||||||
|
import art.arcane.iris.testsupport.PlatformLeakGuard;
|
||||||
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.SplittableRandom;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.Answers.CALLS_REAL_METHODS;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyDouble;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.doReturn;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class IrisComplexBakeParityTest {
|
||||||
|
@ClassRule public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
|
||||||
|
@Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
|
private IrisSettings previousSettings;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void prepareRuntime() {
|
||||||
|
previousSettings = IrisSettings.settings;
|
||||||
|
IrisSettings.settings = new IrisSettings();
|
||||||
|
IrisServices.register(PreservationRegistry.class, mock(PreservationRegistry.class));
|
||||||
|
IrisPlatform platform = mock(IrisPlatform.class);
|
||||||
|
when(platform.platformName()).thenReturn("bukkit");
|
||||||
|
IrisPlatforms.bind(platform);
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void releaseRuntime() {
|
||||||
|
IrisPlatforms.unbind();
|
||||||
|
IrisServices.remove(PreservationRegistry.class);
|
||||||
|
IrisSettings.settings = previousSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void batchedChunkBaseHeightsEqualScalarHeightsBitForBit() throws Exception {
|
||||||
|
try (Statics ignored = new Statics(); Fixture batched = new Fixture(pack()); Fixture scalar = new Fixture(pack())) {
|
||||||
|
SplittableRandom random = new SplittableRandom(0xBA5EL);
|
||||||
|
for (int chunk = 0; chunk < 48; chunk++) {
|
||||||
|
int chunkX = random.nextInt(-400, 400);
|
||||||
|
int chunkZ = random.nextInt(-400, 400);
|
||||||
|
double[] baked = (double[]) batched.complex.bakePlane(BakePlane.BASE_HEIGHT, chunkX, chunkZ);
|
||||||
|
for (int index = 0; index < 256; index++) {
|
||||||
|
int x = (chunkX << 4) + (index & 15);
|
||||||
|
int z = (chunkZ << 4) + (index >> 4);
|
||||||
|
assertEquals("chunk " + chunkX + "," + chunkZ + " column " + index,
|
||||||
|
Double.doubleToRawLongBits(scalar.complex.getBaseTerrainHeightStream().getDouble(x, z)),
|
||||||
|
Double.doubleToRawLongBits(baked[index]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void batchedChunksEvaluateExactlyTheScalarGeneratorSet() throws Exception {
|
||||||
|
try (Statics ignored = new Statics(); Fixture batched = new Fixture(pack()); Fixture scalar = new Fixture(pack())) {
|
||||||
|
SplittableRandom random = new SplittableRandom(0xC0DEL);
|
||||||
|
long totalBatched = 0L;
|
||||||
|
for (int chunk = 0; chunk < 32; chunk++) {
|
||||||
|
int chunkX = random.nextInt(-400, 400);
|
||||||
|
int chunkZ = random.nextInt(-400, 400);
|
||||||
|
GeneratorEvaluations.Snapshot before = GeneratorEvaluations.snapshot();
|
||||||
|
batched.complex.bakePlane(BakePlane.BASE_HEIGHT, chunkX, chunkZ);
|
||||||
|
GeneratorEvaluations.Snapshot afterBatch = GeneratorEvaluations.snapshot();
|
||||||
|
for (int index = 0; index < 256; index++) {
|
||||||
|
scalar.complex.getBaseTerrainHeightStream().getDouble((chunkX << 4) + (index & 15), (chunkZ << 4) + (index >> 4));
|
||||||
|
}
|
||||||
|
GeneratorEvaluations.Snapshot afterScalar = GeneratorEvaluations.snapshot();
|
||||||
|
long batchedEvaluations = afterBatch.minus(before).bakedEvaluations();
|
||||||
|
assertEquals(0L, afterBatch.minus(before).scalarEvaluations());
|
||||||
|
assertEquals("chunk " + chunkX + "," + chunkZ, batchedEvaluations,
|
||||||
|
afterScalar.minus(afterBatch).scalarEvaluations());
|
||||||
|
totalBatched += batchedEvaluations;
|
||||||
|
}
|
||||||
|
assertTrue(totalBatched > 0L);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path pack() throws Exception {
|
||||||
|
Path pack = temporaryFolder.newFolder().toPath();
|
||||||
|
Files.createDirectories(pack.resolve("dimensions"));
|
||||||
|
Files.createDirectories(pack.resolve("regions"));
|
||||||
|
Files.createDirectories(pack.resolve("biomes"));
|
||||||
|
Files.createDirectories(pack.resolve("generators"));
|
||||||
|
Files.writeString(pack.resolve("dimensions/main.json"), """
|
||||||
|
{"regions":["alpha","beta"],"landChance":0.55,"regionZoom":0.2,"biomeZoom":0.15,
|
||||||
|
"carvingEnabled":false,"hydrology":{"rivers":{"enabled":false}}}
|
||||||
|
""");
|
||||||
|
Files.writeString(pack.resolve("regions/alpha.json"),
|
||||||
|
"{\"landBiomes\":[\"peaks\",\"meadow\",\"dunes\"],\"seaBiomes\":[\"sea\"],\"rarity\":1}");
|
||||||
|
Files.writeString(pack.resolve("regions/beta.json"),
|
||||||
|
"{\"landBiomes\":[\"meadow\",\"spires\"],\"seaBiomes\":[\"sea\"],\"rarity\":2}");
|
||||||
|
generator(pack, "mountain", "BILINEAR_STARCAST_9", 52, 0.5D, """
|
||||||
|
[{"style":{"style":"FRACTAL_SMOKE","zoom":1},"seed":3356}]""", "");
|
||||||
|
generator(pack, "magnet", "BILINEAR_STARCAST_9", 52, 0.5D, """
|
||||||
|
[{"style":{"style":"FRACTAL_SMOKE","zoom":0.6},"seed":911}]""", "");
|
||||||
|
generator(pack, "plain", "BILINEAR_STARCAST_9", 12, 0.5D, """
|
||||||
|
[{"style":{"style":"IRIS_DOUBLE","zoom":0.87,"fracture":{"style":"NOWHERE","zoom":0.195,"multiplier":18}},"seed":10056,"bezier":true}]""", "");
|
||||||
|
generator(pack, "cliffs", "BILINEAR_STARCAST_9", 12, 0.5D, """
|
||||||
|
[{"style":{"style":"GLOB","zoom":0.4,"exponent":0.7,"fracture":{"style":"NOWHERE","zoom":0.1,"multiplier":5}},"negative":true,"seed":10056}]""",
|
||||||
|
",\"cliffHeightMax\":80,\"cliffHeightMin\":35,\"cliffHeightGenerator\":{\"seed\":2348,\"style\":{\"style\":\"CELLULAR_HEIGHT\"},\"zoom\":0.4}");
|
||||||
|
generator(pack, "dunes", "BILINEAR_STARCAST_9", 23, 1D, """
|
||||||
|
[{"style":{"style":"SIMPLEX","zoom":2},"seed":77,"fracture":[{"style":{"style":"PERLIN"},"seed":5,"opacity":2.5}]}]""", "");
|
||||||
|
generator(pack, "spikes", "STARCAST_12", 42, 0D, """
|
||||||
|
[{"style":{"style":"CELLULAR","zoom":0.5},"seed":4}]""", "");
|
||||||
|
biome(pack, "peaks", "[{\"generator\":\"mountain\",\"min\":40,\"max\":120},{\"generator\":\"spikes\",\"min\":0,\"max\":20}]",
|
||||||
|
",\"children\":[\"meadow\"],\"childShrinkFactor\":1.5");
|
||||||
|
biome(pack, "meadow", "[{\"generator\":\"plain\",\"min\":2,\"max\":14},{\"generator\":\"cliffs\",\"min\":0,\"max\":9}]", "");
|
||||||
|
biome(pack, "dunes", "[{\"generator\":\"dunes\",\"min\":3,\"max\":30},{\"generator\":\"magnet\",\"min\":-4,\"max\":6}]", "");
|
||||||
|
biome(pack, "spires", "[{\"generator\":\"spikes\",\"min\":10,\"max\":60},{\"generator\":\"plain\",\"min\":1,\"max\":4}]", "");
|
||||||
|
biome(pack, "sea", "[{\"generator\":\"mountain\",\"min\":-32,\"max\":-10}]", "");
|
||||||
|
return pack;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void generator(Path pack, String key, String function, double scale, double detail,
|
||||||
|
String composite, String extra) throws Exception {
|
||||||
|
Files.writeString(pack.resolve("generators/" + key + ".json"), "{\"surfaceDetail\":" + detail
|
||||||
|
+ ",\"interpolator\":{\"function\":\"" + function + "\",\"horizontalScale\":" + scale + "}"
|
||||||
|
+ ",\"seed\":" + Math.abs(key.hashCode()) + ",\"composite\":" + composite.strip() + extra + "}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void biome(Path pack, String key, String generators, String extra) throws Exception {
|
||||||
|
Files.writeString(pack.resolve("biomes/" + key + ".json"), "{\"generators\":" + generators + extra + "}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Statics implements AutoCloseable {
|
||||||
|
private final MockedStatic<B> blocks = mockStatic(B.class);
|
||||||
|
private final MockedStatic<IrisImageMapRuntime> imageRuntimes = mockStatic(IrisImageMapRuntime.class);
|
||||||
|
|
||||||
|
private Statics() {
|
||||||
|
NativeBlockState block = mock(NativeBlockState.class);
|
||||||
|
when(block.isFluid()).thenReturn(true);
|
||||||
|
blocks.when(() -> B.getState(anyString())).thenReturn(block);
|
||||||
|
blocks.when(() -> B.getStateOrNull(anyString(), eq(false))).thenReturn(block);
|
||||||
|
IrisImageMapRuntime images = mock(IrisImageMapRuntime.class);
|
||||||
|
when(images.has(IrisImageMapApplication.BIOME)).thenReturn(false);
|
||||||
|
when(images.sampleTerrainHeight(anyDouble(), anyDouble(), anyDouble()))
|
||||||
|
.thenAnswer(invocation -> invocation.<Double>getArgument(2));
|
||||||
|
imageRuntimes.when(() -> IrisImageMapRuntime.compile(any(Engine.class))).thenReturn(images);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
imageRuntimes.close();
|
||||||
|
blocks.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Fixture implements AutoCloseable {
|
||||||
|
private final Engine engine = mock(Engine.class, CALLS_REAL_METHODS);
|
||||||
|
private final EnginePlatformHooks hooks = mock(EnginePlatformHooks.class);
|
||||||
|
private final IrisData data;
|
||||||
|
private final IrisComplex complex;
|
||||||
|
|
||||||
|
private Fixture(Path pack) {
|
||||||
|
data = IrisData.openRuntime(pack.toFile());
|
||||||
|
IrisDimension dimension = data.getDimensionLoader().load("main");
|
||||||
|
doReturn(data).when(engine).getData();
|
||||||
|
doReturn(dimension).when(engine).getDimension();
|
||||||
|
doReturn(384).when(engine).getHeight();
|
||||||
|
doReturn(-64).when(engine).getMinHeight();
|
||||||
|
doReturn(320).when(engine).getMaxHeight();
|
||||||
|
when(hooks.isMainThread()).thenReturn(false);
|
||||||
|
doReturn(hooks).when(engine).getPlatformHooks();
|
||||||
|
doReturn(new SeedManager(1337L)).when(engine).getSeedManager();
|
||||||
|
complex = new IrisComplex(engine);
|
||||||
|
doReturn(complex).when(engine).getComplex();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
complex.close();
|
||||||
|
data.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-8
@@ -44,6 +44,7 @@ import static org.mockito.ArgumentMatchers.anyDouble;
|
|||||||
import static org.mockito.ArgumentMatchers.anyInt;
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
import static org.mockito.ArgumentMatchers.anyString;
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.atMost;
|
||||||
import static org.mockito.Mockito.clearInvocations;
|
import static org.mockito.Mockito.clearInvocations;
|
||||||
import static org.mockito.Mockito.doReturn;
|
import static org.mockito.Mockito.doReturn;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
@@ -89,16 +90,10 @@ public class IrisComplexHeightStreamTest {
|
|||||||
try (Fixture fixture = new Fixture(pack())) {
|
try (Fixture fixture = new Fixture(pack())) {
|
||||||
IrisBiome mapped = new IrisBiome();
|
IrisBiome mapped = new IrisBiome();
|
||||||
when(fixture.images.sampleBiome(anyDouble(), anyDouble())).thenReturn(mapped);
|
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, 32));
|
||||||
assertSame(mapped, fixture.complex.getBaseBiomeStream().get(-16.75, 10, 32.5));
|
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, 32));
|
||||||
assertSame(mapped, fixture.complex.getNaturalTrueBiomeStream().get(-16.75, 10, 32.5));
|
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());
|
assertNull(fixture.complex.getSharedCornerBounds());
|
||||||
|
|
||||||
when(fixture.images.sampleBiome(anyDouble(), anyDouble())).thenReturn(null);
|
when(fixture.images.sampleBiome(anyDouble(), anyDouble())).thenReturn(null);
|
||||||
@@ -116,7 +111,7 @@ public class IrisComplexHeightStreamTest {
|
|||||||
int[] rounded = new int[256];
|
int[] rounded = new int[256];
|
||||||
clearInvocations(fixture.hooks);
|
clearInvocations(fixture.hooks);
|
||||||
cache.fillRounded(rounded);
|
cache.fillRounded(rounded);
|
||||||
verify(fixture.hooks, times(1)).isMainThread();
|
verify(fixture.hooks, atMost(2)).isMainThread();
|
||||||
assertEquals(256, samples.get());
|
assertEquals(256, samples.get());
|
||||||
for (int index = 0; index < 256; index++) {
|
for (int index = 0; index < 256; index++) {
|
||||||
int x = index & 15;
|
int x = index & 15;
|
||||||
@@ -127,7 +122,7 @@ public class IrisComplexHeightStreamTest {
|
|||||||
}
|
}
|
||||||
clearInvocations(fixture.hooks);
|
clearInvocations(fixture.hooks);
|
||||||
cache.fill();
|
cache.fill();
|
||||||
verify(fixture.hooks, times(1)).isMainThread();
|
verify(fixture.hooks, atMost(2)).isMainThread();
|
||||||
assertEquals(256, samples.get());
|
assertEquals(256, samples.get());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+51
-9
@@ -2,6 +2,8 @@ package art.arcane.iris.generation.runtime;
|
|||||||
|
|
||||||
import art.arcane.iris.configuration.IrisSettings;
|
import art.arcane.iris.configuration.IrisSettings;
|
||||||
import art.arcane.iris.world.pregen.PregenPerformanceProfile;
|
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.biome.IrisBiome;
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyPlanner;
|
import art.arcane.iris.generation.hydrology.HydrologyPlanner;
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyPlannerSettings;
|
import art.arcane.iris.generation.hydrology.HydrologyPlannerSettings;
|
||||||
@@ -48,17 +50,14 @@ public class IrisComplexHydrologyNoiseCacheTest {
|
|||||||
try (Fixture fixture = new Fixture(1_024)) {
|
try (Fixture fixture = new Fixture(1_024)) {
|
||||||
ProceduralStream<Double> wrapper = fixture.complex.getRawHeightStream();
|
ProceduralStream<Double> wrapper = fixture.complex.getRawHeightStream();
|
||||||
assertEquals(13D, fixture.height.getDouble(18, 5), 0D);
|
assertEquals(13D, fixture.height.getDouble(18, 5), 0D);
|
||||||
assertEquals(13D, fixture.rawHeight.getDouble(18, 5), 0D);
|
|
||||||
|
|
||||||
PregenPerformanceProfile.apply(fixture.engine);
|
PregenPerformanceProfile.apply(fixture.engine);
|
||||||
|
|
||||||
assertEquals(4_096L * 256L, fixture.height.getMaxSize());
|
assertTrue(fixture.bakes.capacity() >= 4_096);
|
||||||
assertEquals(4_096L * 256L, fixture.rawHeight.getMaxSize());
|
|
||||||
assertEquals(1_024L * 256L, fixture.biomes.getMaxSize());
|
assertEquals(1_024L * 256L, fixture.biomes.getMaxSize());
|
||||||
assertSame(wrapper, fixture.complex.getRawHeightStream());
|
assertSame(wrapper, fixture.complex.getRawHeightStream());
|
||||||
assertEquals(47D, wrapper.getDouble(18, 5), 0D);
|
assertEquals(47D, wrapper.getDouble(18, 5), 0D);
|
||||||
assertEquals(13D, fixture.height.getDouble(18, 5), 0D);
|
assertEquals(13D, fixture.height.getDouble(18, 5), 0D);
|
||||||
assertEquals(13D, fixture.rawHeight.getDouble(18, 5), 0D);
|
|
||||||
} finally {
|
} finally {
|
||||||
PregenPerformanceProfile.restore();
|
PregenPerformanceProfile.restore();
|
||||||
IrisSettings.settings = previousSettings;
|
IrisSettings.settings = previousSettings;
|
||||||
@@ -75,7 +74,7 @@ public class IrisComplexHydrologyNoiseCacheTest {
|
|||||||
fixture.complex.ensureTerrainNoiseCacheSize(4_096);
|
fixture.complex.ensureTerrainNoiseCacheSize(4_096);
|
||||||
|
|
||||||
fixture.assertCapacities(32_768);
|
fixture.assertCapacities(32_768);
|
||||||
assertEquals(4_096L * 256L, fixture.rawHeight.getMaxSize());
|
assertTrue(fixture.bakes.capacity() >= 4_096);
|
||||||
assertEquals(reservedBytes, budget.reservedBytes());
|
assertEquals(reservedBytes, budget.reservedBytes());
|
||||||
fixture.complex.close();
|
fixture.complex.close();
|
||||||
assertEquals(0L, budget.reservedBytes());
|
assertEquals(0L, budget.reservedBytes());
|
||||||
@@ -88,7 +87,7 @@ public class IrisComplexHydrologyNoiseCacheTest {
|
|||||||
fixture.complex.ensureTerrainNoiseCacheSize(4_096);
|
fixture.complex.ensureTerrainNoiseCacheSize(4_096);
|
||||||
|
|
||||||
fixture.assertCapacities(65_536);
|
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);
|
HydrologyNoiseCacheBudget budget = new HydrologyNoiseCacheBudget(Long.MAX_VALUE);
|
||||||
setField(fixture.complex, "detached", true);
|
setField(fixture.complex, "detached", true);
|
||||||
fixture.complex.expandHydrologyNoiseCaches(budget);
|
fixture.complex.expandHydrologyNoiseCaches(budget);
|
||||||
|
fixture.complex.reserveHydrologyBake(budget);
|
||||||
fixture.assertCapacities(1_024);
|
fixture.assertCapacities(1_024);
|
||||||
|
assertEquals(1_024, fixture.bakes.capacity());
|
||||||
setField(fixture.complex, "detached", false);
|
setField(fixture.complex, "detached", false);
|
||||||
fixture.complex.setHydrologyRuntime(null);
|
fixture.complex.setHydrologyRuntime(null);
|
||||||
fixture.complex.expandHydrologyNoiseCaches(budget);
|
fixture.complex.expandHydrologyNoiseCaches(budget);
|
||||||
|
fixture.complex.reserveHydrologyBake(budget);
|
||||||
fixture.assertCapacities(1_024);
|
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());
|
assertEquals(0L, budget.reservedBytes());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -297,7 +339,7 @@ public class IrisComplexHydrologyNoiseCacheTest {
|
|||||||
private final CachedStream2D<IrisRegion> regions;
|
private final CachedStream2D<IrisRegion> regions;
|
||||||
private final CachedStream2D<InferredType> bridge;
|
private final CachedStream2D<InferredType> bridge;
|
||||||
private final CachedDoubleStream2D height;
|
private final CachedDoubleStream2D height;
|
||||||
private final CachedDoubleStream2D rawHeight;
|
private final ChunkBakeCache bakes;
|
||||||
|
|
||||||
private Fixture(int capacity) throws Exception {
|
private Fixture(int capacity) throws Exception {
|
||||||
services.when(() -> IrisServices.get(PreservationRegistry.class)).thenReturn(mock(PreservationRegistry.class));
|
services.when(() -> IrisServices.get(PreservationRegistry.class)).thenReturn(mock(PreservationRegistry.class));
|
||||||
@@ -305,10 +347,10 @@ public class IrisComplexHydrologyNoiseCacheTest {
|
|||||||
regions = objectCache(capacity);
|
regions = objectCache(capacity);
|
||||||
bridge = objectCache(capacity);
|
bridge = objectCache(capacity);
|
||||||
height = heightCache(capacity);
|
height = heightCache(capacity);
|
||||||
rawHeight = heightCache(capacity);
|
bakes = new ChunkBakeCache(mock(PlaneSource.class), capacity);
|
||||||
complex.setNaturalHeightStream(height);
|
complex.setNaturalHeightStream(height);
|
||||||
complex.setHeightStream(ProceduralStream.ofDouble((x, z) -> 47D));
|
complex.setHeightStream(ProceduralStream.ofDouble((x, z) -> 47D));
|
||||||
setField(complex, "cachedHeightStream", rawHeight);
|
setField(complex, "bakeCache", bakes);
|
||||||
when(engine.getComplex()).thenReturn(complex);
|
when(engine.getComplex()).thenReturn(complex);
|
||||||
complex.setBaseBiomeStream(biomes);
|
complex.setBaseBiomeStream(biomes);
|
||||||
complex.setRegionStream(regions);
|
complex.setRegionStream(regions);
|
||||||
|
|||||||
+2
-2
@@ -83,7 +83,7 @@ public class IrisComplexSelfSelectionTest {
|
|||||||
Field field = IrisComplex.class.getDeclaredField("childSelectionPlans");
|
Field field = IrisComplex.class.getDeclaredField("childSelectionPlans");
|
||||||
field.setAccessible(true);
|
field.setAccessible(true);
|
||||||
field.set(complex, plans);
|
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);
|
implode.setAccessible(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ public class IrisComplexSelfSelectionTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CNG getChildrenGenerator(RNG rng, int signature, double scale) {
|
public CNG getChildrenGenerator(long seed, double scale) {
|
||||||
return noise;
|
return noise;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -97,11 +97,11 @@ public class IrisCarveModifierInferenceIsolationTest {
|
|||||||
doReturn(0D).when(region).getShoreHeight(anyDouble(), anyDouble());
|
doReturn(0D).when(region).getShoreHeight(anyDouble(), anyDouble());
|
||||||
Method fixBiomeType = IrisComplex.class.getDeclaredMethod(
|
Method fixBiomeType = IrisComplex.class.getDeclaredMethod(
|
||||||
"fixBiomeType",
|
"fixBiomeType",
|
||||||
Double.class,
|
double.class,
|
||||||
IrisBiome.class,
|
IrisBiome.class,
|
||||||
IrisRegion.class,
|
IrisRegion.class,
|
||||||
Double.class,
|
double.class,
|
||||||
Double.class,
|
double.class,
|
||||||
double.class
|
double.class
|
||||||
);
|
);
|
||||||
fixBiomeType.setAccessible(true);
|
fixBiomeType.setAccessible(true);
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package art.arcane.iris.generation.stream;
|
||||||
|
|
||||||
|
import art.arcane.volmlib.util.math.Rarity;
|
||||||
|
import art.arcane.volmlib.util.stream.ProceduralStream;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.SplittableRandom;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
|
||||||
|
public class RarityStreamTest {
|
||||||
|
@Test
|
||||||
|
public void picksMatchRarityStreamSelectionForEveryNoiseValue() {
|
||||||
|
SplittableRandom random = new SplittableRandom(4242L);
|
||||||
|
for (int trial = 0; trial < 200; trial++) {
|
||||||
|
List<Option> options = new ArrayList<>();
|
||||||
|
int count = 1 + random.nextInt(12);
|
||||||
|
for (int index = 0; index < count; index++) {
|
||||||
|
options.add(new Option(random.nextInt(-2, 40)));
|
||||||
|
}
|
||||||
|
double[] value = new double[1];
|
||||||
|
ProceduralStream<Double> noise = ProceduralStream.ofDouble((x, z) -> value[0]);
|
||||||
|
ProceduralStream<Option> expected = noise.selectRarity(options);
|
||||||
|
RarityStream<Option> candidate = new RarityStream<>(noise, options);
|
||||||
|
for (int sample = 0; sample < 2_000; sample++) {
|
||||||
|
value[0] = switch (random.nextInt(5)) {
|
||||||
|
case 0 -> random.nextDouble();
|
||||||
|
case 1 -> random.nextDouble(-0.5D, 1.5D);
|
||||||
|
case 2 -> random.nextInt(0, 64) / 63D;
|
||||||
|
case 3 -> sample % 2 == 0 ? Double.NaN : -0D;
|
||||||
|
default -> Math.nextUp(random.nextInt(0, 16) / 15D);
|
||||||
|
};
|
||||||
|
assertSame(expected.get(sample, -sample), candidate.get(sample, -sample));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void emptyOptionsSelectNothing() {
|
||||||
|
assertNull(new RarityStream<Option>(ProceduralStream.ofDouble((x, z) -> 0.5D), List.of()).get(0D, 0D));
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Option(int rarity) implements Rarity {
|
||||||
|
@Override
|
||||||
|
public int getRarity() {
|
||||||
|
return rarity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -791,7 +791,7 @@ public class IrisObjectPlacementRunnerRegressionTest {
|
|||||||
|
|
||||||
object.place(0, ANCHOR_Y, 0, placer, placement, new RNG(2L), data);
|
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());
|
assertEquals(1, placer.writes().size());
|
||||||
assertSame(submerged && (enabled || authored) ? wet : dry,
|
assertSame(submerged && (enabled || authored) ? wet : dry,
|
||||||
placer.writes().get(0).state());
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,8 @@
|
|||||||
|
|
||||||
package art.arcane.iris.probe;
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.bake.BakePlane;
|
||||||
|
import art.arcane.iris.generation.bake.GeneratorEvaluations;
|
||||||
import art.arcane.iris.generation.mantle.IrisCaveCarver3D;
|
import art.arcane.iris.generation.mantle.IrisCaveCarver3D;
|
||||||
import art.arcane.iris.pack.datapack.DataVersion;
|
import art.arcane.iris.pack.datapack.DataVersion;
|
||||||
import art.arcane.iris.pack.loading.IrisData;
|
import art.arcane.iris.pack.loading.IrisData;
|
||||||
@@ -622,9 +624,11 @@ public final class GenerationProbe {
|
|||||||
int totalChunks = coordinates.size();
|
int totalChunks = coordinates.size();
|
||||||
long measuredStartEpochMillis = 0L;
|
long measuredStartEpochMillis = 0L;
|
||||||
ProbeWindowMetrics measuredStart = null;
|
ProbeWindowMetrics measuredStart = null;
|
||||||
|
GeneratorEvaluations.Snapshot measuredStartEvaluations = GeneratorEvaluations.snapshot();
|
||||||
for (int index = 0; index < totalChunks; index++) {
|
for (int index = 0; index < totalChunks; index++) {
|
||||||
if (index == configuration.warmupChunks()) {
|
if (index == configuration.warmupChunks()) {
|
||||||
measuredStartEpochMillis = System.currentTimeMillis();
|
measuredStartEpochMillis = System.currentTimeMillis();
|
||||||
|
measuredStartEvaluations = GeneratorEvaluations.snapshot();
|
||||||
measuredStart = ProbeWindowMetrics.capture(generator.engine(), options.counters());
|
measuredStart = ProbeWindowMetrics.capture(generator.engine(), options.counters());
|
||||||
}
|
}
|
||||||
drainReported();
|
drainReported();
|
||||||
@@ -633,7 +637,7 @@ public final class GenerationProbe {
|
|||||||
sink.accept(outcome, index >= configuration.warmupChunks());
|
sink.accept(outcome, index >= configuration.warmupChunks());
|
||||||
printProgress(configuration, index + 1, totalChunks);
|
printProgress(configuration, index + 1, totalChunks);
|
||||||
}
|
}
|
||||||
printWindow(measuredStartEpochMillis, System.currentTimeMillis());
|
printWindow(generator.engine(), measuredStartEpochMillis, System.currentTimeMillis(), measuredStartEvaluations);
|
||||||
printWindowMetrics(measuredStart, ProbeWindowMetrics.capture(generator.engine(), options.counters()),
|
printWindowMetrics(measuredStart, ProbeWindowMetrics.capture(generator.engine(), options.counters()),
|
||||||
configuration.measuredChunks(), options);
|
configuration.measuredChunks(), options);
|
||||||
}
|
}
|
||||||
@@ -646,9 +650,19 @@ public final class GenerationProbe {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void printWindow(long startEpochMillis, long endEpochMillis) {
|
private static void printWindow(Engine engine, long startEpochMillis, long endEpochMillis, GeneratorEvaluations.Snapshot startEvaluations) {
|
||||||
System.out.println("IRIS_GENPROBE_WINDOW phase=measured start_epoch_ms=" + startEpochMillis
|
System.out.println("IRIS_GENPROBE_WINDOW phase=measured start_epoch_ms=" + startEpochMillis
|
||||||
+ " end_epoch_ms=" + endEpochMillis);
|
+ " end_epoch_ms=" + endEpochMillis);
|
||||||
|
GeneratorEvaluations.Snapshot evaluations = GeneratorEvaluations.snapshot().minus(startEvaluations);
|
||||||
|
System.out.printf(Locale.ROOT, "IRIS_GENPROBE_GENERATORS baked_chunks=%d baked_evaluations=%d evaluations_per_baked_chunk=%.3f batched_evaluations=%d scalar_evaluations=%d%n",
|
||||||
|
evaluations.bakedChunks(), evaluations.bakedEvaluations(), evaluations.bakedEvaluationsPerChunk(),
|
||||||
|
evaluations.batchedEvaluations(), evaluations.scalarEvaluations());
|
||||||
|
long[] census = engine.getComplex().bakeCensus();
|
||||||
|
StringBuilder planes = new StringBuilder("IRIS_GENPROBE_BAKE entries=").append(census[BakePlane.values().length]);
|
||||||
|
for (BakePlane plane : BakePlane.values()) {
|
||||||
|
planes.append(' ').append(plane.name().toLowerCase(Locale.ROOT)).append('=').append(census[plane.ordinal()]);
|
||||||
|
}
|
||||||
|
System.out.println(planes);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void generateParallel(ProbeChunkGenerator generator, ProbeConfiguration configuration,
|
private static void generateParallel(ProbeChunkGenerator generator, ProbeConfiguration configuration,
|
||||||
@@ -676,10 +690,11 @@ public final class GenerationProbe {
|
|||||||
configuration.centerChunkX(), configuration.centerChunkZ());
|
configuration.centerChunkX(), configuration.centerChunkZ());
|
||||||
ProbeWindowMetrics measuredStart = ProbeWindowMetrics.capture(generator.engine(), options.counters());
|
ProbeWindowMetrics measuredStart = ProbeWindowMetrics.capture(generator.engine(), options.counters());
|
||||||
long measuredStartEpochMillis = System.currentTimeMillis();
|
long measuredStartEpochMillis = System.currentTimeMillis();
|
||||||
|
GeneratorEvaluations.Snapshot measuredStartEvaluations = GeneratorEvaluations.snapshot();
|
||||||
long measuredStarted = System.nanoTime();
|
long measuredStarted = System.nanoTime();
|
||||||
runPhase(executor, generator, configuration, coordinates, measured, outcomes);
|
runPhase(executor, generator, configuration, coordinates, measured, outcomes);
|
||||||
measuredWallNanos = System.nanoTime() - measuredStarted;
|
measuredWallNanos = System.nanoTime() - measuredStarted;
|
||||||
printWindow(measuredStartEpochMillis, System.currentTimeMillis());
|
printWindow(generator.engine(), measuredStartEpochMillis, System.currentTimeMillis(), measuredStartEvaluations);
|
||||||
printWindowMetrics(measuredStart, ProbeWindowMetrics.capture(generator.engine(), options.counters()),
|
printWindowMetrics(measuredStart, ProbeWindowMetrics.capture(generator.engine(), options.counters()),
|
||||||
configuration.measuredChunks(), options);
|
configuration.measuredChunks(), options);
|
||||||
unattributed.addAll(drainReported());
|
unattributed.addAll(drainReported());
|
||||||
|
|||||||
Reference in new issue
Block a user