mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-11 19:14:11 +00:00
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:
@@ -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;
|
||||
}
|
||||
|
||||
+4
-4
@@ -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;
|
||||
}
|
||||
|
||||
+5
-5
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+3
@@ -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
-8
@@ -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;
|
||||
|
||||
+52
@@ -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;
|
||||
}
|
||||
}
|
||||
+84
@@ -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;
|
||||
}
|
||||
}
|
||||
+148
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+109
@@ -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;
|
||||
}
|
||||
}
|
||||
+64
@@ -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();
|
||||
|
||||
+101
@@ -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;
|
||||
}
|
||||
}
|
||||
+11
@@ -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;
|
||||
}
|
||||
}
|
||||
+12
-9
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
+39
@@ -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();
|
||||
|
||||
+60
@@ -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"]
|
||||
}
|
||||
Reference in New Issue
Block a user