Enforce seed cohesion

- Scope cached generation fields to their seed and context.
- Warm decorators and ores with their canonical generation seeds.
- Stabilize object traversal and procedural placement choices.
- Isolate smart-bore variants from shared source objects.
- Pin noise and full-engine output across generation orders.
- Verify same-jar generation through cold server restarts.
This commit is contained in:
Brian Neumann-Fopiano
2026-10-02 14:43:48 -04:00
parent 6b82ea99e2
commit 4ef6fc7cc6
50 changed files with 1734 additions and 259 deletions
@@ -0,0 +1,102 @@
import { createHash } from 'node:crypto'
import { readFile, writeFile } from 'node:fs/promises'
import path from 'node:path'
const chunks = [[-64, -64], [64, -64], [-64, 64], [64, 64]]
export default {
name: 'iris-seed-cohesion',
description: 'Compare client-visible decorated chunks in two same-seed worlds explored in opposite orders.',
async run(context) {
const source = await readFile(path.join(context.server.directory, '.server-source'), 'utf8')
context.expect(/^isolated=true\s*$/m.test(source), 'Seed cohesion requires an isolated instance')
const wait = (predicate, label) => context.waitUntil(predicate, { label, timeoutMs: 60000, intervalMs: 100 })
const currentWorld = async () => {
const observation = await context.observe()
const player = observation.players.find(entry => entry.username === context.bot.username)
return observation.worlds.find(entry => entry.id === player?.world)
}
const enter = async name => {
await context.command(`/iris tp ${name}`)
await wait(async () => (await currentWorld())?.key === `iris:${name}`
|| (await currentWorld())?.name.endsWith(name), `Enter ${name}`)
await context.command('/minecraft:gamemode spectator @s')
await wait(() => context.bot.game.gameMode === 'spectator', 'Spectator mode')
await context.command('/minecraft:seed', /Seed:.*1337/)
}
const capture = async coordinates => {
const hashes = {}
const decorators = {}
let decorationCount = 0
for (const [cx, cz] of coordinates) {
const x = cx * 16
const z = cz * 16
await context.command(`/minecraft:tp @s ${x + 8.5} 48 ${z + 8.5}`, /Teleported/i, 30000)
await wait(() => Math.abs(context.bot.entity.position.x - x - 8.5) < 1
&& Math.abs(context.bot.entity.position.z - z - 8.5) < 1, 'Chunk sample teleport')
const position = context.bot.entity.position.clone()
await wait(() => context.bot.blockAt(position.clone().set(x, 1, z))
&& context.bot.blockAt(position.clone().set(x + 15, 63, z + 15)), 'Complete sample chunk')
const digest = createHash('sha256')
let decoration = 0
let terrain = 0
for (let dx = 0; dx < 16; dx++) {
for (let dz = 0; dz < 16; dz++) {
for (let y = 0; y < 64; y++) {
const block = context.bot.blockAt(position.set(x + dx, y, z + dz))
context.expect(block !== null, 'Missing sampled block', { x: x + dx, y, z: z + dz })
digest.update(`${block.stateId},`)
if (block.name === 'granite' || block.name === 'andesite') decoration++
if (block.name === 'stone') terrain++
}
}
}
context.expect(terrain > 1000, 'Fixture terrain was not generated', { cx, cz, terrain })
decorationCount += decoration
decorators[`${cx},${cz}`] = decoration
hashes[`${cx},${cz}`] = digest.digest('hex')
}
context.expect(decorationCount > 0, 'Fixture decorators were not generated in sampled chunks', { decorators })
context.report.decoratorCoverage ??= []
context.report.decoratorCoverage.push(decorators)
return hashes
}
const baseline = 'seed-cohesion-forward'
const reverse = 'seed-cohesion-reverse'
const stateFile = path.join(context.server.directory, '.seed-cohesion.json')
const jarHash = createHash('sha256').update(await readFile(path.join(context.server.directory, 'plugins', 'Iris.jar'))).digest('hex')
const restart = context.options.command === 'restart-check'
const saved = restart ? JSON.parse(await readFile(stateFile, 'utf8')) : undefined
if (restart) {
context.expect(jarHash === saved.jarHash, 'Restart-check requires the same Iris jar', { before: saved.jarHash, after: jarHash })
context.expect((await context.observe()).processId !== saved.processId, 'Restart-check requires a cold server restart')
}
const outputs = []
for (const [name, coordinates] of [[baseline, chunks], [reverse, [...chunks].reverse()]]) {
await context.step(`${restart ? 'Reload' : 'Create'} and sample ${name}`, async () => {
if (!restart) {
await context.command(`/iris create name=${name} type=cohesion seed=1337`, /Successfully created your world/i, 180000)
}
await enter(name)
const output = await capture(coordinates)
if (restart) {
for (const [coordinate, hash] of Object.entries(output)) {
context.expect(saved.hashes[coordinate] === hash, 'Persisted chunk changed after restart', { name, coordinate, before: saved.hashes[coordinate], after: hash })
}
Object.assign(output, await capture(coordinates.map(([cx, cz]) => [cx * 3, cz * 3])))
}
outputs.push(output)
})
}
await context.step('Compare all sampled blocks', async () => {
for (const [coordinate, hash] of Object.entries(outputs[0])) {
context.expect(outputs[1][coordinate] === hash, 'Same-seed worlds differ',
{ coordinate, forward: hash, reverse: outputs[1][coordinate] })
}
context.report.seedCohesion = { seed: 1337, jarHash, blocksPerChunk: 16384, restart, hashes: outputs[0] }
if (!restart) {
await writeFile(stateFile, JSON.stringify({ processId: (await context.observe()).processId, jarHash, hashes: outputs[0] }))
}
})
}
}
@@ -51,7 +51,7 @@ public final class FloatingIslandSample {
private static final double CARVE_MAX_VERTICAL_RUN_FRACTION = 0.18D;
private static final ThreadLocal<int[]> LAST_REJECT = ThreadLocal.withInitial(() -> new int[1]);
private static final ThreadLocal<double[]> LAST_DENSITY = ThreadLocal.withInitial(() -> new double[2]);
private static final ThreadLocal<HashMap<Long, FloatingIslandSample>> CHUNK_MEMO = ThreadLocal.withInitial(HashMap::new);
private static final ThreadLocal<ChunkMemo> CHUNK_MEMO = ThreadLocal.withInitial(ChunkMemo::new);
private static final ThreadLocal<IdentityHashMap<CNG, Long2DoubleOpenHashMap>> FOOTPRINT_MEMO = ThreadLocal.withInitial(IdentityHashMap::new);
private static final AtomicBoolean NULL_CNG_WARNED = new AtomicBoolean(false);
@@ -68,21 +68,32 @@ public final class FloatingIslandSample {
}
public static void clearThreadCaches() {
CHUNK_MEMO.remove();
FOOTPRINT_MEMO.remove();
LAST_REJECT.remove();
LAST_DENSITY.remove();
}
public static void clearChunkMemo() {
CHUNK_MEMO.get().clear();
CHUNK_MEMO.get().samples.clear();
FOOTPRINT_MEMO.get().clear();
}
public static FloatingIslandSample sampleMemoized(IrisBiome parent, int wx, int wz, int chunkHeight, long baseSeed, IrisData data, Engine engine, FloatingIslandBoundarySampler boundarySampler) {
long key = (((long) wx) << 32) ^ (wz & 0xFFFFFFFFL);
HashMap<Long, FloatingIslandSample> memo = CHUNK_MEMO.get();
if (memo.containsKey(key)) {
return memo.get(key);
ChunkMemo memo = CHUNK_MEMO.get();
if (memo.context == null || !memo.context.matches(wx >> 4, wz >> 4, chunkHeight, baseSeed,
data, engine, boundarySampler)) {
memo.samples.clear();
FOOTPRINT_MEMO.get().clear();
memo.context = new MemoContext(wx >> 4, wz >> 4, chunkHeight, baseSeed, data, engine, boundarySampler);
}
MemoizedSample cached = memo.samples.get(key);
if (cached != null && cached.parent == parent) {
return cached.sample;
}
FloatingIslandSample result = sample(parent, wx, wz, chunkHeight, baseSeed, data, engine, boundarySampler);
memo.put(key, result);
memo.samples.put(key, new MemoizedSample(parent, result));
return result;
}
@@ -799,4 +810,21 @@ public final class FloatingIslandSample {
}
};
}
private static final class ChunkMemo {
private final HashMap<Long, MemoizedSample> samples = new HashMap<>(256);
private MemoContext context;
}
private record MemoContext(int chunkX, int chunkZ, int height, long seed, IrisData data, Engine engine,
FloatingIslandBoundarySampler boundarySampler) {
private boolean matches(int chunkX, int chunkZ, int height, long seed, IrisData data, Engine engine,
FloatingIslandBoundarySampler boundarySampler) {
return this.chunkX == chunkX && this.chunkZ == chunkZ && this.height == height && this.seed == seed
&& this.data == data && this.engine == engine && this.boundarySampler == boundarySampler;
}
}
private record MemoizedSample(IrisBiome parent, FloatingIslandSample sample) {
}
}
@@ -105,7 +105,12 @@ public class IrisBiome extends IrisRegistrant implements Rarity {
private final transient AtomicCache<Color> cacheColorDecoratorLoad = new AtomicCache<>();
private final transient AtomicCache<Color> cacheColorLayerLoad = new AtomicCache<>();
private final transient AtomicCache<Color> cacheColorDepositLoad = new AtomicCache<>();
private final transient AtomicCache<CNG> childrenCell = new AtomicCache<>();
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private final transient LazyBoundedCache<ChildGeneratorKey, CNG> childrenGenerators = new LazyBoundedCache<>(8);
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private transient volatile CachedChildGenerator recentChildGenerator;
@Getter(AccessLevel.NONE)
private final transient LazyBoundedCache<Long, CNG> biomeGenerators =
new LazyBoundedCache<>(BIOME_GENERATOR_CACHE_SIZE);
@@ -512,11 +517,18 @@ public class IrisBiome extends IrisRegistrant implements Rarity {
}
public CNG getChildrenGenerator(RNG random, int sig, double scale) {
return childrenCell.aquire(() -> createChildrenGenerator(random, sig, scale));
}
private CNG createChildrenGenerator(RNG random, int sig, double scale) {
return getChildStyle().createScaledGenerator(deriveChildStyleSeed(random, sig), getLoader(), scale);
long seed = deriveChildStyleSeed(random, sig).getSeed();
IrisData data = getLoader();
Engine engine = data == null ? null : data.getEngine();
CachedChildGenerator recent = recentChildGenerator;
if (recent != null && recent.key.matches(seed, scale, data, engine)) {
return recent.generator;
}
ChildGeneratorKey key = new ChildGeneratorKey(seed, scale, data, engine);
CNG generator = childrenGenerators.computeIfAbsent(key,
ignored -> getChildStyle().createScaledGenerator(new RNG(seed), data, scale));
recentChildGenerator = new CachedChildGenerator(key, generator);
return generator;
}
static RNG deriveChildStyleSeed(RNG random, int signature) {
@@ -698,4 +710,27 @@ public class IrisBiome extends IrisRegistrant implements Rarity {
this.generator = generator;
}
}
private record ChildGeneratorKey(long seed, double scale, IrisData data, Engine engine) {
private boolean matches(long seed, double scale, IrisData data, Engine engine) {
return this.seed == seed && Double.doubleToLongBits(this.scale) == Double.doubleToLongBits(scale)
&& this.data == data && this.engine == engine;
}
@Override
public boolean equals(Object object) {
return object instanceof ChildGeneratorKey other && matches(other.seed, other.scale, other.data, other.engine);
}
@Override
public int hashCode() {
int hash = Long.hashCode(seed);
hash = 31 * hash + Double.hashCode(scale);
hash = 31 * hash + System.identityHashCode(data);
return 31 * hash + System.identityHashCode(engine);
}
}
private record CachedChildGenerator(ChildGeneratorKey key, CNG generator) {
}
}
@@ -33,6 +33,10 @@ import art.arcane.volmlib.util.math.Rarity;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.volmlib.util.cache.AtomicCache;
import art.arcane.iris.generation.cache.LazyBoundedCache;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.pack.schema.annotation.ArrayType;
import art.arcane.volmlib.util.documentation.Description;
@@ -43,7 +47,6 @@ import art.arcane.iris.pack.schema.annotation.RegistryListResource;
import art.arcane.iris.pack.schema.annotation.Snippet;
import art.arcane.volmlib.util.noise.CNG;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -59,48 +62,59 @@ import lombok.experimental.Accessors;
@Data
public class IrisFloatingChildBiomes implements Rarity {
private final transient AtomicCache<IrisBiome> resolvedBiome = new AtomicCache<>();
private final transient AtomicCache<CNG> footprintCache = new AtomicCache<>();
private final transient AtomicCache<FloatingIslandEdgeProfile> edgeTaperProfileCache = new AtomicCache<>();
private final transient AtomicCache<CNG> pickerCache = new AtomicCache<>();
private final transient AtomicCache<CNG> altitudeCache = new AtomicCache<>();
private final transient AtomicCache<CNG> topShapeCache = new AtomicCache<>();
private final transient AtomicCache<CNG> bottomCache = new AtomicCache<>();
private final transient AtomicCache<CNG> wallWarpCache = new AtomicCache<>();
private final transient AtomicCache<CNG> carveCache = new AtomicCache<>();
private final transient AtomicCache<IrisCaveProfileSampler> carvingProfileSamplerCache = new AtomicCache<>(true);
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private final transient LazyBoundedCache<RuntimeKey, FloatingIslandEdgeProfile> edgeTaperProfiles = new LazyBoundedCache<>(8);
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private transient volatile CachedEdgeProfile recentEdgeProfile;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private final transient LazyBoundedCache<RuntimeKey, IrisCaveProfileSampler> carvingProfileSamplers = new LazyBoundedCache<>(8);
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private transient volatile CachedCarvingProfile recentCarvingProfile;
private final transient AtomicCache<IrisObjectScale> shrinkScaleCache = new AtomicCache<>();
public CNG getFootprintCng(long baseSeed, IrisData data) {
return footprintCache.aquire(() -> getFootprintStyle().create(new RNG(baseSeed ^ 0xF007B17DL), data));
return getFootprintStyle().create(baseSeed ^ 0xF007B17DL, data);
}
public FloatingIslandEdgeProfile getEdgeTaperProfile(long baseSeed, IrisData data) {
return edgeTaperProfileCache.aquire(() -> {
Engine engine = data == null ? null : data.getEngine();
CachedEdgeProfile recent = recentEdgeProfile;
if (recent != null && recent.key.matches(baseSeed, data, engine)) {
return recent.profile;
}
RuntimeKey key = new RuntimeKey(baseSeed, data, engine);
FloatingIslandEdgeProfile profile = edgeTaperProfiles.computeIfAbsent(key, ignored -> {
int width = FloatingIslandEdgeProfile.clampWidth(getEdgeTaperWidth());
double amplitude = FloatingIslandEdgeProfile.clampVariationAmplitude(
getEdgeTaperVariationAmplitude(), width);
IrisGeneratorStyle style = getEdgeTaperVariationStyle();
CNG variation = amplitude > 0.0D && style != null
? style.create(new RNG(baseSeed ^ 0xED6E7A9E5L), data)
? style.create(baseSeed ^ 0xED6E7A9E5L, data)
: null;
return new FloatingIslandEdgeProfile(width, getEdgeTaperExponent(), amplitude, variation);
});
recentEdgeProfile = new CachedEdgeProfile(key, profile);
return profile;
}
public CNG getPickerCng(long baseSeed, IrisData data) {
return pickerCache.aquire(() -> getPickerStyle().create(new RNG(baseSeed ^ 0x91C4E72DL), data));
return getPickerStyle().create(baseSeed ^ 0x91C4E72DL, data);
}
public CNG getAltitudeCng(long baseSeed, IrisData data) {
return altitudeCache.aquire(() -> getAltitudeStyle().create(new RNG(baseSeed ^ 0xA17DEBBL), data));
return getAltitudeStyle().create(baseSeed ^ 0xA17DEBBL, data);
}
public CNG getTopShapeCng(long baseSeed, IrisData data) {
return topShapeCache.aquire(() -> getTopShapeStyle().create(new RNG(baseSeed ^ 0x70970601DEFL), data));
return getTopShapeStyle().create(baseSeed ^ 0x70970601DEFL, data);
}
public CNG getBottomCng(long baseSeed, IrisData data) {
return bottomCache.aquire(() -> getBottomStyle().create(new RNG(baseSeed ^ 0xB0770075CAFEL), data));
return getBottomStyle().create(baseSeed ^ 0xB0770075CAFEL, data);
}
public CNG getWallWarpCng(long baseSeed, IrisData data) {
@@ -108,7 +122,7 @@ public class IrisFloatingChildBiomes implements Rarity {
if (style == null) {
return null;
}
return wallWarpCache.aquire(() -> style.create(new RNG(baseSeed ^ 0xA117BA17E0FL), data));
return style.create(baseSeed ^ 0xA117BA17E0FL, data);
}
public CNG getCarveCng(long baseSeed, IrisData data) {
@@ -116,7 +130,7 @@ public class IrisFloatingChildBiomes implements Rarity {
if (style == null) {
return null;
}
return carveCache.aquire(() -> style.create(new RNG(baseSeed ^ 0xCA5EC1EE5EL), data));
return style.create(baseSeed ^ 0xCA5EC1EE5EL, data);
}
public IrisCaveProfileSampler getCarvingProfileSampler(Engine engine, IrisData data) {
@@ -124,7 +138,12 @@ public class IrisFloatingChildBiomes implements Rarity {
return null;
}
return carvingProfileSamplerCache.aquire(() -> {
CachedCarvingProfile recent = recentCarvingProfile;
if (recent != null && recent.key.matches(0L, data, engine)) {
return recent.sampler;
}
RuntimeKey key = new RuntimeKey(0L, data, engine);
IrisCaveProfileSampler sampler = carvingProfileSamplers.computeIfAbsent(key, ignored -> {
IrisBiome resolved = resolveCarvingBiome(carving, engine, data);
if (resolved == null || resolved.getCaveProfile() == null || !resolved.getCaveProfile().isEnabled()) {
return null;
@@ -132,6 +151,8 @@ public class IrisFloatingChildBiomes implements Rarity {
return new IrisCaveProfileSampler(engine, resolved.getCaveProfile());
});
recentCarvingProfile = new CachedCarvingProfile(key, sampler);
return sampler;
}
public boolean hasCarvingReference() {
@@ -379,4 +400,28 @@ public class IrisFloatingChildBiomes implements Rarity {
return loaded;
});
}
private record RuntimeKey(long seed, IrisData data, Engine engine) {
private boolean matches(long seed, IrisData data, Engine engine) {
return this.seed == seed && this.data == data && this.engine == engine;
}
@Override
public boolean equals(Object object) {
return object instanceof RuntimeKey other && matches(other.seed, other.data, other.engine);
}
@Override
public int hashCode() {
int hash = Long.hashCode(seed);
hash = 31 * hash + System.identityHashCode(data);
return 31 * hash + System.identityHashCode(engine);
}
}
private record CachedEdgeProfile(RuntimeKey key, FloatingIslandEdgeProfile profile) {
}
private record CachedCarvingProfile(RuntimeKey key, IrisCaveProfileSampler sampler) {
}
}
@@ -6,18 +6,28 @@ import art.arcane.iris.generation.geometry.IrisBlockVector;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BiConsumer;
import java.util.function.Function;
public class VectorMap<T> implements Iterable<Map.Entry<IrisBlockVector, T>> {
private final Map<Key, Map<Key, T>> map = new KMap<>();
private transient volatile long modificationRevision;
private final AtomicLong modificationRevision = new AtomicLong();
private transient volatile OrderedSnapshot<T> orderedSnapshot;
public long modificationRevision() {
return modificationRevision;
return modificationRevision.get();
}
private void changed() {
modificationRevision.incrementAndGet();
orderedSnapshot = null;
}
public int size() {
@@ -30,7 +40,7 @@ public class VectorMap<T> implements Iterable<Map.Entry<IrisBlockVector, T>> {
public boolean containsKey(@NonNull IrisBlockVector vector) {
if (map.isEmpty()) return false;
var chunk = map.get(chunk(vector));
Map<Key, T> chunk = map.get(chunk(vector));
return chunk != null && chunk.containsKey(relative(vector));
}
@@ -40,14 +50,14 @@ public class VectorMap<T> implements Iterable<Map.Entry<IrisBlockVector, T>> {
public @Nullable T get(@NonNull IrisBlockVector vector) {
if (map.isEmpty()) return null;
var chunk = map.get(chunk(vector));
Map<Key, T> chunk = map.get(chunk(vector));
return chunk == null ? null : chunk.get(relative(vector));
}
public @Nullable T put(@NonNull IrisBlockVector vector, @NonNull T value) {
T previous = map.computeIfAbsent(chunk(vector), k -> new KMap<>())
.put(relative(vector), value);
modificationRevision++;
changed();
return previous;
}
@@ -59,7 +69,7 @@ public class VectorMap<T> implements Iterable<Map.Entry<IrisBlockVector, T>> {
return mappingFunction.apply(vector);
});
if (inserted[0]) {
modificationRevision++;
changed();
}
return value;
}
@@ -77,7 +87,7 @@ public class VectorMap<T> implements Iterable<Map.Entry<IrisBlockVector, T>> {
});
if (removed[0] != null) {
modificationRevision++;
changed();
}
return (T) removed[0];
@@ -89,22 +99,47 @@ public class VectorMap<T> implements Iterable<Map.Entry<IrisBlockVector, T>> {
public void clear() {
if (!map.isEmpty()) {
modificationRevision++;
map.clear();
changed();
}
map.clear();
}
public void forEach(@NonNull BiConsumer<@NonNull IrisBlockVector, @NonNull T> consumer) {
map.forEach((chunk, values) -> {
int rX = chunk.x << 10;
int rY = chunk.y << 10;
int rZ = chunk.z << 10;
Cursor cursor = cursor();
while (cursor.next()) {
consumer.accept(cursor.key().clone(), cursor.value());
}
}
values.forEach((relative, value) -> consumer.accept(
relative.resolve(rX, rY, rZ),
value
));
});
private List<OrderedEntry<T>> orderedEntries() {
long revision = modificationRevision.get();
OrderedSnapshot<T> cached = orderedSnapshot;
if (cached != null && cached.revision() == revision) {
return cached.entries();
}
synchronized (this) {
revision = modificationRevision.get();
cached = orderedSnapshot;
if (cached != null && cached.revision() == revision) {
return cached.entries();
}
List<OrderedEntry<T>> entries = new ArrayList<>();
map.forEach((chunk, values) -> values.forEach((relative, value) -> entries.add(
new OrderedEntry<>((chunk.x << 10) + relative.x, (chunk.y << 10) + relative.y,
(chunk.z << 10) + relative.z, value))));
entries.sort(Comparator.comparingInt((OrderedEntry<T> entry) -> entry.x())
.thenComparingInt(OrderedEntry::y).thenComparingInt(OrderedEntry::z));
if (modificationRevision.get() == revision) {
orderedSnapshot = new OrderedSnapshot<>(revision, entries);
}
return entries;
}
}
private record OrderedEntry<T>(int x, int y, int z, T value) {
}
private record OrderedSnapshot<T>(long revision, List<OrderedEntry<T>> entries) {
}
private static Key chunk(IrisBlockVector vector) {
@@ -139,32 +174,21 @@ public class VectorMap<T> implements Iterable<Map.Entry<IrisBlockVector, T>> {
}
public final class Cursor {
private final Iterator<Map.Entry<Key, Map<Key, T>>> chunkIterator = map.entrySet().iterator();
private final List<OrderedEntry<T>> entries = orderedEntries();
private final IrisBlockVector position = new IrisBlockVector(0, 0, 0);
private Iterator<Map.Entry<Key, T>> relativeIterator;
private int rX, rY, rZ;
private int index;
private T value;
public boolean next() {
while (relativeIterator == null || !relativeIterator.hasNext()) {
if (!chunkIterator.hasNext()) {
value = null;
return false;
}
Map.Entry<Key, Map<Key, T>> chunk = chunkIterator.next();
rX = chunk.getKey().x << 10;
rY = chunk.getKey().y << 10;
rZ = chunk.getKey().z << 10;
relativeIterator = chunk.getValue().entrySet().iterator();
if (index >= entries.size()) {
value = null;
return false;
}
Map.Entry<Key, T> entry = relativeIterator.next();
Key relative = entry.getKey();
position.setX(rX + relative.x);
position.setY(rY + relative.y);
position.setZ(rZ + relative.z);
value = entry.getValue();
OrderedEntry<T> entry = entries.get(index++);
position.setX(entry.x());
position.setY(entry.y());
position.setZ(entry.z());
value = entry.value();
return true;
}
@@ -181,79 +205,62 @@ public class VectorMap<T> implements Iterable<Map.Entry<IrisBlockVector, T>> {
}
public class EntryIterator implements Iterator<Map.Entry<IrisBlockVector, T>> {
private final Iterator<Map.Entry<Key, Map<Key, T>>> chunkIterator = map.entrySet().iterator();
private Iterator<Map.Entry<Key, T>> relativeIterator;
private int rX, rY, rZ;
private final List<OrderedEntry<T>> entries = orderedEntries();
private int index;
private OrderedEntry<T> last;
@Override
public boolean hasNext() {
return advance();
return index < entries.size();
}
@Override
public Map.Entry<IrisBlockVector, T> next() {
if (!advance()) throw new NoSuchElementException();
var entry = relativeIterator.next();
return Map.entry(entry.getKey().resolve(rX, rY, rZ), entry.getValue());
}
private boolean advance() {
while (relativeIterator == null || !relativeIterator.hasNext()) {
if (!chunkIterator.hasNext()) return false;
var chunk = chunkIterator.next();
rX = chunk.getKey().x << 10;
rY = chunk.getKey().y << 10;
rZ = chunk.getKey().z << 10;
relativeIterator = chunk.getValue().entrySet().iterator();
if (!hasNext()) {
throw new NoSuchElementException();
}
return true;
OrderedEntry<T> entry = entries.get(index++);
last = entry;
return Map.entry(new IrisBlockVector(entry.x(), entry.y(), entry.z()), entry.value());
}
@Override
public void remove() {
if (relativeIterator == null) throw new IllegalStateException("No element to remove");
relativeIterator.remove();
modificationRevision++;
if (last == null) {
throw new IllegalStateException("No element to remove");
}
VectorMap.this.remove(new IrisBlockVector(last.x(), last.y(), last.z()));
last = null;
}
}
public class KeyIterator implements Iterator<IrisBlockVector>, Iterable<IrisBlockVector> {
private final Iterator<Map.Entry<Key, Map<Key, T>>> chunkIterator = map.entrySet().iterator();
private Iterator<Key> relativeIterator;
private int rX, rY, rZ;
private final List<OrderedEntry<T>> entries = orderedEntries();
private int index;
private OrderedEntry<T> last;
@Override
public boolean hasNext() {
return advance();
return index < entries.size();
}
@Override
public IrisBlockVector next() {
if (!advance()) throw new NoSuchElementException();
return relativeIterator.next().resolve(rX, rY, rZ);
}
private boolean advance() {
while (relativeIterator == null || !relativeIterator.hasNext()) {
if (!chunkIterator.hasNext()) return false;
var chunk = chunkIterator.next();
rX = chunk.getKey().x << 10;
rY = chunk.getKey().y << 10;
rZ = chunk.getKey().z << 10;
relativeIterator = chunk.getValue().keySet().iterator();
if (!hasNext()) {
throw new NoSuchElementException();
}
return true;
OrderedEntry<T> entry = entries.get(index++);
last = entry;
return new IrisBlockVector(entry.x(), entry.y(), entry.z());
}
@Override
public void remove() {
if (relativeIterator == null) throw new IllegalStateException("No element to remove");
relativeIterator.remove();
modificationRevision++;
if (last == null) {
throw new IllegalStateException("No element to remove");
}
VectorMap.this.remove(new IrisBlockVector(last.x(), last.y(), last.z()));
last = null;
}
@Override
@@ -263,35 +270,31 @@ public class VectorMap<T> implements Iterable<Map.Entry<IrisBlockVector, T>> {
}
public class ValueIterator implements Iterator<T>, Iterable<T> {
private final Iterator<Map<Key, T>> chunkIterator = map.values().iterator();
private Iterator<T> relativeIterator;
private final List<OrderedEntry<T>> entries = orderedEntries();
private int index;
private OrderedEntry<T> last;
@Override
public boolean hasNext() {
return advance();
return index < entries.size();
}
@Override
public T next() {
if (!advance()) throw new NoSuchElementException();
return relativeIterator.next();
}
private boolean advance() {
while (relativeIterator == null || !relativeIterator.hasNext()) {
if (!chunkIterator.hasNext()) return false;
relativeIterator = chunkIterator.next().values().iterator();
if (!hasNext()) {
throw new NoSuchElementException();
}
return true;
last = entries.get(index++);
return last.value();
}
@Override
public void remove() {
if (relativeIterator == null) throw new IllegalStateException("No element to remove");
relativeIterator.remove();
modificationRevision++;
if (last == null) {
throw new IllegalStateException("No element to remove");
}
VectorMap.this.remove(new IrisBlockVector(last.x(), last.y(), last.z()));
last = null;
}
@Override
@@ -35,7 +35,7 @@ public class FloatingDecorator {
int xf, int zf, int realX, int realZ,
int height, int max, Hunk<NativeBlockState> data, RNG rng,
Runnable candidatesNullCallback) {
RNG gRNG = new RNG(DecoratorCore.partSeed(engine.getSeedManager().getDecorator(), part));
RNG gRNG = new RNG(DecoratorCore.partSeed(engine.getSeedManager().getComponent(), part));
IrisDecorator decorator = DecoratorCore.pickDecorator(target, part, gRNG, rng, engine.getData(), realX, realZ);
if (decorator == null) {
@@ -45,11 +45,11 @@ public class FloatingDecorator {
if (!decorator.isStacking()) {
DecoratorCore.placeFloatingSimple(decorator, xf, zf, realX, realZ, height, max, data,
rng, engine.getData(), engine.getMantle());
gRNG, engine.getData(), engine.getMantle());
return max > 1 ? 1 : 0;
}
return DecoratorCore.placeFloatingStacked(decorator, xf, zf, realX, realZ, height, max, data,
rng, engine.getData(), engine.getMantle());
gRNG, engine.getData(), engine.getMantle());
}
}
@@ -60,13 +60,13 @@ public class IrisCeilingDecorator extends IrisEngineDecorator {
return;
}
}
DecoratorCore.placeSingleAt(decorator, x, z, realX, height, realZ, data, rng, getData(), true, getEngine().getMantle());
DecoratorCore.placeSingleAt(decorator, x, z, realX, height, realZ, data, partRNG, getData(), true, getEngine().getMantle());
return;
}
DecoratorCore.PlaceOpts opts = DecoratorCore.SCRATCH_OPTS.get();
opts.reset();
opts.caveSkipFluid = caveSkipFluid;
DecoratorCore.placeStackDown(decorator, x, z, realX, realZ, height, 0, data, rng, getData(), max, opts, getEngine().getMantle());
DecoratorCore.placeStackDown(decorator, x, z, realX, realZ, height, 0, data, partRNG, getData(), max, opts, getEngine().getMantle());
}
}
@@ -19,6 +19,8 @@
package art.arcane.iris.generation.decoration;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.cache.LazyBoundedCache;
import art.arcane.iris.generation.block.IrisBlockData;
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
import art.arcane.iris.generation.noise.NoiseStyle;
@@ -50,9 +52,8 @@ import lombok.experimental.Accessors;
@Description("A biome decorator is used for placing flowers, grass, cacti and so on")
@Data
public class IrisDecorator {
private final transient AtomicCache<CNG> layerGenerator = new AtomicCache<>();
private final transient AtomicCache<CNG> varianceGenerator = new AtomicCache<>();
private final transient AtomicCache<CNG> heightGenerator = new AtomicCache<>();
private final transient LazyBoundedCache<GeneratorKey, Generators> generators = new LazyBoundedCache<>(32);
private transient volatile Generators recentGenerators;
private final transient AtomicCache<KList<NativeBlockState>> blockData = new AtomicCache<>();
private final transient AtomicCache<KList<NativeBlockState>> blockDataTops = new AtomicCache<>();
private final transient AtomicCache<NativeBlockState[]> blockDataArray = new AtomicCache<>();
@@ -129,33 +130,34 @@ public class IrisDecorator {
}
public CNG getHeightGenerator(RNG rng, IrisData data) {
CNG cached = heightGenerator.getIfPresent();
if (cached != null) {
return cached;
}
return heightGenerator.aquire(() ->
AtomicCache<CNG> cache = generators(rng, data).height;
CNG cached = cache.getIfPresent();
return cached != null ? cached : cache.aquire(() ->
heightVariance.create(rng.nextParallelRNG(getBlockData(data).size() + stackMax + stackMin), data));
}
public CNG getGenerator(RNG rng, IrisData data) {
CNG cached = layerGenerator.getIfPresent();
if (cached != null) {
return cached;
}
return layerGenerator.aquire(() -> style.create(rng.nextParallelRNG(getBlockData(data).size()), data));
AtomicCache<CNG> cache = generators(rng, data).layer;
CNG cached = cache.getIfPresent();
return cached != null ? cached : cache.aquire(() -> style.create(rng.nextParallelRNG(getBlockData(data).size()), data));
}
public CNG getVarianceGenerator(RNG rng, IrisData data) {
CNG cached = varianceGenerator.getIfPresent();
return cached != null ? cached : buildVarianceGeneratorOnce(rng, data);
AtomicCache<CNG> cache = generators(rng, data).variance;
CNG cached = cache.getIfPresent();
return cached != null ? cached : cache.aquire(() -> createVarianceGenerator(rng, data));
}
private CNG buildVarianceGeneratorOnce(RNG rng, IrisData data) {
return varianceGenerator.aquire(() -> createVarianceGenerator(rng, data));
private Generators generators(RNG rng, IrisData data) {
long seed = rng.getSeed();
Engine engine = data == null ? null : data.getEngine();
Generators recent = recentGenerators;
if (recent != null && recent.key.seed == seed && recent.key.data == data && recent.key.engine == engine) {
return recent;
}
Generators selected = generators.computeIfAbsent(new GeneratorKey(seed, data, engine), Generators::new);
recentGenerators = selected;
return selected;
}
private CNG createVarianceGenerator(RNG rng, IrisData data) {
@@ -358,4 +360,29 @@ public class IrisDecorator {
public boolean isStacking() {
return getStackMax() > 1;
}
private static final class Generators {
private final GeneratorKey key;
private final AtomicCache<CNG> layer = new AtomicCache<>();
private final AtomicCache<CNG> variance = new AtomicCache<>();
private final AtomicCache<CNG> height = new AtomicCache<>();
private Generators(GeneratorKey key) {
this.key = key;
}
}
private record GeneratorKey(long seed, IrisData data, Engine engine) {
@Override
public boolean equals(Object other) {
return other instanceof GeneratorKey key
&& seed == key.seed && data == key.data && engine == key.engine;
}
@Override
public int hashCode() {
return 31 * (31 * Long.hashCode(seed) + System.identityHashCode(data))
+ System.identityHashCode(engine);
}
}
}
@@ -39,6 +39,8 @@ import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KSet;
import art.arcane.volmlib.util.math.BlockPosition;
import art.arcane.volmlib.util.math.RNG;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import lombok.AllArgsConstructor;
@@ -269,7 +271,7 @@ public class IrisDepositGenerator {
return new IrisObject(1, 1, 1);
}
KSet<BlockPosition> cells = new KSet<>();
Set<BlockPosition> cells = new LinkedHashSet<>();
vanillaEllipsoidCells(rng, size, (x, y, z) -> cells.add(new BlockPosition(x, y, z)));
return objectFromCells(cells, rng, rdata);
}
@@ -407,7 +409,7 @@ public class IrisDepositGenerator {
}
IrisObject generateVanillaScattered(RNG rng, IrisData rdata, int size) {
KSet<BlockPosition> cells = new KSet<>();
Set<BlockPosition> cells = new LinkedHashSet<>();
int candidates = rng.nextInt(Math.max(0, size) + 1);
for (int i = 0; i < candidates; i++) {
int magnitude = Math.min(i, 7);
@@ -419,7 +421,7 @@ public class IrisDepositGenerator {
return objectFromCells(cells, rng, rdata);
}
private IrisObject objectFromCells(KSet<BlockPosition> cells, RNG rng, IrisData rdata) {
private IrisObject objectFromCells(Set<BlockPosition> cells, RNG rng, IrisData rdata) {
if (cells.isEmpty()) {
return new IrisObject(1, 1, 1);
}
@@ -34,6 +34,10 @@ public abstract class IrisEngineDecorator extends EngineAssignedComponent implem
this.part = part;
}
public static long seedForPart(long baseSeed, IrisDecorationPart part) {
return DecoratorCore.partSeed(baseSeed, part);
}
@BlockCoordinates
protected RNG getRNG(int x, int z) {
long seed = DecoratorCore.partSeed(getSeed(), part);
@@ -24,7 +24,8 @@ import art.arcane.iris.generation.terrain.IrisMaterialPalette;
import art.arcane.iris.pack.value.IrisRange;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.volmlib.util.cache.AtomicCache;
import art.arcane.iris.generation.cache.LazyBoundedCache;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.volmlib.util.documentation.Description;
import art.arcane.iris.pack.schema.annotation.MaxNumber;
import art.arcane.iris.pack.schema.annotation.MinNumber;
@@ -55,10 +56,11 @@ public class IrisOreGenerator {
@Description("Vertical band (min, max) this ore can generate in, in engine-local Y where 0 is the bottom of the dimension, not world Y.")
private IrisRange range = new IrisRange(30, 80);
private transient AtomicCache<OreChance> chanceCache = new AtomicCache<>();
private final transient LazyBoundedCache<ChanceKey, OreChance> chanceCache = new LazyBoundedCache<>(32);
private transient volatile CachedChance recentChance;
public void warm(RNG rng, IrisData data) {
chanceCache.aquire(() -> OreChance.of(chanceStyle.create(rng, data)));
chance(rng, data);
palette.getLayerGenerator(rng, data);
}
@@ -71,18 +73,43 @@ public class IrisOreGenerator {
return null;
}
OreChance chance = chanceCache.getIfPresent();
if (chance == null) {
chance = chanceCache.aquire(() -> OreChance.of(chanceStyle.create(rng, data)));
}
if (chance.sample(x, y, z) > threshold) {
if (chance(rng, data).sample(x, y, z) > threshold) {
return null;
}
return palette.get(rng, x, y, z, data);
}
private OreChance chance(RNG rng, IrisData data) {
long seed = rng.getSeed();
Engine engine = data == null ? null : data.getEngine();
CachedChance recent = recentChance;
if (recent != null && recent.key.seed == seed && recent.key.data == data && recent.key.engine == engine) {
return recent.chance;
}
ChanceKey key = new ChanceKey(seed, data, engine);
OreChance selected = chanceCache.computeIfAbsent(key, ignored -> OreChance.of(chanceStyle.create(rng, data)));
recentChance = new CachedChance(key, selected);
return selected;
}
private record ChanceKey(long seed, IrisData data, Engine engine) {
@Override
public boolean equals(Object other) {
return other instanceof ChanceKey key
&& seed == key.seed && data == key.data && engine == key.engine;
}
@Override
public int hashCode() {
return 31 * (31 * Long.hashCode(seed) + System.identityHashCode(data))
+ System.identityHashCode(engine);
}
}
private record CachedChance(ChanceKey key, OreChance chance) {
}
/**
* The chance field, with its single value resolved once when it is constant (a FLAT style),
* which bands such as a full deepslate layer sample at every block.
@@ -53,7 +53,7 @@ public class IrisSeaFloorDecorator extends IrisEngineDecorator {
&& !decorator.getSlopeCondition().isValid(getComplex().getSlopeStream().get(realX, realZ))) {
return;
}
NativeBlockState block = decorator.getBlockData100(biome, rng, realX, height, realZ, getData());
NativeBlockState block = decorator.getBlockData100(biome, partRNG, realX, height, realZ, getData());
if (block != null) {
data.set(x, height, z, block);
}
@@ -61,13 +61,13 @@ public class IrisSeaFloorDecorator extends IrisEngineDecorator {
}
int stack = Math.min(limit - height,
DecoratorCore.computeStack(decorator, rng, realX, realZ, getData(), max - height));
DecoratorCore.computeStack(decorator, partRNG, realX, realZ, getData(), max - height));
for (int i = 0; i < stack; i++) {
int h = height + i;
double threshold = stack == 1 ? 1.0 : ((double) i) / (stack - 1);
NativeBlockState block = threshold >= decorator.getTopThreshold()
? decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData())
: decorator.getBlockData100(biome, rng, realX, h, realZ, getData());
? decorator.getBlockDataForTop(biome, partRNG, realX, h, realZ, getData())
: decorator.getBlockData100(biome, partRNG, realX, h, realZ, getData());
if (block == null) {
break;
}
@@ -51,7 +51,7 @@ public class IrisSeaSurfaceDecorator extends IrisEngineDecorator {
if (!decorator.isStacking()) {
int targetY = height + 1;
if (DecoratorCore.canReplaceStackTarget(data.get(x, targetY, z), false)) {
NativeBlockState block = decorator.getBlockData100(biome, rng, realX, height, realZ, getData());
NativeBlockState block = decorator.getBlockData100(biome, partRNG, realX, height, realZ, getData());
if (block != null) {
data.set(x, targetY, z, block);
}
@@ -60,7 +60,7 @@ public class IrisSeaSurfaceDecorator extends IrisEngineDecorator {
}
int stack = Math.min(maxBaseY - height,
DecoratorCore.computeStack(decorator, rng, realX, realZ, getData(), max - height));
DecoratorCore.computeStack(decorator, partRNG, realX, realZ, getData(), max - height));
for (int i = 0; i < stack; i++) {
int h = height + i;
int targetY = h + 1;
@@ -69,8 +69,8 @@ public class IrisSeaSurfaceDecorator extends IrisEngineDecorator {
}
double threshold = stack == 1 ? 1.0 : ((double) i) / (stack - 1);
NativeBlockState block = threshold >= decorator.getTopThreshold()
? decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData())
: decorator.getBlockData100(biome, rng, realX, h, realZ, getData());
? decorator.getBlockDataForTop(biome, partRNG, realX, h, realZ, getData())
: decorator.getBlockData100(biome, partRNG, realX, h, realZ, getData());
if (block == null) {
break;
}
@@ -102,7 +102,7 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator {
|| !DecoratorCore.canReplaceStackTarget(data.get(x, targetY, z), false)) {
return;
}
NativeBlockState block = decorator.getBlockData100(biome, rng, realX, height, realZ, getData());
NativeBlockState block = decorator.getBlockData100(biome, partRNG, realX, height, realZ, getData());
if (block != null && DecoratorCore.isValidShorelineSupport(decorator, block, support)
&& IrisSugarCane.canPlace(block, data, x, targetY, z, realX, realZ, getEngine())) {
data.set(x, targetY, z, block);
@@ -111,7 +111,7 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator {
return;
}
int stack = decorator.getHeight(rng, realX, realZ, getData());
int stack = decorator.getHeight(partRNG, realX, realZ, getData());
if (decorator.isScaleStack()) {
stack = (int) Math.ceil((double) (max - height) * ((double) stack / 100));
} else {
@@ -124,7 +124,7 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator {
return;
}
NativeBlockState block = decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData());
NativeBlockState block = decorator.getBlockDataForTop(biome, partRNG, realX, height, realZ, getData());
if (block != null && DecoratorCore.isValidShorelineSupport(decorator, block, support)
&& IrisSugarCane.canPlace(block, data, x, targetY, z, realX, realZ, getEngine())) {
data.set(x, targetY, z, block);
@@ -142,8 +142,8 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator {
}
double threshold = ((double) i) / (stack - 1);
NativeBlockState block = threshold >= decorator.getTopThreshold()
? decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData())
: decorator.getBlockData100(biome, rng, realX, h, realZ, getData());
? decorator.getBlockDataForTop(biome, partRNG, realX, h, realZ, getData())
: decorator.getBlockData100(biome, partRNG, realX, h, realZ, getData());
if (block == null) {
break;
}
@@ -86,13 +86,13 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator {
opts.fluidHeight = fluidHeight;
opts.caveSkipFluid = caveSkipFluid;
opts.mantle = getEngine().getMantle();
DecoratorCore.placeStackUp(decorator, x, z, realX, realZ, height, max, data, rng, getData(), opts);
DecoratorCore.placeStackUp(decorator, x, z, realX, realZ, height, max, data, partRNG, getData(), opts);
aquaticSnapshot.restoreIfUnsupported(data, x, z);
return;
}
DecoratorCore.placeSurfaceSingle(decorator, x, z, realX, height, realZ,
data, rng, getData(), underwater, caveSkipFluid, getEngine().getMantle());
data, partRNG, getData(), underwater, caveSkipFluid, getEngine().getMantle());
aquaticSnapshot.restoreIfUnsupported(data, x, z);
}
@@ -25,6 +25,7 @@ import art.arcane.volmlib.util.math.Vector3i;
import art.arcane.volmlib.util.math.RNG;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -42,6 +43,8 @@ final class RuinDecoratorApplier {
case BASE_SCATTER -> baseScatterCandidates(canvas, Math.max(1, decorator.getScatterRadius()));
};
candidates.sort(Comparator.comparingInt(Vector3i::getX)
.thenComparingInt(Vector3i::getY).thenComparingInt(Vector3i::getZ));
RNG paletteRng = new RNG(seed);
for (Vector3i v : candidates) {
if (!rng.chance(decorator.getChance())) {
@@ -24,7 +24,15 @@ import java.util.Map;
import java.util.Set;
public class TreeBlockCanvas {
public record Vec(int x, int y, int z) {
public record Vec(int x, int y, int z) implements Comparable<Vec> {
@Override
public int compareTo(Vec other) {
int comparison = Integer.compare(x, other.x);
if (comparison == 0) {
comparison = Integer.compare(y, other.y);
}
return comparison == 0 ? Integer.compare(z, other.z) : comparison;
}
}
public enum Role {
@@ -23,6 +23,9 @@ import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.math.RNG;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
@@ -90,7 +93,7 @@ public final class TreeDecoratorApplier {
private static void trunkSurface(TreeBlockCanvas canvas, IrisTreeDecorator dec, int idx, RNG rng) {
int[][] sides = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
String[] facings = {"east", "west", "south", "north"};
for (TreeBlockCanvas.Vec v : new KList<>(canvas.getTrunk())) {
for (TreeBlockCanvas.Vec v : sortedPositions(canvas.getTrunk())) {
for (int i = 0; i < sides.length; i++) {
int nx = v.x() + sides[i][0];
int nz = v.z() + sides[i][1];
@@ -105,7 +108,7 @@ public final class TreeDecoratorApplier {
private static void leafSurface(TreeBlockCanvas canvas, IrisTreeDecorator dec, int idx, RNG rng) {
int[][] sides = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
String[] facings = {"east", "west", "south", "north"};
for (TreeBlockCanvas.Vec v : new KList<>(canvas.getLeaf())) {
for (TreeBlockCanvas.Vec v : sortedPositions(canvas.getLeaf())) {
for (int i = 0; i < sides.length; i++) {
int nx = v.x() + sides[i][0];
int nz = v.z() + sides[i][1];
@@ -118,7 +121,7 @@ public final class TreeDecoratorApplier {
}
private static void branchSurface(TreeBlockCanvas canvas, IrisTreeDecorator dec, int idx, RNG rng) {
for (TreeBlockCanvas.Vec v : new KList<>(canvas.getTrunk())) {
for (TreeBlockCanvas.Vec v : sortedPositions(canvas.getTrunk())) {
int ay = v.y() + 1;
if (canvas.has(v.x(), ay, v.z()) || rng.nextDouble() > dec.getChance()) {
continue;
@@ -136,7 +139,7 @@ public final class TreeDecoratorApplier {
colTop.put(key, v.y());
}
}
for (Map.Entry<Long, Integer> e : colTop.entrySet()) {
for (Map.Entry<Long, Integer> e : sortedColumns(colTop)) {
int x = unpackX(e.getKey());
int z = unpackZ(e.getKey());
int aboveY = e.getValue() + 1;
@@ -149,7 +152,7 @@ public final class TreeDecoratorApplier {
private static void canopyBottom(TreeBlockCanvas canvas, IrisTreeDecorator dec, int idx, RNG rng) {
KMap<Long, Integer> colBottom = columnLeafBottoms(canvas);
for (Map.Entry<Long, Integer> e : colBottom.entrySet()) {
for (Map.Entry<Long, Integer> e : sortedColumns(colBottom)) {
int x = unpackX(e.getKey());
int z = unpackZ(e.getKey());
int belowY = e.getValue() - 1;
@@ -163,7 +166,7 @@ public final class TreeDecoratorApplier {
private static void canopyHang(TreeBlockCanvas canvas, IrisTreeDecorator dec, int idx, RNG rng) {
int maxLen = Math.max(1, dec.getLength());
KMap<Long, Integer> colBottom = columnLeafBottoms(canvas);
for (Map.Entry<Long, Integer> e : colBottom.entrySet()) {
for (Map.Entry<Long, Integer> e : sortedColumns(colBottom)) {
if (rng.nextDouble() > dec.getChance()) {
continue;
}
@@ -182,7 +185,7 @@ public final class TreeDecoratorApplier {
private static void trunkBase(TreeBlockCanvas canvas, IrisTreeDecorator dec, int idx, RNG rng) {
int[][] sides = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
for (TreeBlockCanvas.Vec v : new KList<>(canvas.getTrunk())) {
for (TreeBlockCanvas.Vec v : sortedPositions(canvas.getTrunk())) {
if (v.y() != 0) {
continue;
}
@@ -205,7 +208,7 @@ public final class TreeDecoratorApplier {
if (maxY == Integer.MIN_VALUE) {
return;
}
for (TreeBlockCanvas.Vec v : new KList<>(canvas.getTrunk())) {
for (TreeBlockCanvas.Vec v : sortedPositions(canvas.getTrunk())) {
if (v.y() != maxY) {
continue;
}
@@ -269,6 +272,18 @@ public final class TreeDecoratorApplier {
return colBottom;
}
private static List<TreeBlockCanvas.Vec> sortedPositions(Collection<TreeBlockCanvas.Vec> positions) {
List<TreeBlockCanvas.Vec> sorted = new ArrayList<>(positions);
sorted.sort(null);
return sorted;
}
private static List<Map.Entry<Long, Integer>> sortedColumns(Map<Long, Integer> columns) {
List<Map.Entry<Long, Integer>> sorted = new ArrayList<>(columns.entrySet());
sorted.sort(Comparator.comparingLong(Map.Entry::getKey));
return sorted;
}
private static String facingAway(int cx, int cz, int x, int z) {
int dx = x - cx;
int dz = z - cz;
@@ -44,15 +44,13 @@ public final class TreeSupport {
int best = Integer.MAX_VALUE;
for (TreeBlockCanvas.Vec wood : canvas.getTrunk()) {
int cd = chebyshev(leaf, wood);
if (cd < best) {
if (cd < best || cd == best && nearest != null && wood.compareTo(nearest) < 0) {
best = cd;
nearest = wood;
if (cd <= 1) {
break;
}
}
}
if (nearest != null && best > worstGap) {
if (nearest != null && (best > worstGap
|| best == worstGap && worstLeaf != null && leaf.compareTo(worstLeaf) < 0)) {
worstGap = best;
worstLeaf = leaf;
worstWood = nearest;
@@ -24,6 +24,10 @@ import com.dfsek.paralithic.eval.parser.Parser;
import com.dfsek.paralithic.eval.parser.Scope;
import art.arcane.iris.pack.loading.IrisRegistrant;
import art.arcane.volmlib.util.cache.AtomicCache;
import art.arcane.iris.generation.cache.LazyBoundedCache;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import art.arcane.iris.generation.noise.IrisExpressionFunction.FunctionContext;
import art.arcane.iris.pack.schema.annotation.ArrayType;
import art.arcane.volmlib.util.documentation.Description;
@@ -58,7 +62,9 @@ public class IrisExpression extends IrisRegistrant {
private String expression;
private transient AtomicCache<Expression> expressionCache = new AtomicCache<>();
private transient AtomicCache<ProceduralStream<Double>> streamCache = new AtomicCache<>();
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private final transient LazyBoundedCache<Long, ProceduralStream<Double>> streams = new LazyBoundedCache<>(8);
private Expression expression() {
return expressionCache.aquire(() -> {
@@ -94,8 +100,11 @@ public class IrisExpression extends IrisRegistrant {
}
public ProceduralStream<Double> stream(RNG rng) {
return streamCache.aquire(() -> ProceduralStream.of((x, z) -> evaluate(rng, x, z),
(x, y, z) -> evaluate(rng, x, y, z), Interpolated.DOUBLE));
return streams.computeIfAbsent(rng.getSeed(), seed -> {
RNG streamRng = new RNG(seed);
return ProceduralStream.of((x, z) -> evaluate(streamRng, x, z),
(x, y, z) -> evaluate(streamRng, x, y, z), Interpolated.DOUBLE);
});
}
public double evaluate(RNG rng, double x, double z) {
@@ -10,7 +10,11 @@ import art.arcane.volmlib.util.documentation.Description;
import art.arcane.iris.pack.schema.annotation.MinNumber;
import art.arcane.iris.pack.schema.annotation.Required;
import art.arcane.iris.pack.schema.annotation.Snippet;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.iris.generation.cache.LazyBoundedCache;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.volmlib.util.stream.ProceduralStream;
import java.util.Objects;
import art.arcane.volmlib.util.math.RNG;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
@@ -48,7 +52,10 @@ public class IrisExpressionFunction implements DynamicFunction {
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private transient final KMap<FunctionContext, Provider> cache = new KMap<>();
private transient final LazyBoundedCache<FunctionKey, Provider> providers = new LazyBoundedCache<>(8);
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private transient volatile CachedProvider recentProvider;
private transient IrisData data;
public boolean isValid() {
@@ -74,19 +81,53 @@ public class IrisExpressionFunction implements DynamicFunction {
@Override
public double eval(@Nullable Context raw, double... args) {
return cache.computeIfAbsent((FunctionContext) raw, context -> {
assert context != null;
if (engineStreamValue != null) {
var stream = engineStreamValue.get(data.getEngine());
return d -> stream.get(d[0], d[1]);
}
FunctionContext context = Objects.requireNonNull((FunctionContext) raw, "Expression context");
IrisData currentData = data;
Engine engine = currentData == null ? null : currentData.getEngine();
long seed = context.rng.getSeed();
CachedProvider recent = recentProvider;
if (recent != null && recent.key.matches(seed, currentData, engine)) {
return recent.provider.eval(args);
}
FunctionKey key = new FunctionKey(seed, currentData, engine);
Provider provider = providers.computeIfAbsent(key, ignored -> createProvider(key));
recentProvider = new CachedProvider(key, provider);
return provider.eval(args);
}
if (styleValue != null) {
return styleValue.createNoCache(context.rng, data)::noise;
private Provider createProvider(FunctionKey key) {
if (engineStreamValue != null) {
if (key.engine == null) {
throw new IllegalStateException("Expression function '" + name + "' requires an active Iris engine.");
}
ProceduralStream<Double> stream = engineStreamValue.get(key.engine);
return coordinates -> stream.get(coordinates[0], coordinates[1]);
}
if (styleValue != null) {
return styleValue.create(new RNG(key.seed), key.data, key.engine)::noise;
}
return coordinates -> Double.NaN;
}
return d -> Double.NaN;
}).eval(args);
private record FunctionKey(long seed, IrisData data, Engine engine) {
private boolean matches(long seed, IrisData data, Engine engine) {
return this.seed == seed && this.data == data && this.engine == engine;
}
@Override
public boolean equals(Object object) {
return object instanceof FunctionKey other && matches(other.seed, other.data, other.engine);
}
@Override
public int hashCode() {
int hash = Long.hashCode(seed);
hash = 31 * hash + System.identityHashCode(data);
return 31 * hash + System.identityHashCode(engine);
}
}
private record CachedProvider(FunctionKey key, Provider provider) {
}
public record FunctionContext(@NonNull RNG rng) implements Context {
@@ -19,7 +19,7 @@
package art.arcane.iris.generation.noise;
import art.arcane.iris.pack.loading.IrisRegistrant;
import art.arcane.volmlib.util.cache.AtomicCache;
import art.arcane.iris.generation.cache.LazyBoundedCache;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.pack.schema.annotation.ArrayType;
@@ -54,7 +54,12 @@ public class IrisGenerator extends IrisRegistrant {
private static final int SURFACE_CACHE_STRIPES = 16;
private static final int SURFACE_CACHE_STRIPE_SIZE = 1024;
private final transient AtomicCache<CellGenerator> cellGen = new AtomicCache<>();
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private final transient LazyBoundedCache<Long, CellGenerator> cellGenerators = new LazyBoundedCache<>(8);
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private transient volatile CachedCellGenerator recentCellGenerator;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private transient volatile SurfaceCache surfaceSamples;
@@ -115,7 +120,17 @@ public class IrisGenerator extends IrisRegistrant {
}
public CellGenerator getCellGenerator(long seed) {
return cellGen.aquire(() -> new CellGenerator(new RNG(seed + 239466)));
CachedCellGenerator recent = recentCellGenerator;
if (recent != null && recent.seed == seed) {
return recent.generator;
}
CellGenerator generator = cellGenerators.computeIfAbsent(seed, ignored -> {
CellGenerator created = new CellGenerator(new RNG(seed + 239466));
created.setShuffle(getCellFractureShuffle());
return created;
});
recentCellGenerator = new CachedCellGenerator(seed, generator);
return generator;
}
public <T> T fit(T[] v, long superSeed, double rx, double rz) {
@@ -296,7 +311,6 @@ public class IrisGenerator extends IrisRegistrant {
}
public double cell(double rx, double rz, double v, double superSeed) {
getCellGenerator(getSeed() + 46222).setShuffle(getCellFractureShuffle());
double fractureX = rx / getCellFractureZoom();
double fractureZ = rz / getCellFractureZoom();
return getCellGenerator(getSeed() + 46222).getDistance(fractureX, fractureZ) > getCellPercentSize() ? (v * getCellFractureHeight()) : v;
@@ -363,4 +377,7 @@ public class IrisGenerator extends IrisRegistrant {
entries.set(slot, sample);
}
}
private record CachedCellGenerator(long seed, CellGenerator generator) {
}
}
@@ -58,6 +58,9 @@ public class IrisGeneratorStyle {
@Setter(AccessLevel.NONE)
private final transient LazyBoundedCache<GeneratorCacheKey, CNG> generatorCache =
new LazyBoundedCache<>(GENERATOR_CACHE_SIZE);
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private transient volatile CachedGenerator recentGenerator;
@Description("The base noise style. Used when neither expression nor imageMap is set; a failed expression also falls back to this style.")
private NoiseStyle style = NoiseStyle.FLAT;
@@ -124,7 +127,7 @@ public class IrisGeneratorStyle {
}
private int hash() {
return Objects.hash(expression, imageMapHash(), multiplier, fracture != null ? fracture.hash() : 0, exponent, cacheSize, zoom, cellularZoom, cellularFrequency, style);
return Objects.hash(expression, imageMapHash(), multiplier, fracture != null ? fracture.hash() : 0, exponent, cacheSize, zoom, cellularZoom, cellularFrequency, style == null ? null : style.name());
}
public int prebakeSignature() {
@@ -233,10 +236,24 @@ public class IrisGeneratorStyle {
return create(rng, data, resolveEngine(data));
}
public CNG create(long seed, IrisData data) {
return cachedGenerator(seed, data, resolveEngine(data));
}
public CNG create(RNG rng, IrisData data, Engine engine) {
GeneratorCacheKey key = new GeneratorCacheKey(data, engine, rng.getSeed());
return generatorCache.computeIfAbsent(key,
ignored -> createNoCache(rng, data, 1, 0, false, engine));
return cachedGenerator(rng.getSeed(), data, engine);
}
private CNG cachedGenerator(long seed, IrisData data, Engine engine) {
CachedGenerator recent = recentGenerator;
if (recent != null && recent.key.matches(data, engine, seed)) {
return recent.generator;
}
GeneratorCacheKey key = new GeneratorCacheKey(data, engine, seed);
CNG generator = generatorCache.computeIfAbsent(key,
ignored -> createNoCache(new RNG(seed), data, 1, 0, false, engine));
recentGenerator = new CachedGenerator(key, generator);
return generator;
}
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
@@ -295,6 +312,10 @@ public class IrisGeneratorStyle {
this.seed = seed;
}
private boolean matches(IrisData data, Engine engine, long seed) {
return this.data == data && this.engine == engine && this.seed == seed;
}
@Override
public boolean equals(Object other) {
if (this == other) {
@@ -313,4 +334,7 @@ public class IrisGeneratorStyle {
return 31 * result + Long.hashCode(seed);
}
}
private record CachedGenerator(GeneratorCacheKey key, CNG generator) {
}
}
@@ -21,6 +21,7 @@ package art.arcane.iris.generation.runtime;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.decoration.IrisDecorator;
import art.arcane.iris.generation.decoration.IrisEngineDecorator;
import art.arcane.iris.structure.object.IrisObjectPlacement;
import art.arcane.iris.generation.decoration.IrisOreGenerator;
import art.arcane.iris.generation.decoration.IrisProceduralObjects;
@@ -47,8 +48,8 @@ public final class GenerationCacheWarmer {
biomes.sort(Comparator.comparing(IrisBiome::getLoadKey));
for (IrisBiome biome : biomes) {
warmPlacements(biome.getObjects(), root, counter, data, engine);
warmDecorators(biome.getDecorators(), root, counter, data);
warmOres(biome.getOres(), root, counter, data);
warmDecorators(biome.getDecorators(), engine.getSeedManager().getComponent(), counter, data);
warmOres(biome.getOres(), engine.getSeedManager().getTerrain(), counter, data);
warmProcedural(biome.getProceduralObjects(), root, counter, data);
}
@@ -56,11 +57,11 @@ public final class GenerationCacheWarmer {
regions.sort(Comparator.comparing(IrisRegion::getLoadKey));
for (IrisRegion region : regions) {
warmPlacements(region.getObjects(), root, counter, data, engine);
warmOres(region.getOres(), root, counter, data);
warmOres(region.getOres(), engine.getSeedManager().getTerrain(), counter, data);
warmProcedural(region.getProceduralObjects(), root, counter, data);
}
warmOres(engine.getDimension().getOres(), root, counter, data);
warmOres(engine.getDimension().getOres(), engine.getSeedManager().getTerrain(), counter, data);
IrisLogging.debug("[IrisEngine timing] cache warm " + counter[0] + " configs=" + (M.ms() - start) + "ms");
}
@@ -80,7 +81,7 @@ public final class GenerationCacheWarmer {
}
}
private static void warmDecorators(KList<IrisDecorator> decorators, RNG root, int[] counter, IrisData data) {
private static void warmDecorators(KList<IrisDecorator> decorators, long componentSeed, int[] counter, IrisData data) {
if (decorators == null) {
return;
}
@@ -88,14 +89,15 @@ public final class GenerationCacheWarmer {
if (decorator == null) {
continue;
}
RNG rng = root.nextParallelRNG(counter[0]++);
counter[0]++;
RNG rng = new RNG(IrisEngineDecorator.seedForPart(componentSeed, decorator.getPartOf()));
decorator.getHeightGenerator(rng, data);
decorator.getGenerator(rng, data);
decorator.getVarianceGenerator(rng, data);
}
}
private static void warmOres(KList<IrisOreGenerator> ores, RNG root, int[] counter, IrisData data) {
private static void warmOres(KList<IrisOreGenerator> ores, long terrainSeed, int[] counter, IrisData data) {
if (ores == null) {
return;
}
@@ -103,7 +105,8 @@ public final class GenerationCacheWarmer {
if (ore == null) {
continue;
}
ore.warm(root.nextParallelRNG(counter[0]++), data);
counter[0]++;
ore.warm(new RNG(terrainSeed), data);
}
}
@@ -67,6 +67,7 @@ import art.arcane.iris.pack.datapack.DataVersion;
import art.arcane.iris.pack.datapack.IDataFixer;
import art.arcane.iris.pack.datapack.IDataFixer.Dimension;
import art.arcane.volmlib.util.cache.AtomicCache;
import art.arcane.iris.generation.cache.LazyBoundedCache;
import art.arcane.iris.world.history.GenerationRegistryContractFactory;
import art.arcane.iris.pack.schema.annotation.ArrayType;
import art.arcane.volmlib.util.documentation.Description;
@@ -89,6 +90,9 @@ import art.arcane.volmlib.util.math.Position2;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.noise.CNG;
import lombok.Data;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
@@ -130,7 +134,12 @@ public class IrisDimension extends IrisRegistrant {
private final transient AtomicCache<IrisStaticObjectLayer> staticObjectLayer = new AtomicCache<>();
private final transient AtomicCache<CNG> rockLayerGenerator = new AtomicCache<>();
private final transient AtomicCache<CNG> fluidLayerGenerator = new AtomicCache<>();
private final transient AtomicCache<CNG> coordFracture = new AtomicCache<>();
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private final transient LazyBoundedCache<Long, CNG> coordinateFractures = new LazyBoundedCache<>(8);
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private transient volatile CachedCoordinateFracture recentCoordinateFracture;
private final transient AtomicCache<Double> sinr = new AtomicCache<>();
private final transient AtomicCache<Double> cosr = new AtomicCache<>();
private final transient AtomicCache<Double> rad = new AtomicCache<>();
@@ -537,12 +546,15 @@ public class IrisDimension extends IrisRegistrant {
}
public CNG getCoordFracture(RNG rng, int signature) {
return coordFracture.aquire(() ->
{
CNG coordFracture = CNG.signature(rng.nextParallelRNG(signature));
coordFracture.scale(0.012 / coordFractureZoom);
return coordFracture;
});
long seed = rng.getSeed() + signature;
CachedCoordinateFracture recent = recentCoordinateFracture;
if (recent != null && recent.seed == seed) {
return recent.generator;
}
CNG generator = coordinateFractures.computeIfAbsent(seed, ignored ->
CNG.signature(new RNG(seed)).scale(0.012 / coordFractureZoom));
recentCoordinateFracture = new CachedCoordinateFracture(seed, generator);
return generator;
}
public double getDimensionAngle() {
@@ -1301,4 +1313,7 @@ public class IrisDimension extends IrisRegistrant {
IO.writeAll(dimTypeVanilla, json);
}
}
private record CachedCoordinateFracture(long seed, CNG generator) {
}
}
@@ -67,7 +67,6 @@ public class IrisMaterialPalette implements StructurePalette {
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private final transient AtomicReference<CachedLayerGenerator> recentLayerGenerator = new AtomicReference<>();
private final transient AtomicCache<CNG> heightGenerator = new AtomicCache<>();
@Description("The style of noise")
private IrisGeneratorStyle style = NoiseStyle.STATIC.style();
@MinNumber(0.0001)
@@ -138,6 +138,7 @@ public class IrisObject extends IrisRegistrant {
@Getter
@Setter
protected transient volatile boolean smartBored = false;
transient volatile IrisObjectShaping.SmartBoreVariant smartBoreVariant;
@Setter
protected transient AtomicCache<AxisAlignedBB> aabb = new AtomicCache<>();
transient final AtomicCache<KList<IrisBlockVector>> surfaceSupportOffsets = new AtomicCache<>();
@@ -335,7 +336,8 @@ public class IrisObject extends IrisRegistrant {
}
public int place(int x, int yv, int z, IObjectPlacer oplacer, IrisObjectPlacement config, RNG rng, BiConsumer<BlockPosition, NativeBlockState> listener, CarveResult c, IrisData rdata) {
return new IrisObjectPlacementRunner(this).place(x, yv, z, oplacer, config, rng, listener, c, rdata);
IrisObject placedObject = config.isSmartBore() ? IrisObjectShaping.smartBoredVariant(this) : this;
return new IrisObjectPlacementRunner(placedObject).place(x, yv, z, oplacer, config, rng, listener, c, rdata);
}
KList<IrisBlockVector> getSurfaceSupportOffsets() {
@@ -49,6 +49,7 @@ import art.arcane.volmlib.util.math.Position2;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.matter.MatterMarker;
import java.util.Comparator;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
@@ -123,10 +124,6 @@ final class IrisObjectPlacementRunner {
}
}
if (config.isSmartBore()) {
IrisObjectShaping.ensureSmartBored(self);
}
boolean warped = !config.getWarp().isFlat();
// Placement-invariant hoists off the per-voxel loop: the warp CNG resolution went
// through a keyed cache probe per block, and the two boolean gates below cut per-block
@@ -902,7 +899,9 @@ final class IrisObjectPlacementRunner {
if (heightmap != null) {
RNG rngx = rng.nextParallelRNG(3468854);
for (Position2 i : heightmap.k()) {
KList<Position2> snowColumns = heightmap.k();
snowColumns.sort(Comparator.comparingInt(Position2::getX).thenComparingInt(Position2::getZ));
for (Position2 i : snowColumns) {
int vx = i.getX();
int vy = heightmap.get(i);
int vz = i.getZ();
@@ -39,6 +39,41 @@ final class IrisObjectShaping {
private IrisObjectShaping() {
}
static IrisObject smartBoredVariant(IrisObject self) {
SmartBoreVariant cached = self.smartBoreVariant;
if (cached != null && cached.matches(self)) {
return cached.object();
}
self.writeLock.lock();
try {
cached = self.smartBoreVariant;
if (cached != null && cached.matches(self)) {
return cached.object();
}
IrisObject variant = self.copy();
ensureSmartBored(variant);
self.smartBoreVariant = new SmartBoreVariant(self.blocks, self.states,
self.blocks.modificationRevision(), self.states.modificationRevision(),
self.w, self.h, self.d, self.center.getX(), self.center.getY(), self.center.getZ(), variant);
return variant;
} finally {
self.writeLock.unlock();
}
}
record SmartBoreVariant(VectorMap<NativeBlockState> blocks, VectorMap<TileData> states,
long blockRevision, long stateRevision, int width, int height, int depth,
int centerX, int centerY, int centerZ, IrisObject object) {
boolean matches(IrisObject source) {
return blocks == source.blocks && states == source.states
&& blockRevision == source.blocks.modificationRevision()
&& stateRevision == source.states.modificationRevision()
&& width == source.w && height == source.h && depth == source.d
&& centerX == source.center.getX() && centerY == source.center.getY()
&& centerZ == source.center.getZ();
}
}
static void ensureSmartBored(IrisObject self) {
if (self.smartBored) {
return;
@@ -0,0 +1,52 @@
package art.arcane.iris.generation.biome;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.volmlib.util.collection.KList;
import org.junit.After;
import org.junit.Test;
import static org.junit.Assert.assertNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class FloatingIslandMemoContextTest {
@After
public void clearMemo() {
FloatingIslandSample.clearThreadCaches();
}
@Test
public void rejectedColumnsAreCachedOnlyForTheirParentAndContext() {
FloatingIslandSample.clearThreadCaches();
IrisBiome first = parent();
IrisBiome second = parent();
IrisData data = mock(IrisData.class);
Engine engine = mock(Engine.class);
FloatingIslandBoundarySampler boundary = new FloatingIslandBoundarySampler((x, z) -> first);
sample(first, 320, 7L, data, engine, boundary);
sample(first, 320, 7L, data, engine, boundary);
sample(second, 320, 7L, data, engine, boundary);
verify(first, times(1)).getFloatingChildBiomes();
verify(second, times(1)).getFloatingChildBiomes();
sample(first, 320, 9L, data, engine, boundary);
sample(first, 128, 9L, data, engine, boundary);
sample(first, 128, 9L, data, mock(Engine.class), boundary);
sample(first, 128, 9L, mock(IrisData.class), engine, boundary);
sample(first, 128, 9L, data, engine, new FloatingIslandBoundarySampler((x, z) -> first));
verify(first, times(6)).getFloatingChildBiomes();
}
private static void sample(IrisBiome parent, int height, long seed, IrisData data, Engine engine,
FloatingIslandBoundarySampler boundary) {
assertNull(FloatingIslandSample.sampleMemoized(parent, -16, 32, height, seed, data, engine, boundary));
}
private static IrisBiome parent() {
IrisBiome parent = mock(IrisBiome.class);
when(parent.getFloatingChildBiomes()).thenReturn(new KList<>());
return parent;
}
}
@@ -0,0 +1,84 @@
package art.arcane.iris.generation.biome;
import art.arcane.iris.generation.noise.NoiseStyle;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.noise.CNG;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertSame;
import static org.mockito.Mockito.mock;
public class IrisBiomeFieldSeedCohesionTest {
@Test
public void childrenGeneratorsSeparateSeedsSignaturesAndScales() {
IrisData data = mock(IrisData.class);
IrisBiome shared = biome(data);
for (long seed : new long[]{29L, 11L, 29L}) {
for (int signature : new int[]{2137, 91}) {
for (double scale : new double[]{4D, 0.25D}) {
assertArrayEquals(samples(biome(data).getChildrenGenerator(new RNG(seed), signature, scale)),
samples(shared.getChildrenGenerator(new RNG(seed), signature, scale)), 0D);
}
}
}
CNG first = shared.getChildrenGenerator(new RNG(29L), 2137, 4D);
assertSame(first, shared.getChildrenGenerator(new RNG(29L), 2137, 4D));
}
@Test(timeout = 15000)
public void concurrentFloatingFieldsAndEvictionRetainSeedAssignments() throws Exception {
IrisData data = mock(IrisData.class);
IrisFloatingChildBiomes shared = new IrisFloatingChildBiomes();
ExecutorService workers = Executors.newFixedThreadPool(4);
try {
List<Future<double[]>> results = new ArrayList<>(16);
for (int index = 0; index < 16; index++) {
long seed = index;
results.add(workers.submit(() -> floatingSamples(shared, seed, data)));
}
for (int index = 0; index < results.size(); index++) {
double[] expected = floatingSamples(new IrisFloatingChildBiomes(), index, data);
assertArrayEquals(expected, results.get(index).get(5L, TimeUnit.SECONDS), 0D);
assertArrayEquals(expected, floatingSamples(shared, index, data), 0D);
}
} finally {
workers.shutdownNow();
}
}
private static IrisBiome biome(IrisData data) {
IrisBiome biome = new IrisBiome().setChildStyle(NoiseStyle.SIMPLEX.style());
biome.setLoader(data);
return biome;
}
private static double[] floatingSamples(IrisFloatingChildBiomes entry, long seed, IrisData data) {
double x = -71.25D;
double z = 145.75D;
return new double[]{
entry.getFootprintCng(seed, data).noise(x, z),
entry.getPickerCng(seed, data).noise(x, z),
entry.getAltitudeCng(seed, data).noise(x, z),
entry.getTopShapeCng(seed, data).noise(x, z),
entry.getBottomCng(seed, data).noise(x, z),
entry.getEdgeTaperProfile(seed, data).fade(4, -71, 145)
};
}
private static double[] samples(CNG generator) {
double[] sampled = new double[16];
for (int index = 0; index < sampled.length; index++) {
sampled[index] = generator.noise(index * 23.25D - 128D, index * -19.75D + 64D);
}
return sampled;
}
}
@@ -0,0 +1,102 @@
package art.arcane.iris.generation.block;
import art.arcane.iris.generation.geometry.IrisBlockVector;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public class VectorMapDeterminismTest {
@Test
public void collisionBucketsAndChunkBoundariesHaveCanonicalOrder() {
List<IrisBlockVector> positions = new ArrayList<>();
for (int chunk = -2; chunk <= 2; chunk++) {
for (int index = 0; index < 16; index++) {
positions.add(new IrisBlockVector((chunk * 1024) + (index * 64), chunk - 1, index & 1));
}
}
List<String> expected = null;
for (int fixture = 0; fixture < 64; fixture++) {
Collections.shuffle(positions, new Random(fixture));
VectorMap<Integer> map = new VectorMap<>();
for (IrisBlockVector position : positions) {
map.put(position, position.getBlockX());
}
List<String> observed = walk(map);
if (expected == null) {
expected = observed;
}
assertEquals("fixture " + fixture, expected, observed);
assertEquals(observed, walk(map));
}
}
@Test
public void cachedCursorReflectsReplacementInsertionRemovalAndClear() {
VectorMap<String> map = new VectorMap<>();
IrisBlockVector origin = new IrisBlockVector(0, 0, 0);
map.put(origin, "before");
assertEquals(List.of("0:0:0:before"), walk(map));
map.put(origin, "after");
assertEquals(List.of("0:0:0:after"), walk(map));
map.computeIfAbsent(new IrisBlockVector(-1, 0, 0), position -> "added");
assertEquals(List.of("-1:0:0:added", "0:0:0:after"), walk(map));
map.remove(origin);
assertEquals(List.of("-1:0:0:added"), walk(map));
map.clear();
assertTrue(walk(map).isEmpty());
map.put(origin, "fresh");
assertEquals(List.of("0:0:0:fresh"), walk(map));
}
@Test
public void returnedMutablePositionsCannotCorruptCacheOrIteratorRemoval() {
VectorMap<String> map = new VectorMap<>();
map.put(new IrisBlockVector(0, 0, 0), "first");
map.put(new IrisBlockVector(1, 0, 0), "second");
Iterator<IrisBlockVector> keys = map.keys();
keys.next().setX(200);
keys.remove();
assertFalse(map.containsKey(new IrisBlockVector(0, 0, 0)));
assertEquals(List.of("1:0:0:second"), walk(map));
Iterator<Map.Entry<IrisBlockVector, String>> entries = map.iterator();
entries.next().getKey().setX(300);
entries.remove();
assertTrue(map.isEmpty());
}
@Test
public void warmedCursorReusesItsPositionAndDoesNotExposeSnapshotKeys() {
VectorMap<String> map = new VectorMap<>();
map.put(new IrisBlockVector(0, 0, 0), "first");
map.put(new IrisBlockVector(1, 0, 0), "second");
VectorMap<String>.Cursor cursor = map.cursor();
assertTrue(cursor.next());
IrisBlockVector first = cursor.key();
first.setX(100);
assertTrue(cursor.next());
assertSame(first, cursor.key());
assertEquals(1, cursor.key().getBlockX());
assertEquals(List.of("0:0:0:first", "1:0:0:second"), walk(map));
}
private static <T> List<String> walk(VectorMap<T> map) {
List<String> output = new ArrayList<>();
VectorMap<T>.Cursor cursor = map.cursor();
while (cursor.next()) {
IrisBlockVector position = cursor.key();
output.add(position.getBlockX() + ":" + position.getBlockY() + ":" + position.getBlockZ()
+ ":" + cursor.value());
}
return output;
}
}
@@ -0,0 +1,148 @@
package art.arcane.iris.generation.decoration;
import art.arcane.iris.generation.biome.IrisBiome;
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.GenerationCacheWarmer;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.testsupport.KeyedBlockState;
import art.arcane.iris.testsupport.RunningEnginePackData;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.hunk.Hunk;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisDecoratorSeedCohesionTest {
private static final long[] SEEDS = {101L, -73L, 90181L};
private static final int COLUMNS = 96;
@Test
public void decorationKeepsPinnedOutputAcrossColumnOrderAndWarmup() {
long[] expected = {151633654196388515L, -2981136988801910995L, -8591254808024440278L};
long[] actual = new long[SEEDS.length];
for (int i = 0; i < SEEDS.length; i++) {
Fixture cold = new Fixture(SEEDS[i], decorator(), RunningEnginePackData.create());
long[] baseline = cold.generate(false);
Fixture reverse = new Fixture(SEEDS[i], decorator(), RunningEnginePackData.create());
Fixture warm = new Fixture(SEEDS[i], decorator(), RunningEnginePackData.create());
GenerationCacheWarmer.warm(warm.engine);
assertArrayEquals(baseline, reverse.generate(true));
assertArrayEquals(baseline, warm.generate(true));
actual[i] = hash(baseline);
}
assertEquals(Arrays.toString(expected), Arrays.toString(actual));
}
@Test
public void concurrentFirstUseKeepsIndependentSeedResults() throws Exception {
IrisData data = RunningEnginePackData.create();
IrisDecorator shared = decorator();
List<long[]> expected = new ArrayList<>();
for (long seed : SEEDS) {
expected.add(new Fixture(seed, decorator(), data).generate(false));
}
ExecutorService executor = Executors.newFixedThreadPool(6);
try {
List<Callable<long[]>> tasks = new ArrayList<>();
for (int round = 0; round < 24; round++) {
long seed = SEEDS[round % SEEDS.length];
tasks.add(() -> new Fixture(seed, shared, data).generate(true));
}
List<Future<long[]>> futures = executor.invokeAll(tasks, 30, TimeUnit.SECONDS);
for (int i = 0; i < futures.size(); i++) {
assertArrayEquals("task=" + i, expected.get(i % SEEDS.length), futures.get(i).get());
}
} finally {
executor.shutdownNow();
executor.awaitTermination(10, TimeUnit.SECONDS);
}
}
private static IrisDecorator decorator() {
KList<NativeBlockState> body = new KList<>(
new KeyedBlockState("minecraft:stone"), new KeyedBlockState("minecraft:dirt"));
KList<NativeBlockState> top = new KList<>(
new KeyedBlockState("minecraft:granite"), new KeyedBlockState("minecraft:andesite"));
IrisDecorator decorator = new IrisDecorator() {
@Override
public KList<NativeBlockState> getBlockData(IrisData data) {
return body;
}
@Override
public KList<NativeBlockState> getBlockDataTops(IrisData data) {
return top;
}
};
return decorator.setPartOf(IrisDecorationPart.SEA_FLOOR).setChance(0.61D).setForcePlace(true)
.setStackMin(2).setStackMax(7)
.setStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).setZoom(7D))
.setVariance(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).setZoom(4D))
.setHeightVariance(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).setZoom(9D));
}
private static long hash(long[] values) {
long hash = 0xcbf29ce484222325L;
for (long value : values) {
hash = (hash ^ value) * 0x100000001b3L;
}
return hash;
}
private static final class Fixture {
private final Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS);
private final IrisBiome biome = mock(IrisBiome.class);
private final IrisSeaFloorDecorator placement;
private Fixture(long seed, IrisDecorator decorator, IrisData data) {
when(engine.getData()).thenReturn(data);
when(engine.getHeight()).thenReturn(32);
when(engine.getSeedManager().getComponent()).thenReturn(seed);
when(engine.getAllBiomes()).thenReturn(new KList<>(biome));
when(engine.getDimension().getAllRegions(engine)).thenReturn(new KList<>());
when(engine.getDimension().getOres()).thenReturn(new KList<>());
when(biome.getLoadKey()).thenReturn("fixture");
when(biome.getObjects()).thenReturn(new KList<>());
when(biome.getOres()).thenReturn(new KList<>());
when(biome.getProceduralObjects()).thenReturn(null);
when(biome.getDecorators()).thenReturn(new KList<>(decorator));
when(biome.getDecoratorBucket(IrisDecorationPart.SEA_FLOOR)).thenReturn(new IrisDecorator[]{decorator});
placement = new IrisSeaFloorDecorator(engine);
}
private long[] generate(boolean reverse) {
long[] output = new long[COLUMNS];
for (int index = 0; index < COLUMNS; index++) {
int column = reverse ? COLUMNS - index - 1 : index;
Hunk<NativeBlockState> hunk = Hunk.newArrayHunk(1, 32, 1);
int x = column * 29 - 1312;
int z = column * 17 - 781;
placement.decorate(0, 0, x, x + 1, x - 1, z, z + 1, z - 1, hunk, biome, 4, 32);
long hash = 0xcbf29ce484222325L;
for (int y = 0; y < 32; y++) {
NativeBlockState block = hunk.get(0, y, 0);
hash = (hash ^ (block == null ? 0 : block.key().hashCode())) * 0x100000001b3L;
}
output[column] = hash;
}
return output;
}
}
}
@@ -0,0 +1,109 @@
package art.arcane.iris.generation.decoration;
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
import art.arcane.iris.generation.noise.NoiseStyle;
import art.arcane.iris.generation.terrain.IrisMaterialPalette;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.pack.value.IrisRange;
import art.arcane.iris.testsupport.KeyedBlockState;
import art.arcane.iris.testsupport.RunningEnginePackData;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
public class IrisOreSeedCohesionTest {
private static final long[] SEEDS = {101L, -73L, 90181L};
private static final int SAMPLES = 4096;
@Test
public void oreFieldsKeepPinnedOutputAcrossSeedOrderAndWarmup() {
IrisData data = RunningEnginePackData.create();
IrisOreGenerator shared = generator();
long[] actual = new long[SEEDS.length];
for (int i = SEEDS.length - 1; i >= 0; i--) {
IrisOreGenerator cold = generator();
long[] expected = sample(cold, new RNG(SEEDS[i]), data, false);
shared.warm(new RNG(SEEDS[(i + 1) % SEEDS.length]), data);
assertArrayEquals(expected, sample(shared, new RNG(SEEDS[i]), data, true));
RNG consumed = new RNG(SEEDS[i]);
for (int draw = 0; draw < 128; draw++) {
consumed.nextLong();
}
assertArrayEquals(expected, sample(generator(), consumed, data, true));
actual[i] = hash(expected);
}
assertEquals(Arrays.toString(new long[]{8469391283002463309L, -3738838309875838243L, 8458758241968211373L}), Arrays.toString(actual));
}
@Test
public void concurrentOreFirstUseKeepsSeedsIndependent() throws Exception {
IrisData data = RunningEnginePackData.create();
IrisOreGenerator shared = generator();
List<long[]> expected = new ArrayList<>();
for (long seed : SEEDS) {
expected.add(sample(generator(), new RNG(seed), data, false));
}
ExecutorService executor = Executors.newFixedThreadPool(6);
try {
List<Callable<long[]>> tasks = new ArrayList<>();
for (int index = 0; index < 18; index++) {
long seed = SEEDS[index % SEEDS.length];
tasks.add(() -> sample(shared, new RNG(seed), data, true));
}
List<Future<long[]>> futures = executor.invokeAll(tasks, 30, TimeUnit.SECONDS);
for (int index = 0; index < futures.size(); index++) {
assertArrayEquals(expected.get(index % SEEDS.length), futures.get(index).get());
}
} finally {
executor.shutdownNow();
executor.awaitTermination(10, TimeUnit.SECONDS);
}
}
private static IrisOreGenerator generator() {
KList<NativeBlockState> states = new KList<>(
new KeyedBlockState("minecraft:iron_ore"), new KeyedBlockState("minecraft:gold_ore"));
IrisMaterialPalette palette = new IrisMaterialPalette() {
@Override
public KList<NativeBlockState> getBlockData(IrisData data) {
return states;
}
};
palette.setStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX));
return new IrisOreGenerator().setPalette(palette).setThreshold(0.57D)
.setRange(new IrisRange(0D, 256D))
.setChanceStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX));
}
private static long[] sample(IrisOreGenerator generator, RNG rng, IrisData data, boolean reverse) {
long[] output = new long[SAMPLES];
for (int index = 0; index < SAMPLES; index++) {
int sample = reverse ? SAMPLES - index - 1 : index;
NativeBlockState block = generator.generate(sample * 13 - 901,
sample % 257, sample * 7 - 713, rng, data);
output[sample] = block == null ? 0L : block.key().hashCode();
}
return output;
}
private static long hash(long[] values) {
long hash = 0xcbf29ce484222325L;
for (long value : values) {
hash = (hash ^ value) * 0x100000001b3L;
}
return hash;
}
}
@@ -4,11 +4,75 @@ import art.arcane.volmlib.util.collection.KList;
import org.junit.Test;
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
public class TreeDecoratorApplierTest {
@Test
public void allDecoratorTargetsKeepPinnedOutputAcrossHashCollisionInsertionOrders() {
long[] seeds = {101L, -73L, 90181L};
long[] actual = new long[seeds.length];
for (int i = 0; i < seeds.length; i++) {
long hash = 0xcbf29ce484222325L;
for (IrisTreeDecoratorTarget target : IrisTreeDecoratorTarget.values()) {
TreeBlockCanvas forward = collisionCanvas(false);
TreeBlockCanvas reverse = collisionCanvas(true);
IrisProceduralTree tree = surfaceTree(target, true);
tree.getDecorators().getFirst().setChance(0.41D).setLength(4);
TreeDecoratorApplier.apply(forward, tree, seeds[i], List.of());
TreeDecoratorApplier.apply(reverse, tree, seeds[i], List.of());
assertEquals(target + ":" + seeds[i], forward.getCells(), reverse.getCells());
List<TreeBlockCanvas.Vec> positions = new ArrayList<>(forward.getCells().keySet());
positions.sort(null);
for (TreeBlockCanvas.Vec position : positions) {
TreeBlockCanvas.Cell cell = forward.getCells().get(position);
hash = (hash ^ position.hashCode()) * 0x100000001b3L;
hash = (hash ^ cell.role().ordinal()) * 0x100000001b3L;
hash = (hash ^ cell.decoratorIndex()) * 0x100000001b3L;
hash = (hash ^ (cell.facing() == null ? 0 : cell.facing().hashCode())) * 0x100000001b3L;
}
}
actual[i] = hash;
}
assertEquals(Arrays.toString(new long[]{4350422081939262412L, 3647024406982151043L, 7110665103288366817L}), Arrays.toString(actual));
}
@Test
public void supportTendrilsChooseTheSameEqualDistanceEndpointsAcrossInsertionOrders() {
TreeBlockCanvas forward = supportCanvas(false);
TreeBlockCanvas reverse = supportCanvas(true);
TreeSupport.ensureLeavesSupported(forward, 4);
TreeSupport.ensureLeavesSupported(reverse, 4);
assertEquals(forward.getCells(), reverse.getCells());
}
private static TreeBlockCanvas collisionCanvas(boolean reverse) {
TreeBlockCanvas canvas = new TreeBlockCanvas();
for (int index = 0; index < 24; index++) {
int position = reverse ? 23 - index : index;
int x = position - 12;
int y = -31 * x;
canvas.setTrunk(x, y, 0, TreeBlockCanvas.Role.TRUNK, TreeBlockCanvas.Axis.Y);
canvas.setLeaf(x, y, 1, TreeBlockCanvas.Role.LEAF);
}
return canvas;
}
private static TreeBlockCanvas supportCanvas(boolean reverse) {
TreeBlockCanvas canvas = new TreeBlockCanvas();
int[] coordinates = reverse ? new int[]{3, -3} : new int[]{-3, 3};
for (int x : coordinates) {
canvas.setTrunk(x, 0, 0, TreeBlockCanvas.Role.TRUNK, TreeBlockCanvas.Axis.Y);
canvas.setLeaf(x * 4, 0, 2, TreeBlockCanvas.Role.LEAF);
canvas.setLeaf(x * 4, 0, -2, TreeBlockCanvas.Role.LEAF);
}
return canvas;
}
@Test
public void trunkAttachmentsFaceAwayFromTheirWoodSupport() {
TreeBlockCanvas canvas = new TreeBlockCanvas();
@@ -0,0 +1,101 @@
package art.arcane.iris.generation.noise;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.runtime.IrisEngineStreamType;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.stream.ProceduralStream;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisExpressionSeedCohesionTest {
@Test
public void expressionStreamsSeparateSeedsAndInitializationOrder() {
IrisData data = mock(IrisData.class);
IrisExpression expression = expression(data);
double first = expression(data).evaluate(new RNG(17L), 23.5D, -94.75D);
double second = expression(data).evaluate(new RNG(53L), 23.5D, -94.75D);
ProceduralStream<Double> secondStream = expression.stream(new RNG(53L));
ProceduralStream<Double> firstStream = expression.stream(new RNG(17L));
assertEquals(second, secondStream.get(23.5D, -94.75D), 0D);
assertEquals(first, firstStream.get(23.5D, -94.75D), 0D);
assertSame(firstStream, expression.stream(new RNG(17L)));
}
@Test
public void expressionFunctionTracksEngineIdentity() {
Engine firstEngine = engine(17D);
Engine secondEngine = engine(53D);
AtomicReference<Engine> active = new AtomicReference<>(firstEngine);
IrisData data = mock(IrisData.class);
when(data.getEngine()).thenAnswer(ignored -> active.get());
IrisExpressionFunction function = new IrisExpressionFunction()
.setName("height")
.setData(data)
.setEngineStreamValue(IrisEngineStreamType.HEIGHT);
IrisExpressionFunction.FunctionContext context = new IrisExpressionFunction.FunctionContext(new RNG(91L));
assertEquals(17D, function.eval(context, 4D, -8D), 0D);
active.set(secondEngine);
assertEquals(53D, function.eval(context, 4D, -8D), 0D);
active.set(firstEngine);
assertEquals(17D, function.eval(context, 4D, -8D), 0D);
}
@Test(timeout = 15000)
public void concurrentFunctionInitializationAndEvictionRetainSeedSamples() throws Exception {
IrisData data = mock(IrisData.class);
IrisExpressionFunction shared = function(data);
ExecutorService workers = Executors.newFixedThreadPool(4);
try {
List<Future<Double>> results = new ArrayList<>(32);
double[] expected = new double[32];
for (int index = 0; index < 32; index++) {
long seed = index;
expected[index] = function(data).eval(new IrisExpressionFunction.FunctionContext(new RNG(seed)), 23.5D, -94.75D);
results.add(workers.submit(() -> shared.eval(
new IrisExpressionFunction.FunctionContext(new RNG(seed)), 23.5D, -94.75D)));
}
for (int index = 0; index < results.size(); index++) {
assertEquals(expected[index], results.get(index).get(5L, TimeUnit.SECONDS), 0D);
assertEquals(expected[index], shared.eval(new IrisExpressionFunction.FunctionContext(new RNG(index)), 23.5D, -94.75D), 0D);
}
} finally {
workers.shutdownNow();
}
}
private static IrisExpression expression(IrisData data) {
IrisExpression expression = new IrisExpression()
.setExpression("field(x, z) + value")
.setFunctions(new KList<>(function(data)))
.setVariables(new KList<>(new IrisExpressionLoad().setName("value").setStyleValue(NoiseStyle.PERLIN.style())));
expression.setLoader(data);
return expression;
}
private static IrisExpressionFunction function(IrisData data) {
return new IrisExpressionFunction().setName("field").setStyleValue(NoiseStyle.SIMPLEX.style()).setData(data);
}
private static Engine engine(double height) {
IrisComplex complex = mock(IrisComplex.class);
when(complex.getHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> height));
Engine engine = mock(Engine.class);
when(engine.getComplex()).thenReturn(complex);
return engine;
}
}
@@ -10,6 +10,7 @@ import org.junit.rules.TemporaryFolder;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
@@ -20,6 +21,16 @@ public class IrisGeneratorStyleCacheIsolationTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void persistedCacheSignaturesUseStableStyleNames() {
assertEquals(-492944473, new IrisGeneratorStyle(NoiseStyle.SIMPLEX).prebakeSignature());
assertEquals(992324154, new IrisGeneratorStyle(NoiseStyle.FLAT).prebakeSignature());
assertEquals(-848988881, new IrisGeneratorStyle(NoiseStyle.STATIC).prebakeSignature());
assertEquals(990163649, new IrisGeneratorStyle().setStyle(null).prebakeSignature());
assertEquals(1067243926, new IrisGeneratorStyle(NoiseStyle.SIMPLEX)
.setFracture(new IrisGeneratorStyle(NoiseStyle.STATIC)).prebakeSignature());
}
@Test
public void cacheSeparatesSeedsAndEngineIdentity() throws Exception {
Engine firstEngine = mock(Engine.class);
@@ -0,0 +1,81 @@
package art.arcane.iris.generation.noise;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.noise.CNG;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
public class NoiseSeedCohesionTest {
private static final long[] SEEDS = {813753L, -935731L, Long.MIN_VALUE};
private static final double[] COORDINATES = {-29_999_983.25D, -513D, -64D, -1D, 0D, 0.125D, 17D, 64D, 8192D, 29_999_983.25D};
@Test
public void allStylesRetainPinnedSamples() {
long hash = 0xcbf29ce484222325L;
for (NoiseStyle style : NoiseStyle.values()) {
for (long seed : SEEDS) {
for (double sample : samples(style.create(new RNG(seed)), false)) {
hash = (hash ^ Double.doubleToLongBits(sample)) * 0x100000001b3L;
}
}
}
assertEquals("All noise style samples", 5059606393854142913L, hash);
}
@Test(timeout = 30000)
public void allStylesIgnoreSamplingOrderAndConcurrentReaders() throws Exception {
ExecutorService workers = Executors.newFixedThreadPool(4);
try {
for (NoiseStyle style : NoiseStyle.values()) {
CNG generator = style.create(new RNG(SEEDS[0]));
double[] expected = samples(generator, false);
assertArrayEquals(style.name(), expected, samples(generator, true), 0D);
List<Future<double[]>> results = new ArrayList<>(4);
for (int index = 0; index < 4; index++) {
boolean reversed = index % 2 == 0;
results.add(workers.submit(() -> samples(generator, reversed)));
}
for (Future<double[]> result : results) {
assertArrayEquals(style.name(), expected, result.get(5L, TimeUnit.SECONDS), 0D);
}
}
} finally {
workers.shutdownNow();
}
}
@Test
public void generatorCacheEvictionRetainsSamples() {
for (NoiseStyle style : NoiseStyle.values()) {
IrisGeneratorStyle configured = style.style();
double[] expected = samples(configured.create(new RNG(SEEDS[0]), null), false);
for (long seed = 0; seed < 9; seed++) {
configured.create(new RNG(seed), null);
}
assertArrayEquals(style.name(), expected,
samples(configured.create(new RNG(SEEDS[0]), null), true), 0D);
}
}
private static double[] samples(CNG generator, boolean reversed) {
double[] sampled = new double[COORDINATES.length * 3];
for (int cursor = 0; cursor < COORDINATES.length; cursor++) {
int index = reversed ? COORDINATES.length - 1 - cursor : cursor;
double x = COORDINATES[index];
double z = x * -0.713D;
sampled[index * 3] = generator.noise(x);
sampled[index * 3 + 1] = generator.noise(x, z);
sampled[index * 3 + 2] = generator.noise(x, 81.25D, z);
}
return sampled;
}
}
@@ -35,6 +35,7 @@ import org.junit.Test;
import org.mockito.MockedStatic;
import art.arcane.iris.pack.loading.ResourceLoader;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
@@ -78,16 +79,16 @@ public class IrisDepositModifierParityTest {
public void configuredShapesKeepRecordedBlocksAcrossScopesAndCavePolicies() {
long[] seeds = {101L, -73L, 90181L};
long[][] expected = {
{-4609692219192155345L, 784L, 2118L, 53L, 67L, 11L},
{-331296667371868787L, 818L, 2413L, 94L, 49L, 20L},
{-3365014205342608518L, 641L, 2340L, 58L, 45L, 27L}
{-681153659952315909L, 784L, 2118L, 53L, 67L, 10L},
{9111006654067022779L, 818L, 2414L, 93L, 49L, 20L},
{831979810025539302L, 641L, 2340L, 58L, 45L, 28L}
};
long[][] actual = new long[seeds.length][];
for (int i = 0; i < seeds.length; i++) {
for (boolean multicore : new boolean[]{false, true}) {
assertArrayEquals("seed=" + seeds[i] + ", multicore=" + multicore,
expected[i], generateConfigured(seeds[i], multicore));
}
actual[i] = generateConfigured(seeds[i], false);
assertArrayEquals("seed=" + seeds[i], actual[i], generateConfigured(seeds[i], true));
}
assertEquals(Arrays.deepToString(expected), Arrays.deepToString(actual));
}
@Test
@@ -104,9 +105,11 @@ public class IrisDepositModifierParityTest {
@Test
public void unmatchableSurfaceBiomeFiltersSkipClumpsWithoutChangingOutput() {
long[] expected = {-4609692219192155345L, 784L, 2118L, 53L, 67L, 11L};
long[] expected = {-681153659952315909L, 784L, 2118L, 53L, 67L, 10L};
AtomicInteger clumps = new AtomicInteger();
assertArrayEquals(expected, generateConfigured(101L, false, true, false, new AtomicInteger(), clumps));
long[] filtered = generateConfigured(101L, false, true, false, new AtomicInteger(), clumps);
assertArrayEquals(generateConfigured(101L, false), filtered);
assertEquals(Arrays.toString(expected), Arrays.toString(filtered));
assertEquals(0, clumps.get());
}
@@ -0,0 +1,39 @@
package art.arcane.iris.generation.terrain;
import art.arcane.iris.generation.noise.IrisGenerator;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.noise.CNG;
import art.arcane.volmlib.util.noise.CellGenerator;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
public class TerrainFieldSeedCohesionTest {
@Test
public void coordinateFracturesSeparateSeedAndSignature() {
IrisDimension shared = new IrisDimension();
for (long seed : new long[]{91L, 17L, 91L}) {
for (int signature : new int[]{73, 211}) {
CNG expected = new IrisDimension().getCoordFracture(new RNG(seed), signature);
CNG actual = shared.getCoordFracture(new RNG(seed), signature);
for (int index = 0; index < 8; index++) {
assertEquals(expected.noise(index * 128D - 600D, index * -251D + 300D),
actual.noise(index * 128D - 600D, index * -251D + 300D), 0D);
}
}
}
}
@Test
public void cellGeneratorsRetainSeedAssignments() {
IrisGenerator generator = new IrisGenerator();
CellGenerator first = generator.getCellGenerator(17L);
CellGenerator second = generator.getCellGenerator(91L);
assertNotSame(first, second);
assertSame(first, generator.getCellGenerator(17L));
CellGenerator expected = new IrisGenerator().getCellGenerator(91L);
assertEquals(expected.getDistance(17D, -81D), second.getDistance(17D, -81D), 0D);
}
}
@@ -1,9 +1,11 @@
package art.arcane.iris.structure.object;
import art.arcane.iris.testsupport.KeyedBlockState;
import art.arcane.iris.testsupport.PlatformBinding;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.iris.generation.geometry.IrisBlockVector;
import org.junit.Rule;
import org.junit.Test;
import java.io.ByteArrayInputStream;
@@ -13,21 +15,30 @@ import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Random;
import java.util.HashSet;
import java.util.HexFormat;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
public class IrisObjectIoPaletteTest {
private static final int BLOCK_COUNT = 30_000;
private static final int UNIQUE_KEYS = 3_000;
private static final String PINNED_DIGEST = "9fbd42fe7686cc725e0acab8595b0223d71c1a45762b78ac0e6146230dbb8849";
private static final String PINNED_DIGEST = "eee0bae5702dc3d8c79b5ba28d0782cdfa581662237629bb13a3adee6f757fc7";
@Rule
public final PlatformBinding platform = PlatformBinding.mockPlatform();
@Test
public void writesTheSameBytesAsTheOrderedPaletteScan() throws IOException {
public void pinsCanonicalPaletteAndBlockOrder() throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
IrisObjectIO.write(representativeObject(), out);
@@ -36,7 +47,7 @@ public class IrisObjectIoPaletteTest {
}
@Test
public void paletteKeepsFirstSeenInsertionOrder() throws IOException {
public void paletteKeepsFirstSeenCanonicalTraversalOrder() throws IOException {
IrisObject object = representativeObject();
List<String> firstSeen = new ArrayList<>();
Set<String> seen = new HashSet<>();
@@ -53,6 +64,38 @@ public class IrisObjectIoPaletteTest {
assertEquals(firstSeen, writtenPalette(out.toByteArray()));
}
@Test
public void insertionPermutationsWriteIdenticalBytesAndReadBackEveryBlock() throws Throwable {
when(platform.registries().blockOrNull(anyString(), eq(false)))
.thenAnswer(invocation -> new KeyedBlockState(invocation.getArgument(0)));
IrisObject source = representativeObject();
ByteArrayOutputStream canonical = new ByteArrayOutputStream();
IrisObjectIO.write(source, canonical);
byte[] expected = canonical.toByteArray();
List<Map.Entry<IrisBlockVector, NativeBlockState>> blocks = new ArrayList<>();
source.blocks.forEach((position, state) -> blocks.add(Map.entry(position, state)));
for (int fixture = 0; fixture < 8; fixture++) {
Collections.shuffle(blocks, new Random(fixture));
IrisObject permuted = new IrisObject(source.w, source.h, source.d);
for (Map.Entry<IrisBlockVector, NativeBlockState> entry : blocks) {
permuted.blocks.put(entry.getKey(), entry.getValue());
}
ByteArrayOutputStream encoded = new ByteArrayOutputStream();
IrisObjectIO.write(permuted, encoded);
assertArrayEquals("fixture " + fixture, expected, encoded.toByteArray());
IrisObject decoded = new IrisObject();
IrisObjectIO.read(decoded, new ByteArrayInputStream(encoded.toByteArray()));
assertEquals(source.w, decoded.w);
assertEquals(source.h, decoded.h);
assertEquals(source.d, decoded.d);
assertEquals(source.blocks.size(), decoded.blocks.size());
assertEquals(source.states.size(), decoded.states.size());
for (Map.Entry<IrisBlockVector, NativeBlockState> entry : blocks) {
assertEquals(entry.getValue().key(), decoded.blocks.get(entry.getKey()).key());
}
}
}
private static List<String> writtenPalette(byte[] bytes) throws IOException {
DataInputStream din = new DataInputStream(new ByteArrayInputStream(bytes));
din.readInt();
@@ -1,6 +1,7 @@
package art.arcane.iris.structure.object;
import art.arcane.iris.generation.block.TileData;
import art.arcane.iris.generation.geometry.IrisBlockVector;
import art.arcane.iris.generation.decoration.IrisStiltSettings;
import art.arcane.iris.generation.block.IrisBlockData;
import art.arcane.iris.generation.decoration.formation.FormationGenerator;
@@ -32,6 +33,8 @@ import org.junit.ClassRule;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Random;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -218,6 +221,63 @@ public class IrisObjectPlacementRunnerRegressionTest {
assertEquals(ANCHOR_Y, snowWrites.get(0).y());
}
@Test
public void smartBorePlacementLeavesSharedObjectUnchangedForPlainPlacements() {
IrisObject object = new IrisObject(3, 1, 1);
object.setLoader(data);
object.setUnsigned(0, 0, 0, solid);
object.setUnsigned(2, 0, 0, solid);
RecordingPlacer before = new RecordingPlacer(null);
RecordingPlacer bored = new RecordingPlacer(null);
RecordingPlacer after = new RecordingPlacer(null);
IrisObjectPlacement plain = placement().setMode(ObjectPlaceMode.STRUCTURE_PIECE);
object.place(0, ANCHOR_Y, 0, before, plain, new RNG(12L), data);
object.place(0, ANCHOR_Y, 0, bored, plain.toPlacement().setSmartBore(true), new RNG(12L), data);
object.place(0, ANCHOR_Y, 0, after, plain, new RNG(12L), data);
assertEquals(before.writes(), after.writes());
assertEquals(2, object.getBlocks().size());
assertEquals(3, bored.writes().size());
assertFalse(object.isSmartBored());
}
@Test
public void randomizedEditsAndSnowIgnoreHashCollisionInsertionOrder() {
NativeBlockState replacement = state("minecraft:calcite", true);
IrisObjectReplace edit = mock(IrisObjectReplace.class);
when(edit.getChance()).thenReturn(0.45F);
when(edit.getFind(data)).thenReturn(new KList<>(solid));
when(edit.getReplace(any(RNG.class), anyDouble(), anyDouble(), anyDouble(), any(IrisData.class)))
.thenReturn(replacement);
IrisMaterialPalette palette = mock(IrisMaterialPalette.class);
when(palette.getTile(any(RNG.class), anyDouble(), anyDouble(), anyDouble(), any(IrisData.class)))
.thenReturn(Optional.empty());
when(edit.getReplace()).thenReturn(palette);
IrisObjectPlacement placement = placement().setMode(ObjectPlaceMode.STRUCTURE_PIECE)
.setSnow(0.85D).setEdit(new KList<>(edit));
List<Integer> insertionOrder = new ArrayList<>();
for (int index = 0; index < 16; index++) {
insertionOrder.add(index);
}
Map<String, NativeBlockState> expected = null;
for (int fixture = 0; fixture < 64; fixture++) {
Collections.shuffle(insertionOrder, new Random(fixture));
IrisObject object = new IrisObject(1024, 1, 1);
for (int index : insertionOrder) {
object.getBlocks().put(new IrisBlockVector(index * 64, 0, 0), solid);
}
RecordingPlacer placer = new RecordingPlacer(null);
object.place(0, ANCHOR_Y, 0, placer, placement, new RNG(123456L), data);
if (expected == null) {
expected = new HashMap<>(placer.world);
}
assertEquals("fixture " + fixture, expected, placer.world);
}
assertTrue(expected.containsValue(replacement));
assertTrue(expected.containsValue(solid));
}
@Test
public void debugPlacementDoesNotMutateSmartBoreCache() {
IrisObject object = new IrisObject(1, 1, 1);
@@ -18,6 +18,8 @@ import java.util.Map;
import java.util.Random;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyString;
@@ -94,6 +96,27 @@ public class IrisObjectShapingTest {
assertTrue(object.getBlocks().isEmpty());
}
@Test
public void smartBoreVariantIsReusedAndInvalidatedBySourceEdits() {
IrisObject source = new IrisObject(3, 1, 1);
source.getBlocks().put(new IrisBlockVector(-1, 0, 0), IrisObject.States.stone());
source.getBlocks().put(new IrisBlockVector(1, 0, 0), IrisObject.States.stone());
long sourceRevision = source.getBlocks().modificationRevision();
IrisObject first = IrisObjectShaping.smartBoredVariant(source);
assertFalse(source.isSmartBored());
assertEquals(sourceRevision, source.getBlocks().modificationRevision());
assertEquals(2, source.getBlocks().size());
assertEquals(3, first.getBlocks().size());
assertSame(first, IrisObjectShaping.smartBoredVariant(source));
source.getBlocks().put(new IrisBlockVector(0, 0, 0), IrisObject.States.air());
IrisObject second = IrisObjectShaping.smartBoredVariant(source);
assertNotSame(first, second);
assertSame(IrisObject.States.air(), second.getBlocks().get(new IrisBlockVector(0, 0, 0)));
assertSame(second, IrisObjectShaping.smartBoredVariant(source));
}
private static void closeAxis(Map<Cell, NativeBlockState> blocks, int axis) {
List<Cell> occupied = new ArrayList<>(blocks.keySet());
for (Cell start : occupied) {
@@ -326,7 +326,7 @@ public final class GenerationOrderProbe {
);
}
private static ProbeResult run(ProbeConfiguration configuration) throws Exception {
static ProbeResult run(ProbeConfiguration configuration) throws Exception {
System.out.println(LOG_PREFIX + " pack: " + configuration.packSource().getAbsolutePath());
System.out.println(LOG_PREFIX + " dimension: " + configuration.dimensionKey());
System.out.println(LOG_PREFIX + " seed: " + configuration.seed());
@@ -537,7 +537,7 @@ final class RealPackProbeSupport {
private static File clonePack(File source, File workRoot) throws Exception {
File destination = new File(workRoot, "pack");
if (System.getProperty("os.name").startsWith("Mac")) {
Process clone = new ProcessBuilder("/bin/cp", "-Rc", source.getAbsolutePath(), destination.getAbsolutePath())
Process clone = new ProcessBuilder("/bin/cp", "-RLc", source.getAbsolutePath(), destination.getAbsolutePath())
.inheritIO().start();
if (clone.waitFor() == 0) {
return destination;
@@ -0,0 +1,74 @@
package art.arcane.iris.probe;
import art.arcane.iris.testsupport.IrisRuntimeState;
import org.junit.AfterClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.File;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
public final class SeedCohesionProbeTest {
@Rule
public TemporaryFolder temporary = new TemporaryFolder();
@AfterClass
public static void releaseRuntime() {
IrisRuntimeState.reset();
}
@Test
public void linkedPackInputsAreCopiedIntoAnIndependentWorkspace() throws Exception {
URL resource = SeedCohesionProbeTest.class.getResource("/seed-cohesion");
assertNotNull(resource);
Path source = Path.of(resource.toURI());
Path link = temporary.getRoot().toPath().resolve("linked-pack");
Files.createSymbolicLink(link, source);
try (RealPackProbeSupport.Workspace workspace = RealPackProbeSupport.openWorkspace(
link.toFile(), "cohesion", "[seed-cohesion-copy]")) {
Path copy = workspace.pack().toPath();
assertFalse(Files.isSymbolicLink(copy));
assertFalse(Files.isSameFile(source, copy));
assertEquals(Files.readString(source.resolve("dimensions/cohesion.json")),
Files.readString(copy.resolve("dimensions/cohesion.json")));
}
}
@Test(timeout = 180_000L)
public void completeChunksRemainStableAcrossSeedOrderThreadsAndRestarts() throws Exception {
GenerationOrderProbe.AggregateSignature first = generate(1337L, false);
GenerationOrderProbe.AggregateSignature parallel = generate(1337L, true);
GenerationOrderProbe.AggregateSignature alternate = generate(-8675309L, true);
GenerationOrderProbe.AggregateSignature restarted = generate(1337L, true);
assertEquals(first, parallel);
assertEquals(first, restarted);
assertNotEquals(first.blocks(), alternate.blocks());
assertEquals(new GenerationOrderProbe.AggregateSignature(
"3ddfe89b98273ec609574918ac461bdd0e15365a2840a8ab73285818981feb74",
"0f54e6504dc0b66b3c0d9b9cc4406a07a63d14e5eba4c30db7c3d7f74a5d61c7",
"9bcf2d354c6f17849046153c25beeb3ce08b81d3409037d2968f28d30d832253"), first);
assertEquals(new GenerationOrderProbe.AggregateSignature(
"0ad13dd2ea0d3eb467f1fded74403f7efb5245a3a06799b39c1d1d17dd32b926",
"0f54e6504dc0b66b3c0d9b9cc4406a07a63d14e5eba4c30db7c3d7f74a5d61c7",
"e4f6fb0c86cae9be76a1e957156e42a762b4d75458fb1483fe856bda1cfd3a1e"), alternate);
}
private static GenerationOrderProbe.AggregateSignature generate(long seed, boolean multicore) throws Exception {
URL resource = SeedCohesionProbeTest.class.getResource("/seed-cohesion");
assertNotNull(resource);
File pack = new File(resource.toURI());
GenerationOrderProbe.ProbeConfiguration configuration = new GenerationOrderProbe.ProbeConfiguration(
pack, "cohesion", seed, -1, 1, -1, 1, 4, 91L, multicore, false);
GenerationOrderProbe.ProbeResult result = GenerationOrderProbe.run(configuration);
assertEquals("PASS", result.status());
return result.signature();
}
}
@@ -0,0 +1,18 @@
{
"name": "Terrain",
"derivative": "minecraft:plains",
"vanillaDerivative": "minecraft:plains",
"layers": [{"palette": [{"block": "minecraft:stone"}]}],
"generators": [{"generator": "terrain", "min": 20, "max": 30}],
"decorators": [{
"chance": 0.61,
"style": {"style": "SIMPLEX", "zoom": 7},
"variance": {"style": "SIMPLEX", "zoom": 4},
"heightVariance": {"style": "SIMPLEX", "zoom": 9},
"forcePlace": true,
"stackMin": 1,
"stackMax": 5,
"palette": [{"block": "minecraft:stone"}, {"block": "minecraft:dirt"}],
"topPalette": [{"block": "minecraft:granite"}, {"block": "minecraft:andesite"}]
}]
}
@@ -0,0 +1,11 @@
{
"name": "Seed cohesion fixture",
"version": 1,
"mode": {"type": "OVERWORLD"},
"regions": ["terrain"],
"environment": "NORMAL",
"dimensionHeight": {"min": 0, "max": 64},
"logicalHeight": 64,
"fluidHeight": 10,
"caveProfile": {"enabled": false}
}
@@ -0,0 +1,5 @@
{
"seed": 310,
"interpolator": {"function": "NONE", "horizontalScale": 1},
"composite": [{"seed": 310, "style": {"style": "SIMPLEX", "zoom": 19}}]
}
@@ -0,0 +1,6 @@
{
"name": "Terrain",
"landBiomes": ["terrain"],
"seaBiomes": ["terrain"],
"shoreBiomes": ["terrain"]
}