mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-12 03:24:14 +00:00
Generation and runtime fixes
- Surface detail blends raw noise with cached six-block interpolation - Locked biome layers sample only the requested ranges - Surface layers skip noise when slope requirements fail - Stilt supports exclude configured materials and block entities - Ice and glass remain eligible foundation materials - Block-drop routing filters materials before loading biome history - Vision displays biome previews before river refinement - Interrupted previews retain reusable partial tiles - Hydrology waiters stop without cancelling shared planning tasks - Regression tests cover generation, supports, drop routing, and preview cancellation
This commit is contained in:
@@ -78,10 +78,6 @@ final class IrisBiomeLayerGenerator {
|
||||
|
||||
for (int i = 0; i < layerCount; i++) {
|
||||
IrisBiomePaletteLayer layer = layers.get(i);
|
||||
double zoom = layer.getZoom();
|
||||
CNG hgen = heightGenerators.get(i);
|
||||
int d = hgen.fit(layer.getMinHeight(), layer.getMaxHeight(), wx / zoom, wz / zoom);
|
||||
|
||||
IrisSlopeClip sc = layer.getSlopeCondition();
|
||||
|
||||
if (!sc.isDefault()) {
|
||||
@@ -90,10 +86,13 @@ final class IrisBiomeLayerGenerator {
|
||||
sampledSlope = true;
|
||||
}
|
||||
if (!sc.isValid(surfaceSlope)) {
|
||||
d = 0;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
double zoom = layer.getZoom();
|
||||
CNG hgen = heightGenerators.get(i);
|
||||
int d = hgen.fit(layer.getMinHeight(), layer.getMaxHeight(), wx / zoom, wz / zoom);
|
||||
if (d <= 0) {
|
||||
continue;
|
||||
}
|
||||
@@ -171,56 +170,78 @@ final class IrisBiomeLayerGenerator {
|
||||
|
||||
private static KList<PlatformBlockState> generateLockedLayers(IrisBiome biome, double wx, double wz, RNG random, int maxDepthf, int height, IrisData rdata, IrisComplex complex, ProceduralStream<Double> slopeStream) {
|
||||
KList<PlatformBlockState> data = new KList<>();
|
||||
KList<PlatformBlockState> real = new KList<>();
|
||||
int maxDepth = Math.min(maxDepthf, biome.getLockLayersMax());
|
||||
if (maxDepth <= 0) {
|
||||
KList<IrisBiomePaletteLayer> layers = biome.getLayers();
|
||||
int layerCount = layers.size();
|
||||
if (maxDepth <= 0 || layerCount == 0) {
|
||||
return data;
|
||||
}
|
||||
|
||||
KList<IrisBiomePaletteLayer> layers = biome.getLayers();
|
||||
int layerCount = layers.size();
|
||||
|
||||
if (layerCount > 0) {
|
||||
KList<CNG> heightGenerators = getLayerHeightGenerators(biome, random, rdata);
|
||||
int[] boundaries = new int[layerCount + 1];
|
||||
double surfaceSlope = 0D;
|
||||
boolean sampledSlope = false;
|
||||
|
||||
for (int i = 0; i < layerCount; i++) {
|
||||
IrisBiomePaletteLayer layer = layers.get(i);
|
||||
double zoom = layer.getZoom();
|
||||
CNG hgen = heightGenerators.get(i);
|
||||
int d = hgen.fit(layer.getMinHeight(), layer.getMaxHeight(), wx / zoom, wz / zoom);
|
||||
|
||||
IrisSlopeClip sc = layer.getSlopeCondition();
|
||||
|
||||
boundaries[i + 1] = boundaries[i];
|
||||
if (!sc.isDefault()) {
|
||||
if (!sampledSlope) {
|
||||
surfaceSlope = resolveSurfaceSlope(complex, slopeStream, wx, height, wz);
|
||||
sampledSlope = true;
|
||||
}
|
||||
if (!sc.isValid(surfaceSlope)) {
|
||||
d = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (d <= 0) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
appendLayer(data, layer, i, d, wx, wz, random, Integer.MAX_VALUE, rdata);
|
||||
double zoom = layer.getZoom();
|
||||
CNG hgen = heightGenerators.get(i);
|
||||
int thickness = hgen.fit(layer.getMinHeight(), layer.getMaxHeight(), wx / zoom, wz / zoom);
|
||||
boundaries[i + 1] += Math.max(0, thickness);
|
||||
}
|
||||
|
||||
int cycleLength = boundaries[layerCount];
|
||||
if (cycleLength == 0) {
|
||||
return data;
|
||||
}
|
||||
|
||||
int sampledDepth = Math.min(maxDepth, cycleLength);
|
||||
for (int depth = 0; depth < sampledDepth; depth++) {
|
||||
int position = Math.floorMod(512L - height - depth, cycleLength);
|
||||
int layerIndex = findLayer(boundaries, position);
|
||||
IrisBiomePaletteLayer layer = layers.get(layerIndex);
|
||||
int offset = position - boundaries[layerIndex];
|
||||
double zoom = layer.getZoom();
|
||||
try {
|
||||
data.add(layer.get(random, layerIndex + offset,
|
||||
(wx + offset) / zoom, offset, (wz - offset) / zoom, rdata));
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.reportError(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (data.isEmpty()) {
|
||||
return real;
|
||||
if (sampledDepth == cycleLength && !data.isEmpty()) {
|
||||
int resolvedCycleLength = data.size();
|
||||
for (int depth = resolvedCycleLength; depth < maxDepth; depth++) {
|
||||
data.add(data.get(depth % resolvedCycleLength));
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
for (int i = 0; i < maxDepth; i++) {
|
||||
long offset = 512L - height - i;
|
||||
real.add(data.get(Math.floorMod(offset, data.size())));
|
||||
private static int findLayer(int[] boundaries, int position) {
|
||||
int low = 0;
|
||||
int high = boundaries.length - 1;
|
||||
while (low < high) {
|
||||
int middle = (low + high) >>> 1;
|
||||
if (boundaries[middle + 1] <= position) {
|
||||
low = middle + 1;
|
||||
} else {
|
||||
high = middle;
|
||||
}
|
||||
|
||||
return real;
|
||||
}
|
||||
return low;
|
||||
}
|
||||
|
||||
private static double resolveSurfaceSlope(
|
||||
|
||||
@@ -3,14 +3,21 @@ package art.arcane.iris.generation.decoration;
|
||||
import art.arcane.iris.generation.terrain.IrisMaterialPalette;
|
||||
|
||||
import art.arcane.volmlib.util.documentation.Description;
|
||||
import art.arcane.iris.pack.schema.annotation.ArrayType;
|
||||
import art.arcane.iris.pack.schema.annotation.MaxNumber;
|
||||
import art.arcane.iris.pack.schema.annotation.MinNumber;
|
||||
import art.arcane.iris.pack.schema.annotation.RegistryListBlockType;
|
||||
import art.arcane.iris.pack.schema.annotation.Snippet;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
@Snippet("stilt-settings")
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@@ -32,6 +39,10 @@ public class IrisStiltSettings {
|
||||
private int overStilt;
|
||||
@Description("If defined, stilting will be done using this block palette rather than the last layer of the object.")
|
||||
private IrisMaterialPalette palette;
|
||||
@ArrayType(min = 1, type = String.class)
|
||||
@RegistryListBlockType
|
||||
@Description("Block materials that must never extend into stilts. Matches all block states of each material, before and after palette replacement or object edits. Original object blocks remain in place. Containers and blocks with tile entities are always excluded.")
|
||||
private KList<String> exclude = new KList<>();
|
||||
@MinNumber(1)
|
||||
@MaxNumber(256)
|
||||
@Description("For ORGANIC_STILT / CEILING_HANG: the maximum number of blocks to scan toward the cave floor (or ceiling) looking for solid ground before giving up.")
|
||||
@@ -45,4 +56,16 @@ public class IrisStiltSettings {
|
||||
@Description("For ORGANIC_STILT / CEILING_HANG: in the deepest fraction of each stilt column, blocks are randomly skipped to break the tip up. Higher = more broken and scratchy. 0 disables.")
|
||||
private double organicScratch = 0.55;
|
||||
|
||||
public Set<String> excludedMaterials() {
|
||||
if (exclude == null || exclude.isEmpty()) {
|
||||
return Set.of();
|
||||
}
|
||||
Set<String> materials = new HashSet<>(exclude.size());
|
||||
for (String block : exclude) {
|
||||
String material = block.trim().toLowerCase(Locale.ROOT);
|
||||
materials.add(material.indexOf(':') < 0 ? "minecraft:" + material : material);
|
||||
}
|
||||
return materials;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.Executor;
|
||||
@@ -890,8 +890,11 @@ public final class HydrologyTileCache implements AutoCloseable {
|
||||
|
||||
private static <T> T awaitPlan(CompletableFuture<T> future) {
|
||||
try {
|
||||
return future.join();
|
||||
} catch (CompletionException failure) {
|
||||
return future.get();
|
||||
} catch (InterruptedException interrupted) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new CancellationException("Hydrology plan wait interrupted.");
|
||||
} catch (ExecutionException failure) {
|
||||
Throwable cause = failure.getCause();
|
||||
if (cause instanceof RuntimeException runtimeException) {
|
||||
throw runtimeException;
|
||||
|
||||
@@ -20,6 +20,8 @@ package art.arcane.iris.generation.noise;
|
||||
|
||||
import art.arcane.iris.pack.loading.IrisRegistrant;
|
||||
import art.arcane.iris.generation.cache.AtomicCache;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.pack.schema.annotation.ArrayType;
|
||||
import art.arcane.volmlib.util.documentation.Description;
|
||||
import art.arcane.iris.pack.schema.annotation.MaxNumber;
|
||||
@@ -29,10 +31,14 @@ import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.interpolation.IrisInterpolation;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.noise.CellGenerator;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.util.List;
|
||||
@@ -44,13 +50,24 @@ import java.util.List;
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
public class IrisGenerator extends IrisRegistrant {
|
||||
private static final int SURFACE_GRID = 6;
|
||||
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 transient volatile SurfaceCache surfaceSamples;
|
||||
@MinNumber(0.001)
|
||||
@Description("The zoom or frequency.")
|
||||
private double zoom = 1;
|
||||
@MinNumber(0)
|
||||
@Description("The opacity, essentially a multiplier on the output.")
|
||||
private double opacity = 1;
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Description("Fine terrain detail retained around bilinear interpolation of the original noise on a 6-block grid. 1 preserves the original noise; 0.5 halves the sub-grid residual. Original grid heights, feature sizes and height ranges stay unchanged.")
|
||||
private double surfaceDetail = 1;
|
||||
@Description("Multiply the compsites instead of adding them")
|
||||
private boolean multiplicitive = false;
|
||||
@MinNumber(0.001)
|
||||
@@ -165,6 +182,37 @@ public class IrisGenerator extends IrisRegistrant {
|
||||
}
|
||||
|
||||
public double getHeight(double rx, double ry, double rz, long superSeed, boolean no3d) {
|
||||
if (!Double.isFinite(surfaceDetail) || surfaceDetail < 0D || surfaceDetail > 1D) {
|
||||
throw new IllegalArgumentException("Generator surfaceDetail must be finite and between 0 and 1");
|
||||
}
|
||||
if (surfaceDetail == 1D || composite.isEmpty()) {
|
||||
return sampleHeight(rx, rz, superSeed);
|
||||
}
|
||||
IrisData data = getLoader();
|
||||
Engine engine = data == null ? null : data.getEngine();
|
||||
SurfaceCacheStripe[] stripes = surfaceCache(data, engine).stripes();
|
||||
double lowerX = Math.floor(rx / SURFACE_GRID) * SURFACE_GRID;
|
||||
double lowerZ = Math.floor(rz / SURFACE_GRID) * SURFACE_GRID;
|
||||
double dx = (rx - lowerX) / SURFACE_GRID;
|
||||
double dz = (rz - lowerZ) / SURFACE_GRID;
|
||||
double northWest = cachedSurfaceHeight(lowerX, lowerZ, superSeed, stripes);
|
||||
if (dx == 0D && dz == 0D) {
|
||||
return northWest;
|
||||
}
|
||||
double north = dx == 0D ? northWest : IrisInterpolation.lerp(northWest,
|
||||
cachedSurfaceHeight(lowerX + SURFACE_GRID, lowerZ, superSeed, stripes), dx);
|
||||
double smooth = north;
|
||||
if (dz != 0D) {
|
||||
double southWest = cachedSurfaceHeight(lowerX, lowerZ + SURFACE_GRID, superSeed, stripes);
|
||||
double south = dx == 0D ? southWest : IrisInterpolation.lerp(southWest,
|
||||
cachedSurfaceHeight(lowerX + SURFACE_GRID, lowerZ + SURFACE_GRID, superSeed, stripes), dx);
|
||||
smooth = IrisInterpolation.lerp(north, south, dz);
|
||||
}
|
||||
return surfaceDetail == 0D ? smooth
|
||||
: smooth + (sampleHeight(rx, rz, superSeed) - smooth) * surfaceDetail;
|
||||
}
|
||||
|
||||
private double sampleHeight(double rx, double rz, long superSeed) {
|
||||
if (composite.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
@@ -205,6 +253,47 @@ public class IrisGenerator extends IrisRegistrant {
|
||||
return v;
|
||||
}
|
||||
|
||||
private double cachedSurfaceHeight(double x, double z, long superSeed, SurfaceCacheStripe[] stripes) {
|
||||
if (x != (int) x || z != (int) z) {
|
||||
return sampleHeight(x, z, superSeed);
|
||||
}
|
||||
long key = ((long) (int) x << 32) | ((int) z & 0xffffffffL);
|
||||
long mixed = key ^ (key >>> 33);
|
||||
mixed *= 0xff51afd7ed558ccdL;
|
||||
mixed ^= mixed >>> 33;
|
||||
SurfaceCacheStripe stripe = stripes[(int) mixed & (SURFACE_CACHE_STRIPES - 1)];
|
||||
SurfaceSample cached = stripe.get(key, superSeed);
|
||||
if (cached != null) {
|
||||
return cached.height();
|
||||
}
|
||||
double height = sampleHeight(x, z, superSeed);
|
||||
stripe.put(key, new SurfaceSample(superSeed, height));
|
||||
return height;
|
||||
}
|
||||
|
||||
private SurfaceCache surfaceCache(IrisData data, Engine engine) {
|
||||
SurfaceCache cached = surfaceSamples;
|
||||
if (cached != null && cached.data() == data && cached.engine() == engine) {
|
||||
return cached;
|
||||
}
|
||||
synchronized (this) {
|
||||
cached = surfaceSamples;
|
||||
if (cached == null || cached.data() != data || cached.engine() != engine) {
|
||||
cached = new SurfaceCache(data, engine, createSurfaceCache());
|
||||
surfaceSamples = cached;
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
private static SurfaceCacheStripe[] createSurfaceCache() {
|
||||
SurfaceCacheStripe[] stripes = new SurfaceCacheStripe[SURFACE_CACHE_STRIPES];
|
||||
for (int i = 0; i < stripes.length; i++) {
|
||||
stripes[i] = new SurfaceCacheStripe();
|
||||
}
|
||||
return stripes;
|
||||
}
|
||||
|
||||
public double cell(double rx, double rz, double v, double superSeed) {
|
||||
getCellGenerator(getSeed() + 46222).setShuffle(getCellFractureShuffle());
|
||||
double fractureX = rx / getCellFractureZoom();
|
||||
@@ -231,6 +320,7 @@ public class IrisGenerator extends IrisRegistrant {
|
||||
|
||||
public IrisGenerator rescale(double scale) {
|
||||
zoom /= scale;
|
||||
surfaceSamples = null;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -253,4 +343,26 @@ public class IrisGenerator extends IrisRegistrant {
|
||||
public String getTypeName() {
|
||||
return "Generator";
|
||||
}
|
||||
|
||||
private record SurfaceCache(IrisData data, Engine engine, SurfaceCacheStripe[] stripes) {
|
||||
}
|
||||
|
||||
private record SurfaceSample(long seed, double height) {
|
||||
}
|
||||
|
||||
private static final class SurfaceCacheStripe {
|
||||
private final Long2ObjectLinkedOpenHashMap<SurfaceSample> entries = new Long2ObjectLinkedOpenHashMap<>(16);
|
||||
|
||||
private synchronized SurfaceSample get(long key, long seed) {
|
||||
SurfaceSample sample = entries.getAndMoveToLast(key);
|
||||
return sample != null && sample.seed() == seed ? sample : null;
|
||||
}
|
||||
|
||||
private synchronized void put(long key, SurfaceSample sample) {
|
||||
entries.putAndMoveToLast(key, sample);
|
||||
if (entries.size() > SURFACE_CACHE_STRIPE_SIZE) {
|
||||
entries.removeFirst();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,6 +173,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
getData().prepareBlockDropRules();
|
||||
super.start();
|
||||
if (!looper.isAlive()) {
|
||||
looper.start();
|
||||
|
||||
@@ -21,12 +21,14 @@ package art.arcane.iris.generation.runtime;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.world.tree.BlockDropRouter;
|
||||
import art.arcane.iris.world.history.SavedBiomeUnavailableException;
|
||||
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.world.loot.IrisBlockDrops;
|
||||
import art.arcane.iris.world.entity.IrisMarker;
|
||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||
import art.arcane.iris.platform.generation.EngineBukkitOps;
|
||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
import art.arcane.iris.platform.bukkit.BukkitBlockState;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
@@ -139,6 +141,9 @@ final class WorldBlockDropRouter {
|
||||
}
|
||||
|
||||
private List<IrisBlockDrops> resolveDropProviders(BlockBreakEvent event) {
|
||||
if (!mayHaveBlockDropRules(BukkitBlockState.of(event.getBlock().getBlockData()).materialKey())) {
|
||||
return List.of();
|
||||
}
|
||||
IrisBiome biome = EngineBukkitOps.getBiome(manager.getEngine(), event.getBlock().getLocation());
|
||||
List<IrisBlockDrops> providers = filterDrops(biome.getBlockDrops(), event, manager.getData());
|
||||
if (providers.stream().noneMatch(IrisBlockDrops::isSkipParents)) {
|
||||
@@ -148,4 +153,15 @@ final class WorldBlockDropRouter {
|
||||
}
|
||||
return providers;
|
||||
}
|
||||
|
||||
private boolean mayHaveBlockDropRules(String material) {
|
||||
if (manager.getData().hasBlockDropRules(material)) {
|
||||
return true;
|
||||
}
|
||||
if (manager.getEngine() instanceof IrisEngine engine) {
|
||||
GenerationHistoryRuntimeRouter router = engine.getGenerationHistoryRuntimeRouter().orElse(null);
|
||||
return router != null && router.biomes().mayHaveBlockDropRules(material);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,9 @@ import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.pack.PackDirectoryResolver;
|
||||
import art.arcane.iris.generation.cache.AtomicCache;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.decoration.IrisProceduralBlocks;
|
||||
import art.arcane.iris.world.loot.IrisBlockDrops;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.structure.placement.IrisStructureLocator;
|
||||
import art.arcane.iris.structure.graph.StructureGraphCatalog;
|
||||
import art.arcane.iris.world.history.GenerationPackFingerprint;
|
||||
@@ -102,6 +105,7 @@ import java.util.Optional;
|
||||
import java.util.Collections;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Set;
|
||||
import java.util.HashSet;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Data
|
||||
@@ -157,6 +161,9 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
||||
@EqualsAndHashCode.Exclude
|
||||
private transient volatile GenerationRegistryContractFactory.CustomBiomeResourceResolver customBiomeResourceResolver;
|
||||
private transient volatile Engine soleEngine;
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Setter(AccessLevel.NONE)
|
||||
private transient volatile Set<String> blockDropMaterials;
|
||||
|
||||
private IrisData(File dataFolder) {
|
||||
this(dataFolder, false);
|
||||
@@ -651,6 +658,7 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
||||
}
|
||||
|
||||
public synchronized void hotloaded() {
|
||||
blockDropMaterials = null;
|
||||
StructureGraphCatalog.invalidate(this);
|
||||
IrisObjectScale.invalidate(this);
|
||||
contentGate = null;
|
||||
@@ -730,6 +738,7 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
||||
}
|
||||
|
||||
public void dump() {
|
||||
blockDropMaterials = null;
|
||||
StructureGraphCatalog.invalidate(this);
|
||||
IrisObjectScale.invalidate(this);
|
||||
for (ResourceLoader<?> i : loaders.values()) {
|
||||
@@ -771,12 +780,43 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
||||
}
|
||||
|
||||
public void clearLists() {
|
||||
blockDropMaterials = null;
|
||||
for (ResourceLoader<?> i : loaders.values()) {
|
||||
i.clearList();
|
||||
}
|
||||
possibleSnippets.clear();
|
||||
}
|
||||
|
||||
public boolean hasBlockDropRules(String material) {
|
||||
Set<String> materials = blockDropMaterials;
|
||||
return materials == null || materials.contains(material);
|
||||
}
|
||||
|
||||
public void prepareBlockDropRules() {
|
||||
Set<String> materials = new HashSet<>();
|
||||
for (IrisDimension dimension : dimensionLoader.loadAll(dimensionLoader.getPossibleKeys())) {
|
||||
collectBlockDropMaterials(materials, dimension.getBlockDrops());
|
||||
}
|
||||
for (IrisRegion region : regionLoader.loadAll(regionLoader.getPossibleKeys())) {
|
||||
collectBlockDropMaterials(materials, region.getBlockDrops());
|
||||
}
|
||||
for (IrisBiome biome : biomeLoader.loadAll(biomeLoader.getPossibleKeys())) {
|
||||
collectBlockDropMaterials(materials, biome.getBlockDrops());
|
||||
}
|
||||
blockDropMaterials = Set.copyOf(materials);
|
||||
}
|
||||
|
||||
private void collectBlockDropMaterials(Set<String> materials, KList<IrisBlockDrops> rules) {
|
||||
for (IrisBlockDrops rule : rules) {
|
||||
for (IrisBlockData block : rule.getBlocks()) {
|
||||
PlatformBlockState state = block.getBlockData(this);
|
||||
if (state != null) {
|
||||
materials.add(IrisProceduralBlocks.materialKey(state.placementBaseState()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void invalidateLoader(ResourceLoader<?> loader) {
|
||||
if (loader == null) {
|
||||
return;
|
||||
|
||||
@@ -50,6 +50,7 @@ import art.arcane.volmlib.util.matter.MatterMarker;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.stream.StreamSupport;
|
||||
@@ -143,6 +144,9 @@ final class IrisObjectPlacementRunner {
|
||||
config.getMode() == ObjectPlaceMode.MIN_STILT || config.getMode() == ObjectPlaceMode.FAST_MIN_STILT ||
|
||||
config.getMode() == ObjectPlaceMode.CENTER_STILT || config.getMode() == ObjectPlaceMode.ERODE_STILT || organic);
|
||||
boolean eroding = config.getMode() == ObjectPlaceMode.ERODE_STILT;
|
||||
IrisStiltSettings stiltSettings = config.getStiltSettings();
|
||||
Set<String> excludedStiltMaterials = stilting && stiltSettings != null
|
||||
? stiltSettings.excludedMaterials() : Set.of();
|
||||
KMap<Position2, Integer> heightmap = config.getSnow() > 0 ? new KMap<>() : null;
|
||||
int spinx = rng.imax() / 1000;
|
||||
int spiny = rng.imax() / 1000;
|
||||
@@ -539,7 +543,7 @@ final class IrisObjectPlacementRunner {
|
||||
i.add(translateOffset);
|
||||
}
|
||||
|
||||
if (stilting && IrisObjectShaping.shouldStilt(data)) {
|
||||
if (stilting && IrisObjectShaping.isStiltLayerBlock(data)) {
|
||||
if (i.getBlockY() < lowest) {
|
||||
lowest = i.getBlockY();
|
||||
}
|
||||
@@ -654,7 +658,7 @@ final class IrisObjectPlacementRunner {
|
||||
self.readLock.lock();
|
||||
try {
|
||||
VectorMap<PlatformBlockState> blocks = self.blocks;
|
||||
IrisStiltSettings settings = config.getStiltSettings();
|
||||
IrisStiltSettings settings = stiltSettings;
|
||||
|
||||
double erodeCentroidX = 0;
|
||||
double erodeCentroidZ = 0;
|
||||
@@ -670,7 +674,7 @@ final class IrisObjectPlacementRunner {
|
||||
}
|
||||
if (rot.getBlockY() == lowest) {
|
||||
PlatformBlockState bd = centroidCursor.value();
|
||||
if (bd != null && IrisObjectShaping.shouldStilt(bd)) {
|
||||
if (bd != null && IrisObjectShaping.shouldStilt(bd, excludedStiltMaterials)) {
|
||||
erodeCentroidX += rot.getX();
|
||||
erodeCentroidZ += rot.getZ();
|
||||
centroidCount++;
|
||||
@@ -690,7 +694,7 @@ final class IrisObjectPlacementRunner {
|
||||
}
|
||||
if (rot.getBlockY() == lowest) {
|
||||
PlatformBlockState bd = spreadCursor.value();
|
||||
if (bd != null && IrisObjectShaping.shouldStilt(bd)) {
|
||||
if (bd != null && IrisObjectShaping.shouldStilt(bd, excludedStiltMaterials)) {
|
||||
double dx = rot.getX() - erodeCentroidX;
|
||||
double dz = rot.getZ() - erodeCentroidZ;
|
||||
double dist = Math.sqrt(dx * dx + dz * dz);
|
||||
@@ -719,7 +723,7 @@ final class IrisObjectPlacementRunner {
|
||||
sourceData = IrisObject.States.air();
|
||||
}
|
||||
|
||||
if (!IrisObjectShaping.shouldStilt(sourceData)) {
|
||||
if (!IrisObjectShaping.shouldStilt(sourceData, excludedStiltMaterials)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -768,7 +772,7 @@ final class IrisObjectPlacementRunner {
|
||||
}
|
||||
}
|
||||
|
||||
if (d == null || !d.isOccluding())
|
||||
if (d == null || !IrisObjectShaping.shouldStilt(d, excludedStiltMaterials))
|
||||
continue;
|
||||
|
||||
xx = x + (int) Math.round(i.getX());
|
||||
|
||||
@@ -29,6 +29,8 @@ import art.arcane.volmlib.util.math.Vector3i;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Volume shaping for {@link IrisObject}: smart boring, shrinkwrapping, compaction and the shared block
|
||||
* classification helpers used by placement.
|
||||
@@ -244,17 +246,23 @@ final class IrisObjectShaping {
|
||||
self.floatingFootprint.reset();
|
||||
}
|
||||
|
||||
static boolean shouldStilt(PlatformBlockState state) {
|
||||
if (!state.isOccluding()) {
|
||||
static boolean isStiltLayerBlock(PlatformBlockState state) {
|
||||
String material = materialKey(state);
|
||||
if (!state.isOccluding() && !material.equals("minecraft:ice") && !material.equals("minecraft:glass")
|
||||
&& !material.equals("minecraft:tinted_glass") && !material.endsWith("_stained_glass")) {
|
||||
return false;
|
||||
}
|
||||
String material = materialKey(state);
|
||||
if (material.endsWith("_stairs") || material.endsWith("_slab")) {
|
||||
return false;
|
||||
}
|
||||
return !material.equals("minecraft:dirt_path");
|
||||
}
|
||||
|
||||
static boolean shouldStilt(PlatformBlockState state, Set<String> excludedMaterials) {
|
||||
return isStiltLayerBlock(state) && !state.isStorage() && !state.hasTileEntity()
|
||||
&& !excludedMaterials.contains(materialKey(state));
|
||||
}
|
||||
|
||||
static String materialKey(PlatformBlockState state) {
|
||||
return IrisProceduralBlocks.materialKey(state);
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ public final class IrisRenderer {
|
||||
RenderType currentType,
|
||||
BooleanSupplier cancelled
|
||||
) {
|
||||
return render(sx, sz, size, resolution, currentType, cancelled, false);
|
||||
return render(sx, sz, size, resolution, currentType, cancelled, false, false);
|
||||
}
|
||||
|
||||
public BufferedImage renderStudio(
|
||||
@@ -105,7 +105,41 @@ public final class IrisRenderer {
|
||||
RenderType currentType,
|
||||
BooleanSupplier cancelled
|
||||
) {
|
||||
return render(sx, sz, size, resolution, currentType, cancelled, true);
|
||||
return render(sx, sz, size, resolution, currentType, cancelled, true, true);
|
||||
}
|
||||
|
||||
public BufferedImage renderStudioBase(
|
||||
double sx,
|
||||
double sz,
|
||||
double size,
|
||||
int resolution,
|
||||
RenderType currentType,
|
||||
BooleanSupplier cancelled
|
||||
) {
|
||||
return render(sx, sz, size, resolution, currentType, cancelled, true, false);
|
||||
}
|
||||
|
||||
public BufferedImage refineStudioBiome(
|
||||
double sx,
|
||||
double sz,
|
||||
double size,
|
||||
BufferedImage base,
|
||||
BooleanSupplier cancelled
|
||||
) {
|
||||
Objects.requireNonNull(base, "base");
|
||||
Objects.requireNonNull(cancelled, "cancelled");
|
||||
checkCancelled(cancelled);
|
||||
if (!Double.isFinite(sx) || !Double.isFinite(sz) || !Double.isFinite(size) || size <= 0D
|
||||
|| base.getWidth() != base.getHeight()) {
|
||||
throw new IllegalArgumentException("Vision biome refinement requires finite coordinates and a square base image");
|
||||
}
|
||||
int resolution = base.getWidth();
|
||||
BufferedImage image = new BufferedImage(resolution, resolution, BufferedImage.TYPE_INT_RGB);
|
||||
base.copyData(image.getRaster());
|
||||
int[] pixels = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
|
||||
renderRiverAtlas(pixels, resolution, sx, sz, size / resolution, renderer.getComplex(), cancelled, true);
|
||||
checkCancelled(cancelled);
|
||||
return image;
|
||||
}
|
||||
|
||||
private BufferedImage render(
|
||||
@@ -115,7 +149,8 @@ public final class IrisRenderer {
|
||||
int resolution,
|
||||
RenderType currentType,
|
||||
BooleanSupplier cancelled,
|
||||
boolean studio
|
||||
boolean studio,
|
||||
boolean riverOverlay
|
||||
) {
|
||||
if (!Double.isFinite(sx) || !Double.isFinite(sz) || !Double.isFinite(size) || size <= 0D) {
|
||||
throw new IllegalArgumentException("Vision render coordinates and size must be finite");
|
||||
@@ -142,7 +177,7 @@ public final class IrisRenderer {
|
||||
}
|
||||
if (studio && adaptiveStudioType(currentType)) {
|
||||
renderAdaptiveAtlas(pixels, resolution, sx, sz, step, shader, cancelled);
|
||||
if (currentType == RenderType.BIOME) {
|
||||
if (currentType == RenderType.BIOME && riverOverlay) {
|
||||
renderRiverAtlas(pixels, resolution, sx, sz, step, renderer.getComplex(), cancelled, true);
|
||||
}
|
||||
return image;
|
||||
|
||||
@@ -136,7 +136,7 @@ final class VisionRenderController implements AutoCloseable {
|
||||
List<VisibleTile> tiles = visibleTiles(spec);
|
||||
Frame frame = new Frame(viewSequence.incrementAndGet(), spec, tiles);
|
||||
for (VisibleTile tile : tiles) {
|
||||
tile.setImage(cache.get(frame.key(tile)));
|
||||
tile.setResult(cache.get(frame.key(tile)));
|
||||
}
|
||||
CancellationToken token = new CancellationToken();
|
||||
WorkState work = new WorkState(frame, token);
|
||||
@@ -165,7 +165,7 @@ final class VisionRenderController implements AutoCloseable {
|
||||
}
|
||||
int ready = 0;
|
||||
for (VisibleTile tile : frame.tiles()) {
|
||||
if (tile.image() != null) {
|
||||
if (tile.complete()) {
|
||||
ready++;
|
||||
}
|
||||
}
|
||||
@@ -275,14 +275,19 @@ final class VisionRenderController implements AutoCloseable {
|
||||
if (!isCurrent(work)) {
|
||||
return;
|
||||
}
|
||||
int admissionLimit = work.frame().spec().type() == RenderType.RIVER ? 1 : renderWorkerCount;
|
||||
int admissionLimit = work.refining() || work.frame().spec().type() == RenderType.RIVER ? 1 : renderWorkerCount;
|
||||
while (work.inFlight() < admissionLimit) {
|
||||
VisibleTile tile = work.nextMissing();
|
||||
if (tile == null) {
|
||||
if (work.inFlight() == 0 && work.beginRefinement()) {
|
||||
admissionLimit = 1;
|
||||
continue;
|
||||
}
|
||||
return;
|
||||
}
|
||||
boolean refining = work.refining();
|
||||
work.incrementInFlight();
|
||||
TrackedRenderTask task = new TrackedRenderTask(() -> render(work, tile), activeRenderTasks);
|
||||
TrackedRenderTask task = new TrackedRenderTask(() -> render(work, tile, refining), activeRenderTasks);
|
||||
activeRenderTasks.add(task);
|
||||
try {
|
||||
renderExecutor.execute(task);
|
||||
@@ -295,7 +300,7 @@ final class VisionRenderController implements AutoCloseable {
|
||||
}
|
||||
}
|
||||
|
||||
private void render(WorkState work, VisibleTile tile) {
|
||||
private void render(WorkState work, VisibleTile tile, boolean refining) {
|
||||
Frame frame = work.frame();
|
||||
CancellationToken token = work.token();
|
||||
try {
|
||||
@@ -303,19 +308,26 @@ final class VisionRenderController implements AutoCloseable {
|
||||
return;
|
||||
}
|
||||
double tileSpan = TILE_PIXELS * frame.spec().blocksPerPixel();
|
||||
BufferedImage image = frame.spec().renderer().renderStudio(
|
||||
tile.tileX() * tileSpan,
|
||||
tile.tileZ() * tileSpan,
|
||||
tileSpan,
|
||||
TILE_PIXELS,
|
||||
frame.spec().type(),
|
||||
() -> !isCurrent(frame, token)
|
||||
);
|
||||
double startX = tile.tileX() * tileSpan;
|
||||
double startZ = tile.tileZ() * tileSpan;
|
||||
IrisRenderer renderer = frame.spec().renderer();
|
||||
BufferedImage image;
|
||||
if (refining) {
|
||||
image = renderer.refineStudioBiome(startX, startZ, tileSpan, tile.image(),
|
||||
() -> !isCurrent(frame, token));
|
||||
} else if (frame.spec().type() == RenderType.BIOME) {
|
||||
image = renderer.renderStudioBase(startX, startZ, tileSpan, TILE_PIXELS, frame.spec().type(),
|
||||
() -> !isCurrent(frame, token));
|
||||
} else {
|
||||
image = renderer.renderStudio(startX, startZ, tileSpan, TILE_PIXELS, frame.spec().type(),
|
||||
() -> !isCurrent(frame, token));
|
||||
}
|
||||
if (!isCurrent(frame, token)) {
|
||||
return;
|
||||
}
|
||||
cache.put(frame.key(tile), image);
|
||||
tile.setImage(image);
|
||||
TileResult result = new TileResult(image, refining || frame.spec().type() != RenderType.BIOME);
|
||||
cache.put(frame.key(tile), result);
|
||||
tile.setResult(result);
|
||||
publish(frame, token);
|
||||
} catch (CancellationException ignored) {
|
||||
} catch (Throwable error) {
|
||||
@@ -446,7 +458,7 @@ final class VisionRenderController implements AutoCloseable {
|
||||
private final int screenX;
|
||||
private final int screenY;
|
||||
private final double distanceSquared;
|
||||
private volatile BufferedImage image;
|
||||
private volatile TileResult result;
|
||||
|
||||
private VisibleTile(long tileX, long tileZ, int screenX, int screenY, double distanceSquared) {
|
||||
this.tileX = tileX;
|
||||
@@ -477,11 +489,17 @@ final class VisionRenderController implements AutoCloseable {
|
||||
}
|
||||
|
||||
BufferedImage image() {
|
||||
return image;
|
||||
TileResult current = result;
|
||||
return current == null ? null : current.image();
|
||||
}
|
||||
|
||||
void setImage(BufferedImage image) {
|
||||
this.image = image;
|
||||
boolean complete() {
|
||||
TileResult current = result;
|
||||
return current != null && current.complete();
|
||||
}
|
||||
|
||||
void setResult(TileResult result) {
|
||||
this.result = result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -548,6 +566,7 @@ final class VisionRenderController implements AutoCloseable {
|
||||
private final CancellationToken token;
|
||||
private int index;
|
||||
private int inFlight;
|
||||
private boolean refining;
|
||||
|
||||
private WorkState(Frame frame, CancellationToken token) {
|
||||
this.frame = frame;
|
||||
@@ -565,13 +584,26 @@ final class VisionRenderController implements AutoCloseable {
|
||||
VisibleTile nextMissing() {
|
||||
while (index < frame.tiles().size()) {
|
||||
VisibleTile tile = frame.tiles().get(index++);
|
||||
if (tile.image() == null) {
|
||||
if (refining ? tile.image() != null && !tile.complete() : tile.image() == null) {
|
||||
return tile;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
boolean refining() {
|
||||
return refining;
|
||||
}
|
||||
|
||||
boolean beginRefinement() {
|
||||
if (refining || frame.spec().type() != RenderType.BIOME) {
|
||||
return false;
|
||||
}
|
||||
refining = true;
|
||||
index = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
int inFlight() {
|
||||
return inFlight;
|
||||
}
|
||||
@@ -595,14 +627,15 @@ final class VisionRenderController implements AutoCloseable {
|
||||
this.entries = new LinkedHashMap<>(128, 0.75F, true);
|
||||
}
|
||||
|
||||
synchronized BufferedImage get(TileKey key) {
|
||||
synchronized TileResult get(TileKey key) {
|
||||
CacheEntry entry = entries.get(key);
|
||||
return entry == null ? null : entry.image();
|
||||
return entry == null ? null : entry.result();
|
||||
}
|
||||
|
||||
synchronized void put(TileKey key, BufferedImage image) {
|
||||
synchronized void put(TileKey key, TileResult result) {
|
||||
BufferedImage image = result.image();
|
||||
long imageBytes = (long) image.getWidth() * image.getHeight() * Integer.BYTES;
|
||||
CacheEntry previous = entries.put(key, new CacheEntry(image, imageBytes));
|
||||
CacheEntry previous = entries.put(key, new CacheEntry(result, imageBytes));
|
||||
if (previous != null) {
|
||||
bytes -= previous.bytes();
|
||||
}
|
||||
@@ -621,6 +654,12 @@ final class VisionRenderController implements AutoCloseable {
|
||||
}
|
||||
}
|
||||
|
||||
private record CacheEntry(BufferedImage image, long bytes) {
|
||||
private record TileResult(BufferedImage image, boolean complete) {
|
||||
private TileResult {
|
||||
Objects.requireNonNull(image, "image");
|
||||
}
|
||||
}
|
||||
|
||||
private record CacheEntry(TileResult result, long bytes) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,6 +112,24 @@ public final class SavedBiomeRuntime implements AutoCloseable {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean mayHaveBlockDropRules(String material) {
|
||||
if (engine.getData().hasBlockDropRules(material)) {
|
||||
return true;
|
||||
}
|
||||
GenerationManifest manifest = history.manifest();
|
||||
String activeEpoch = manifest.activeEpoch().epochId();
|
||||
for (GenerationEpoch epoch : manifest.epochs()) {
|
||||
if (epoch.epochId().equals(activeEpoch)) {
|
||||
continue;
|
||||
}
|
||||
Definitions source = definitions.get(epoch.epochId());
|
||||
if (source == null || source.data().hasBlockDropRules(material)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public Optional<BiomeEnvironment> resolveCaveBase(int blockX, int blockZ) {
|
||||
return resolve(blockX, 0, blockZ, QueryKind.CAVE_BASE);
|
||||
}
|
||||
@@ -449,6 +467,7 @@ public final class SavedBiomeRuntime implements AutoCloseable {
|
||||
}
|
||||
Definitions loaded = new Definitions(data, dimension, NativeBiomeSpawnSelection.retainedDerivatives(data),
|
||||
resolveFocusRegions(data, dimension));
|
||||
data.prepareBlockDropRules();
|
||||
definitions.put(epochId, loaded);
|
||||
return loaded;
|
||||
} catch (Throwable failure) {
|
||||
|
||||
@@ -24,6 +24,8 @@ import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.same;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
@@ -70,6 +72,7 @@ public final class IrisBiomeLayerSlopeSourceTest {
|
||||
assertEquals(1, ledge.size());
|
||||
assertSame(block, ledge.get(0));
|
||||
assertTrue(cap.isEmpty());
|
||||
verify(heightGenerator, times(1)).fit(1, 1, 12D, -8D);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -109,5 +112,6 @@ public final class IrisBiomeLayerSlopeSourceTest {
|
||||
assertTrue(hostLayers.isEmpty());
|
||||
assertEquals(1, sourceLayers.size());
|
||||
assertSame(surfaceBlock, sourceLayers.get(0));
|
||||
verify(heightGenerator, times(1)).fit(1, 1, 12D, -8D);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,30 @@
|
||||
package art.arcane.iris.generation.biome;
|
||||
|
||||
import art.arcane.iris.generation.noise.NoiseStyle;
|
||||
import art.arcane.iris.generation.terrain.IrisSlopeClip;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.noise.CNG;
|
||||
import art.arcane.volmlib.util.stream.ProceduralStream;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyDouble;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.ArgumentMatchers.same;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
public class IrisBiomeLockedLayerTest {
|
||||
@Test
|
||||
@@ -32,4 +48,133 @@ public class IrisBiomeLockedLayerTest {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sampledBandsMatchExpandedVariableMixedPalettes() {
|
||||
PlatformBlockState first = mock(PlatformBlockState.class);
|
||||
PlatformBlockState second = mock(PlatformBlockState.class);
|
||||
PlatformBlockState third = mock(PlatformBlockState.class);
|
||||
long[] seeds = {1L, 37L, -291L, Long.MAX_VALUE};
|
||||
int[] heights = {Integer.MIN_VALUE, -1000, -1, 0, 509, 512, 513, 1000, Integer.MAX_VALUE};
|
||||
int[] depths = {0, 1, 7, 39};
|
||||
double[] coordinates = {-71.5D, -1D, 0D, 13.25D, 127D};
|
||||
for (long seed : seeds) {
|
||||
RNG rng = new RNG(seed);
|
||||
IrisBiome biome = new IrisBiome().setLockLayers(true).setLockLayersMax(31).setLayers(new KList<>(
|
||||
palette(0, 0, 1D, first),
|
||||
palette(2, 5, 0.6D, first, second, second),
|
||||
palette(0, 3, 3D, third, first),
|
||||
palette(0, 0, 1D, second),
|
||||
palette(1, 4, 1.5D, second, third, first, third),
|
||||
palette(0, 0, 1D, third)));
|
||||
for (double x : coordinates) {
|
||||
for (int height : heights) {
|
||||
for (int depth : depths) {
|
||||
assertEquals(expandedBands(biome, x, -x, rng, depth, height, 0D),
|
||||
biome.generateLockedLayers(x, -x, rng, depth, height, null, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nullEntriesAndRepeatedCyclesMatchExpandedBands() {
|
||||
PlatformBlockState block = mock(PlatformBlockState.class);
|
||||
IrisBiome biome = new IrisBiome().setLockLayers(true).setLockLayersMax(30).setLayers(new KList<>(
|
||||
palette(1, 1, 1D, block), palette(2, 2, 1D, (PlatformBlockState) null)));
|
||||
RNG rng = new RNG(37L);
|
||||
assertEquals(expandedBands(biome, 3D, -11D, rng, 30, 1000, 0D),
|
||||
biome.generateLockedLayers(3D, -11D, rng, 30, 1000, null, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void zeroThicknessAndEmptyPalettesProduceNoBands() {
|
||||
IrisBiome biome = new IrisBiome().setLockLayers(true).setLayers(new KList<>());
|
||||
RNG rng = new RNG(37L);
|
||||
assertTrue(biome.generateLockedLayers(0D, 0D, rng, 7, 64, null, null).isEmpty());
|
||||
biome.setLayers(new KList<>(palette(0, 0, 1D, mock(PlatformBlockState.class))));
|
||||
assertTrue(biome.generateLockedLayers(0D, 0D, rng, 7, 64, null, null).isEmpty());
|
||||
assertTrue(biome.generateLockedLayers(0D, 0D, rng, -1, 64, null, null).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolvesOnlyRequestedPositionsOfLongBands() {
|
||||
IrisBiomePaletteLayer layer = spy(palette(1000, 1000, 0.6D,
|
||||
mock(PlatformBlockState.class), mock(PlatformBlockState.class)));
|
||||
IrisBiome biome = new IrisBiome().setLockLayers(true).setLayers(new KList<>(layer));
|
||||
RNG rng = new RNG(37L);
|
||||
assertEquals(7, biome.generateLockedLayers(13D, -27D, rng, 7, 64, null, null).size());
|
||||
verify(layer, times(7)).get(same(rng), anyInt(), anyDouble(), anyDouble(), anyDouble(), isNull());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void repeatedCyclesResolveEachPositionOnce() {
|
||||
IrisBiomePaletteLayer layer = spy(palette(3, 3, 0.6D,
|
||||
mock(PlatformBlockState.class), mock(PlatformBlockState.class)));
|
||||
IrisBiome biome = new IrisBiome().setLockLayers(true).setLockLayersMax(100).setLayers(new KList<>(layer));
|
||||
RNG rng = new RNG(37L);
|
||||
assertEquals(100, biome.generateLockedLayers(13D, -27D, rng, 100, 64, null, null).size());
|
||||
verify(layer, times(3)).get(same(rng), anyInt(), anyDouble(), anyDouble(), anyDouble(), isNull());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void slopeFilteredBandsMatchExpandedCycle() {
|
||||
IrisBiome biome = new IrisBiome().setLockLayers(true).setLockLayersMax(30).setLayers(new KList<>(
|
||||
palette(2, 4, 0.6D, mock(PlatformBlockState.class)),
|
||||
palette(3, 8, 1D, mock(PlatformBlockState.class)).setSlopeCondition(new IrisSlopeClip(3D, 255D)),
|
||||
palette(1, 5, 2D, mock(PlatformBlockState.class)).setSlopeCondition(new IrisSlopeClip(0D, 3D))));
|
||||
RNG rng = new RNG(37L);
|
||||
double[] slopes = {0D, 3D, 8D};
|
||||
for (double slope : slopes) {
|
||||
ProceduralStream<Double> slopeStream = ProceduralStream.ofDouble((x, z) -> slope);
|
||||
assertEquals(expandedBands(biome, 13D, -27D, rng, 30, 64, slope),
|
||||
biome.generateLayersWithSlope(null, 13D, -27D, rng, 30, 64, null, slopeStream));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sampledPaletteFailuresAreReported() {
|
||||
IrisBiomePaletteLayer layer = spy(palette(1, 1, 1D, mock(PlatformBlockState.class)));
|
||||
IrisBiome biome = new IrisBiome().setLockLayers(true).setLayers(new KList<>(layer));
|
||||
RNG rng = new RNG(37L);
|
||||
IllegalStateException failure = new IllegalStateException("Palette unavailable");
|
||||
doThrow(failure).when(layer).get(same(rng), anyInt(), anyDouble(), anyDouble(), anyDouble(), isNull());
|
||||
try (MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class)) {
|
||||
assertTrue(biome.generateLockedLayers(0D, 0D, rng, 7, 64, null, null).isEmpty());
|
||||
logging.verify(() -> IrisLogging.reportError(failure));
|
||||
}
|
||||
}
|
||||
|
||||
private static IrisBiomePaletteLayer palette(int minimum, int maximum, double zoom, PlatformBlockState... blocks) {
|
||||
IrisBiomePaletteLayer layer = new IrisBiomePaletteLayer().setMinHeight(minimum).setMaxHeight(maximum)
|
||||
.setZoom(zoom).setStyle(NoiseStyle.NOWHERE.style());
|
||||
layer.getBlockData().aquire(() -> new KList<>(blocks));
|
||||
return layer;
|
||||
}
|
||||
|
||||
private static KList<PlatformBlockState> expandedBands(IrisBiome biome, double x, double z, RNG rng,
|
||||
int maxDepth, int height, double slope) {
|
||||
KList<PlatformBlockState> expanded = new KList<>();
|
||||
KList<PlatformBlockState> result = new KList<>();
|
||||
KList<CNG> heights = biome.getLayerHeightGenerators(rng, null);
|
||||
for (int index = 0; index < biome.getLayers().size(); index++) {
|
||||
IrisBiomePaletteLayer layer = biome.getLayers().get(index);
|
||||
double zoom = layer.getZoom();
|
||||
int thickness = heights.get(index).fit(layer.getMinHeight(), layer.getMaxHeight(), x / zoom, z / zoom);
|
||||
if (!layer.getSlopeCondition().isValid(slope)) {
|
||||
continue;
|
||||
}
|
||||
for (int offset = 0; offset < thickness; offset++) {
|
||||
expanded.add(layer.get(rng, index + offset, (x + offset) / zoom, offset, (z - offset) / zoom, null));
|
||||
}
|
||||
}
|
||||
if (!expanded.isEmpty()) {
|
||||
for (int depth = 0; depth < Math.min(maxDepth, biome.getLockLayersMax()); depth++) {
|
||||
result.add(expanded.get(Math.floorMod(512L - height - depth, expanded.size())));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -50,6 +50,57 @@ import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class HydrologyTileCacheTest {
|
||||
@Test
|
||||
public void interruptedWaiterExitsWithoutCancellingTheSharedPlan() throws Exception {
|
||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||
HydrologyTile tile = mock(HydrologyTile.class);
|
||||
HydrologyTileKey key = new HydrologyTileKey(0, 0);
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
CountDownLatch waiterExited = new CountDownLatch(1);
|
||||
AtomicBoolean interrupted = new AtomicBoolean();
|
||||
AtomicReference<Thread> waiterThread = new AtomicReference<>();
|
||||
when(planner.plan(key)).thenAnswer(invocation -> {
|
||||
started.countDown();
|
||||
assertTrue(release.await(5L, TimeUnit.SECONDS));
|
||||
return tile;
|
||||
});
|
||||
ExecutorService workers = Executors.newSingleThreadExecutor();
|
||||
ExecutorService callers = Executors.newFixedThreadPool(2);
|
||||
HydrologyTileCache cache = new HydrologyTileCache(planner, 4, workers);
|
||||
try {
|
||||
Future<?> waiter = callers.submit(() -> {
|
||||
waiterThread.set(Thread.currentThread());
|
||||
try {
|
||||
assertThrows(CancellationException.class, () -> cache.get(key));
|
||||
interrupted.set(Thread.currentThread().isInterrupted());
|
||||
} finally {
|
||||
waiterExited.countDown();
|
||||
}
|
||||
});
|
||||
assertTrue(started.await(5L, TimeUnit.SECONDS));
|
||||
Future<HydrologyTile> other = callers.submit(() -> cache.get(key));
|
||||
|
||||
waiterThread.get().interrupt();
|
||||
assertTrue(waiterExited.await(2L, TimeUnit.SECONDS));
|
||||
waiter.get(2L, TimeUnit.SECONDS);
|
||||
assertTrue(interrupted.get());
|
||||
assertFalse(other.isDone());
|
||||
|
||||
release.countDown();
|
||||
assertSame(tile, other.get(5L, TimeUnit.SECONDS));
|
||||
assertSame(tile, cache.get(key));
|
||||
verify(planner, times(1)).plan(key);
|
||||
} finally {
|
||||
release.countDown();
|
||||
workers.shutdownNow();
|
||||
callers.shutdownNow();
|
||||
assertTrue(workers.awaitTermination(5L, TimeUnit.SECONDS));
|
||||
assertTrue(callers.awaitTermination(5L, TimeUnit.SECONDS));
|
||||
cache.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void closeDrainsBackgroundPlanningAndRejectsNewDemand() throws Exception {
|
||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
package art.arcane.iris.generation.noise;
|
||||
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.ToDoubleFunction;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class IrisGeneratorSurfaceDetailTest {
|
||||
@Test
|
||||
public void defaultUsesTheOriginalSampleOnce() {
|
||||
AtomicInteger samples = new AtomicInteger();
|
||||
IrisGenerator generator = generator((seed, x, z) -> {
|
||||
samples.incrementAndGet();
|
||||
return x * 0.01D + z * 0.02D;
|
||||
});
|
||||
|
||||
assertEquals(0.07D, generator.getHeight(1, 3, 27L), 1e-12D);
|
||||
assertEquals(1, samples.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void halvesSubgridDetailAndPreservesLatticeHeights() {
|
||||
IrisGenerator generator = generator((seed, x, z) ->
|
||||
(int) x % 6 == 0 && (int) z % 6 == 0 ? 0.2D : 0.8D);
|
||||
generator.setSurfaceDetail(0.5D);
|
||||
|
||||
for (int z = -12; z <= 12; z++) {
|
||||
for (int x = -12; x <= 12; x++) {
|
||||
double expected = x % 6 == 0 && z % 6 == 0 ? 0.2D : 0.5D;
|
||||
assertEquals(expected, generator.getHeight(x, z, 27L), 1e-12D);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preservesPlanarSlopesAtNegativeAndFractionalCoordinates() {
|
||||
IrisGenerator generator = generator((seed, x, z) -> 0.5D + x * 0.01D + z * 0.02D);
|
||||
generator.setSurfaceDetail(0.5D);
|
||||
|
||||
for (double z = -4.25D; z < 4D; z += 0.5D) {
|
||||
for (double x = -4.75D; x < 4D; x += 0.5D) {
|
||||
assertEquals(0.5D + x * 0.01D + z * 0.02D,
|
||||
generator.getHeight(x, z, 27L), 1e-12D);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reusesLatticeSamplesDuringDenseGeneration() {
|
||||
AtomicInteger samples = new AtomicInteger();
|
||||
IrisGenerator generator = generator((seed, x, z) -> {
|
||||
samples.incrementAndGet();
|
||||
return 0.5D;
|
||||
});
|
||||
generator.setSurfaceDetail(0.5D);
|
||||
|
||||
for (int z = 0; z < 16; z++) {
|
||||
for (int x = 0; x < 16; x++) {
|
||||
assertEquals(0.5D, generator.getHeight(x, z, 27L), 0D);
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(263, samples.get());
|
||||
assertTrue(samples.get() < 256 * 1.1D);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fourBlockPreviewSamplingReusesCoarseCorners() {
|
||||
AtomicInteger samples = new AtomicInteger();
|
||||
IrisGenerator generator = generator((seed, x, z) -> {
|
||||
samples.incrementAndGet();
|
||||
return 0.5D;
|
||||
});
|
||||
generator.setSurfaceDetail(0.5D);
|
||||
|
||||
for (int z = -16; z < 16; z += 4) {
|
||||
for (int x = -16; x < 16; x += 4) {
|
||||
generator.getHeight(x, z, 27L);
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(91, samples.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cachedLatticeHeightsRemainIsolatedBySeed() {
|
||||
IrisGenerator generator = generator((seed, x, z) -> seed * 0.000001D);
|
||||
generator.setSurfaceDetail(0.5D);
|
||||
double first = generator.getHeight(1, 1, 27L);
|
||||
double second = generator.getHeight(1, 1, 28L);
|
||||
|
||||
assertEquals(0.000001D, second - first, 1e-12D);
|
||||
assertEquals(first, generator.getHeight(1, 1, 27L), 0D);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cachedLatticeHeightsRemainIsolatedByEngine() {
|
||||
Engine firstEngine = mock(Engine.class);
|
||||
Engine secondEngine = mock(Engine.class);
|
||||
AtomicReference<Engine> activeEngine = new AtomicReference<>(firstEngine);
|
||||
IrisData data = mock(IrisData.class);
|
||||
when(data.getEngine()).thenAnswer(ignored -> activeEngine.get());
|
||||
IrisGenerator generator = new IrisGenerator().setSurfaceDetail(0.5D).setComposite(new KList<>(
|
||||
new ContextNoise(loader -> loader.getEngine() == firstEngine ? 0.2D : 0.8D)));
|
||||
generator.setLoader(data);
|
||||
|
||||
assertEquals(0.2D, generator.getHeight(0, 0, 27L), 1e-12D);
|
||||
assertEquals(0.2D, generator.getHeight(1, 1, 27L), 1e-12D);
|
||||
activeEngine.set(secondEngine);
|
||||
assertEquals(0.8D, generator.getHeight(0, 0, 27L), 1e-12D);
|
||||
assertEquals(0.8D, generator.getHeight(1, 1, 27L), 1e-12D);
|
||||
activeEngine.set(firstEngine);
|
||||
assertEquals(0.2D, generator.getHeight(1, 1, 27L), 1e-12D);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cachedLatticeHeightsRemainIsolatedByLoader() {
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisData firstData = mock(IrisData.class);
|
||||
IrisData secondData = mock(IrisData.class);
|
||||
when(firstData.getEngine()).thenReturn(engine);
|
||||
when(secondData.getEngine()).thenReturn(engine);
|
||||
IrisGenerator generator = new IrisGenerator().setSurfaceDetail(0.5D).setComposite(new KList<>(
|
||||
new ContextNoise(loader -> loader == firstData ? 0.2D : 0.8D)));
|
||||
generator.setLoader(firstData);
|
||||
|
||||
assertEquals(0.2D, generator.getHeight(0, 0, 27L), 1e-12D);
|
||||
assertEquals(0.2D, generator.getHeight(1, 1, 27L), 1e-12D);
|
||||
generator.setLoader(secondData);
|
||||
assertEquals(0.8D, generator.getHeight(0, 0, 27L), 1e-12D);
|
||||
assertEquals(0.8D, generator.getHeight(1, 1, 27L), 1e-12D);
|
||||
generator.setLoader(firstData);
|
||||
assertEquals(0.2D, generator.getHeight(1, 1, 27L), 1e-12D);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rescalingClearsCachedLatticeHeights() {
|
||||
IrisGenerator generator = generator((seed, x, z) -> x * 0.01D + z * 0.02D);
|
||||
generator.setSurfaceDetail(0.5D);
|
||||
assertEquals(0.06D, generator.getHeight(2, 2, 27L), 1e-12D);
|
||||
|
||||
generator.rescale(2D);
|
||||
|
||||
assertEquals(0.12D, generator.getHeight(2, 2, 27L), 1e-12D);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void rejectsNonfiniteDetail() {
|
||||
generator((seed, x, z) -> 0D).setSurfaceDetail(Double.NaN).getHeight(1, 1, 27L);
|
||||
}
|
||||
|
||||
private static IrisGenerator generator(Sampler sampler) {
|
||||
return new IrisGenerator().setComposite(new KList<>(new SampledNoise(sampler)));
|
||||
}
|
||||
|
||||
private interface Sampler {
|
||||
double sample(long seed, double x, double z);
|
||||
}
|
||||
|
||||
private static final class SampledNoise extends IrisNoiseGenerator {
|
||||
private final Sampler sampler;
|
||||
|
||||
private SampledNoise(Sampler sampler) {
|
||||
this.sampler = sampler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getNoise(long superSeed, double x, double z, IrisData data) {
|
||||
return sampler.sample(superSeed, x, z);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ContextNoise extends IrisNoiseGenerator {
|
||||
private final ToDoubleFunction<IrisData> sampler;
|
||||
|
||||
private ContextNoise(ToDoubleFunction<IrisData> sampler) {
|
||||
this.sampler = sampler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getNoise(long superSeed, double x, double z, IrisData data) {
|
||||
return sampler.applyAsDouble(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.GameMode;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
@@ -41,6 +42,30 @@ import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class WorldBlockDropRouterTest {
|
||||
@Test
|
||||
public void packWithoutDropRulesAllowsTheFirstCreativeAndSurvivalBreakWhileBiomesAreCold() {
|
||||
for (GameMode mode : new GameMode[] {GameMode.CREATIVE, GameMode.SURVIVAL}) {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
when(fixture.data.hasBlockDropRules("minecraft:stone")).thenReturn(false);
|
||||
when(fixture.data.hasBlockDropRules("minecraft:grass_block")).thenReturn(true);
|
||||
when(fixture.player.getGameMode()).thenReturn(mode);
|
||||
fixture.queries.when(() -> EngineBukkitOps.getBiome(fixture.engine, fixture.location))
|
||||
.thenThrow(new SavedBiomeUnavailableException("Biome loading", true));
|
||||
BlockBreakEvent event = fixture.event();
|
||||
|
||||
fixture.router.onBlockBreak(event);
|
||||
|
||||
assertFalse(mode.name(), event.isCancelled());
|
||||
assertTrue(event.isDropItems());
|
||||
fixture.queries.verifyNoInteractions();
|
||||
verify(fixture.drops, never()).fillDrops(anyBoolean(), any());
|
||||
assertEquals(1, fixture.delayed.size());
|
||||
fixture.delayed.remove().run();
|
||||
assertEquals(1, fixture.markers.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void coldBiomeCancelsBeforeChangingDropsOrSchedulingWork() {
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
@@ -206,6 +231,7 @@ public class WorldBlockDropRouterTest {
|
||||
when(manager.getEngine()).thenReturn(engine);
|
||||
when(manager.getTarget()).thenReturn(target);
|
||||
when(manager.getData()).thenReturn(data);
|
||||
when(data.hasBlockDropRules(anyString())).thenReturn(true);
|
||||
when(target.getWorld()).thenReturn(irisWorld);
|
||||
when(engine.getWorld()).thenReturn(irisWorld);
|
||||
when(irisWorld.minHeight()).thenReturn(-64);
|
||||
@@ -217,6 +243,7 @@ public class WorldBlockDropRouterTest {
|
||||
when(block.getZ()).thenReturn(32);
|
||||
when(block.getLocation()).thenReturn(location);
|
||||
when(block.getBlockData()).thenReturn(blockData);
|
||||
when(blockData.getAsString()).thenReturn("minecraft:stone");
|
||||
when(biome.getBlockDrops()).thenReturn(new KList<>(drops));
|
||||
when(region.getBlockDrops()).thenReturn(new KList<>());
|
||||
when(dimension.getBlockDrops()).thenReturn(new KList<>());
|
||||
|
||||
@@ -3,6 +3,9 @@ package art.arcane.iris.pack.loading;
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.generation.runtime.PreservationRegistry;
|
||||
import art.arcane.iris.generation.noise.IrisGenerator;
|
||||
import art.arcane.iris.generation.block.B;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.pack.validation.ContentGate;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.testsupport.PlatformLeakGuard;
|
||||
import org.junit.After;
|
||||
@@ -11,6 +14,7 @@ import org.junit.ClassRule;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
@@ -25,6 +29,8 @@ import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class IrisDataAuthoringCacheTest {
|
||||
@ClassRule
|
||||
@@ -105,4 +111,61 @@ public class IrisDataAuthoringCacheTest {
|
||||
assertSame(keys, entry.getKey().getPossibleKeys());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dropRulePresenceIncludesEachScopeAndIsResetByPackReload() throws Exception {
|
||||
for (String folder : List.of("dimensions", "regions", "biomes")) {
|
||||
Path root = temporary.newFolder(folder + "-drops").toPath();
|
||||
Files.createDirectories(root.resolve(folder));
|
||||
Path resource = root.resolve(folder + "/sample.json");
|
||||
Files.writeString(resource, "{}");
|
||||
source = IrisData.openRuntime(root.toFile());
|
||||
try (MockedStatic<B> blocks = mockStatic(B.class)) {
|
||||
PlatformBlockState state = mock(PlatformBlockState.class);
|
||||
when(state.placementBaseState()).thenReturn(state);
|
||||
when(state.materialKey()).thenReturn("minecraft:stone");
|
||||
blocks.when(() -> B.getStateOrNull("minecraft:stone", false)).thenReturn(state);
|
||||
source.setContentGate(mock(ContentGate.class));
|
||||
assertTrue(source.hasBlockDropRules("minecraft:stone"));
|
||||
source.prepareBlockDropRules();
|
||||
assertFalse(source.hasBlockDropRules("minecraft:stone"));
|
||||
Files.writeString(resource, "{\"blockDrops\":[{\"blocks\":[{\"block\":\"minecraft:stone\"}]}]}");
|
||||
source.hotloaded();
|
||||
source.setContentGate(mock(ContentGate.class));
|
||||
assertTrue(source.hasBlockDropRules("minecraft:stone"));
|
||||
source.prepareBlockDropRules();
|
||||
assertTrue(source.hasBlockDropRules("minecraft:stone"));
|
||||
assertFalse(source.hasBlockDropRules("minecraft:grass_block"));
|
||||
} finally {
|
||||
source.close();
|
||||
source = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void customDropCandidatesUseTheResolvedCarrierAndKeepExactStateRulesEligible() throws Exception {
|
||||
Path root = temporary.newFolder("custom-drops").toPath();
|
||||
Files.createDirectories(root.resolve("biomes"));
|
||||
Files.writeString(root.resolve("biomes/sample.json"), """
|
||||
{"blockDrops":[{"exactBlocks":true,"blocks":[{"block":"rocks:ruby_ore"}]}]}
|
||||
""");
|
||||
source = IrisData.openRuntime(root.toFile());
|
||||
source.setContentGate(mock(ContentGate.class));
|
||||
PlatformBlockState custom = mock(PlatformBlockState.class);
|
||||
PlatformBlockState carrier = mock(PlatformBlockState.class);
|
||||
when(custom.materialKey()).thenReturn("rocks:ruby_ore");
|
||||
when(custom.placementBaseState()).thenReturn(carrier);
|
||||
when(carrier.materialKey()).thenReturn("minecraft:note_block");
|
||||
|
||||
try (MockedStatic<B> blocks = mockStatic(B.class)) {
|
||||
blocks.when(() -> B.getStateOrNull("rocks:ruby_ore", false)).thenReturn(custom);
|
||||
|
||||
source.prepareBlockDropRules();
|
||||
|
||||
assertTrue(source.hasBlockDropRules("minecraft:note_block"));
|
||||
assertFalse(source.hasBlockDropRules("minecraft:stone"));
|
||||
assertFalse(source.hasBlockDropRules("rocks:ruby_ore"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ public class SchemaBuilderIdentityTest {
|
||||
@ClassRule
|
||||
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
|
||||
|
||||
private static final String EXPECTED_SCHEMA_DIGEST = "da993d49349b961b46f86a4966b8d2ab228a6b005c2a4ee279018e03e1139783";
|
||||
private static final String EXPECTED_SCHEMA_DIGEST = "545cbfcdaa4b4c8dd03fca96f6b2617c37bbe9bf73d2011679d2fabcf3f2a6be";
|
||||
|
||||
private static final List<Class<?>> SCHEMA_ROOTS = List.of(
|
||||
IrisDimension.class,
|
||||
|
||||
@@ -22,6 +22,7 @@ import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.pack.loading.IrisRegistrant;
|
||||
import art.arcane.iris.pack.loading.ResourceLoader;
|
||||
import art.arcane.iris.generation.block.IrisBlockData;
|
||||
import art.arcane.iris.generation.decoration.IrisStiltSettings;
|
||||
import art.arcane.iris.generation.noise.IrisExpression;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.structure.jigsaw.IrisJigsawPiece;
|
||||
@@ -124,6 +125,17 @@ public class SchemaBuilderParityTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stiltExclusionsExposeRegisteredMaterialKeys() {
|
||||
IrisData data = structureSchemaData();
|
||||
when(data.getBlockLoader().getPossibleKeys()).thenReturn(new String[] {"minecraft:barrel", "minecraft:calcite"});
|
||||
JSONObject schema = new SchemaBuilder(IrisStiltSettings.class, data).construct();
|
||||
JSONObject exclude = rootProperties(schema).getJSONObject("exclude");
|
||||
|
||||
assertEquals("array", exclude.getString("type"));
|
||||
assertTrue(exclude.getJSONObject("items").getString("$ref").endsWith("enum-block-type"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void registryDependentEnumsMatchLegacyTransforms() {
|
||||
JSONObject definitions = new SchemaBuilder(RegistryModel.class, (IrisData) null).construct().getJSONObject("definitions");
|
||||
|
||||
+193
@@ -1,6 +1,11 @@
|
||||
package art.arcane.iris.structure.object;
|
||||
|
||||
import art.arcane.iris.generation.block.TileData;
|
||||
import art.arcane.iris.generation.decoration.IrisStiltSettings;
|
||||
import art.arcane.iris.generation.block.IrisBlockData;
|
||||
import art.arcane.iris.generation.decoration.formation.FormationGenerator;
|
||||
import art.arcane.iris.generation.decoration.formation.IrisFormation;
|
||||
import art.arcane.iris.generation.decoration.formation.IrisFormationForm;
|
||||
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
|
||||
import art.arcane.iris.generation.noise.NoiseStyle;
|
||||
import art.arcane.iris.generation.terrain.IrisMaterialPalette;
|
||||
@@ -415,6 +420,194 @@ public class IrisObjectPlacementRunnerRegressionTest {
|
||||
assertTrue(placer.writes().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void everyStiltModePreservesTheBarrelAndItsTileWithoutRepeatingIt() {
|
||||
PlatformBlockState barrel = state("minecraft:barrel[facing=up,open=false]", true);
|
||||
when(barrel.materialKey()).thenReturn("minecraft:barrel");
|
||||
when(barrel.isStorage()).thenReturn(true);
|
||||
when(barrel.hasTileEntity()).thenReturn(true);
|
||||
TileData tile = new TileData("minecraft:barrel", new KMap<>());
|
||||
for (ObjectPlaceMode mode : List.of(ObjectPlaceMode.STILT, ObjectPlaceMode.FAST_STILT,
|
||||
ObjectPlaceMode.MIN_STILT, ObjectPlaceMode.FAST_MIN_STILT, ObjectPlaceMode.CENTER_STILT,
|
||||
ObjectPlaceMode.ERODE_STILT, ObjectPlaceMode.ORGANIC_STILT, ObjectPlaceMode.CEILING_HANG)) {
|
||||
RecordingPlacer placer = new RecordingPlacer(null);
|
||||
for (int x = -2; x <= 2; x++) {
|
||||
for (int z = -2; z <= 2; z++) {
|
||||
placer.set(x, ANCHOR_Y, z, solid);
|
||||
placer.set(x, ANCHOR_Y + 20, z, solid);
|
||||
}
|
||||
}
|
||||
placer.writes().clear();
|
||||
IrisObject object = boxObject(5, 1, 5);
|
||||
object.setUnsigned(3, 0, 2, barrel);
|
||||
object.setUnsignedTile(3, 0, 2, tile);
|
||||
IrisObjectPlacement placement = placement().setMode(mode)
|
||||
.setRotation(IrisObjectRotation.of(0, 0, 0))
|
||||
.setStiltSettings(new IrisStiltSettings().setOrganicJitter(0).setOrganicScratch(0));
|
||||
|
||||
object.place(0, ANCHOR_Y + 10, 0, placer, placement, new RNG(2L), data);
|
||||
|
||||
assertEquals(mode.name(), 1, placer.writesOf(barrel).size());
|
||||
assertSame(mode.name(), barrel, placer.get(1, ANCHOR_Y + 10, 0));
|
||||
assertSame(mode.name(), tile, placer.getData(1, ANCHOR_Y + 10, 0, TileData.class));
|
||||
assertTrue(mode.name(), placer.writesOf(solid).stream()
|
||||
.anyMatch(write -> write.y() != ANCHOR_Y + 10));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void excludedSourceMaterialKeepsItsBlockAndDoesNotUseTheStiltPalette() {
|
||||
PlatformBlockState calcite = state("minecraft:calcite", true);
|
||||
PlatformBlockState cobblestone = state("minecraft:cobblestone", true);
|
||||
IrisMaterialPalette palette = mock(IrisMaterialPalette.class);
|
||||
when(palette.get(any(RNG.class), anyDouble(), anyDouble(), anyDouble(), any(IrisData.class)))
|
||||
.thenReturn(cobblestone);
|
||||
IrisObject object = lineObject(3);
|
||||
object.setUnsigned(2, 0, 0, calcite);
|
||||
RecordingPlacer placer = new RecordingPlacer(null);
|
||||
IrisObjectPlacement placement = placement().setMode(ObjectPlaceMode.CENTER_STILT)
|
||||
.setRotation(IrisObjectRotation.of(0, 0, 0))
|
||||
.setStiltSettings(new IrisStiltSettings().setExclude(new KList<>("CALCITE")).setPalette(palette));
|
||||
|
||||
object.place(0, ANCHOR_Y + 10, 0, placer, placement, new RNG(2L), data);
|
||||
|
||||
assertEquals(1, placer.writesOf(calcite).size());
|
||||
assertSame(calcite, placer.get(1, ANCHOR_Y + 10, 0));
|
||||
assertNull(placer.get(1, ANCHOR_Y + 9, 0));
|
||||
assertSame(cobblestone, placer.get(0, ANCHOR_Y + 9, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void translucentFullBlocksGrowFoundationsWhileCrystalModelsRemainUnextended() {
|
||||
PlatformBlockState ice = state("minecraft:ice", true);
|
||||
when(ice.isOccluding()).thenReturn(false);
|
||||
PlatformBlockState cluster = state("minecraft:amethyst_cluster", true);
|
||||
when(cluster.isOccluding()).thenReturn(false);
|
||||
IrisObject object = new IrisObject(3, 1, 1);
|
||||
object.setUnsigned(0, 0, 0, ice);
|
||||
object.setUnsigned(2, 0, 0, cluster);
|
||||
IrisMaterialPalette palette = mock(IrisMaterialPalette.class);
|
||||
when(palette.get(any(RNG.class), anyDouble(), anyDouble(), anyDouble(), any(IrisData.class)))
|
||||
.thenReturn(solid);
|
||||
RecordingPlacer placer = new RecordingPlacer(null);
|
||||
IrisObjectPlacement placement = placement().setMode(ObjectPlaceMode.CENTER_STILT)
|
||||
.setRotation(IrisObjectRotation.of(0, 0, 0))
|
||||
.setStiltSettings(new IrisStiltSettings().setPalette(palette));
|
||||
|
||||
object.place(0, ANCHOR_Y + 10, 0, placer, placement, new RNG(2L), data);
|
||||
|
||||
assertSame(solid, placer.get(-1, ANCHOR_Y + 9, 0));
|
||||
assertEquals(1, placer.writesOf(cluster).size());
|
||||
assertNull(placer.get(1, ANCHOR_Y + 9, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generatedGlacialOverhangGrowsPackedIceRootsToTerrain() {
|
||||
PlatformBlockState ice = state("minecraft:ice", true);
|
||||
when(ice.isOccluding()).thenReturn(false);
|
||||
PlatformBlockState packedIce = state("minecraft:packed_ice", true);
|
||||
IrisMaterialPalette body = mock(IrisMaterialPalette.class);
|
||||
when(body.getPalette()).thenReturn(new KList<>(new IrisBlockData("minecraft:ice")));
|
||||
when(body.get(any(RNG.class), anyDouble(), anyDouble(), anyDouble(), any(IrisData.class)))
|
||||
.thenReturn(ice);
|
||||
IrisMaterialPalette roots = mock(IrisMaterialPalette.class);
|
||||
when(roots.get(any(RNG.class), anyDouble(), anyDouble(), anyDouble(), any(IrisData.class)))
|
||||
.thenReturn(packedIce);
|
||||
IrisFormation formation = new IrisFormation().setForm(IrisFormationForm.OVERHANG)
|
||||
.setHeightMin(16).setHeightMax(25).setBaseWidthMin(5).setBaseWidthMax(8)
|
||||
.setOverhangReach(13).setOverhangDrop(6).setRoughness(0.3).setJitter(0.08)
|
||||
.setBlockPalette(body);
|
||||
IrisObject object = FormationGenerator.generate(formation, 16, new RNG(5055004L + 16L), data);
|
||||
RecordingPlacer placer = new RecordingPlacer(null);
|
||||
for (int x = -32; x <= 32; x++) {
|
||||
for (int z = -32; z <= 32; z++) {
|
||||
placer.set(x, ANCHOR_Y, z, solid);
|
||||
}
|
||||
}
|
||||
placer.writes().clear();
|
||||
IrisObjectPlacement placement = placement().setMode(ObjectPlaceMode.ORGANIC_STILT)
|
||||
.setRotation(IrisObjectRotation.of(0, 0, 0))
|
||||
.setStiltSettings(new IrisStiltSettings().setOrganicMaxScan(96).setOrganicJitter(0)
|
||||
.setOrganicScratch(0).setPalette(roots));
|
||||
|
||||
object.place(0, ANCHOR_Y + object.getCenter().getBlockY() + 12, 0, placer, placement,
|
||||
new RNG(2L), data);
|
||||
|
||||
List<BlockWrite> rootWrites = placer.writesOf(packedIce);
|
||||
assertFalse(rootWrites.isEmpty());
|
||||
assertTrue(rootWrites.stream().anyMatch(write -> write.y() == ANCHOR_Y + 1));
|
||||
for (BlockWrite root : rootWrites) {
|
||||
for (int y = ANCHOR_Y + 1; y < root.y(); y++) {
|
||||
assertSame(packedIce, placer.get(root.x(), y, root.z()));
|
||||
}
|
||||
}
|
||||
assertFalse(placer.writesOf(ice).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void excludedLowestBlockDoesNotMoveTheFoundationUpIntoTheOriginalObject() {
|
||||
PlatformBlockState machine = state("test:machine", true);
|
||||
when(machine.hasTileEntity()).thenReturn(true);
|
||||
IrisObject object = new IrisObject(1, 3, 1);
|
||||
object.setUnsigned(0, 0, 0, machine);
|
||||
object.setUnsigned(0, 1, 0, solid);
|
||||
RecordingPlacer placer = new RecordingPlacer(null);
|
||||
|
||||
object.place(0, ANCHOR_Y + 10, 0, placer, placement().setMode(ObjectPlaceMode.CENTER_STILT),
|
||||
new RNG(2L), data);
|
||||
|
||||
assertEquals(2, placer.writes().size());
|
||||
assertSame(machine, placer.get(0, ANCHOR_Y + 9, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stiltPaletteCannotIntroduceExcludedMaterialsOrContainers() {
|
||||
PlatformBlockState calcite = state("minecraft:calcite", true);
|
||||
PlatformBlockState barrel = state("minecraft:barrel", true);
|
||||
when(barrel.isStorage()).thenReturn(true);
|
||||
for (PlatformBlockState replacement : List.of(calcite, barrel)) {
|
||||
IrisMaterialPalette palette = mock(IrisMaterialPalette.class);
|
||||
when(palette.get(any(RNG.class), anyDouble(), anyDouble(), anyDouble(), any(IrisData.class)))
|
||||
.thenReturn(replacement);
|
||||
RecordingPlacer placer = new RecordingPlacer(null);
|
||||
IrisObjectPlacement placement = placement().setMode(ObjectPlaceMode.CENTER_STILT)
|
||||
.setStiltSettings(new IrisStiltSettings().setExclude(new KList<>("minecraft:calcite"))
|
||||
.setPalette(palette));
|
||||
|
||||
lineObject(3).place(0, ANCHOR_Y + 10, 0, placer, placement, new RNG(2L), data);
|
||||
|
||||
assertEquals(replacement.key(), 3, placer.writes().size());
|
||||
assertTrue(placer.writesOf(replacement).isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void objectEditsCannotIntroduceExcludedMaterialsOrTileEntitiesIntoStilts() {
|
||||
PlatformBlockState calcite = state("minecraft:calcite", true);
|
||||
PlatformBlockState machine = state("test:machine", true);
|
||||
when(machine.hasTileEntity()).thenReturn(true);
|
||||
for (PlatformBlockState replacement : List.of(calcite, machine)) {
|
||||
IrisObjectReplace edit = mock(IrisObjectReplace.class);
|
||||
IrisMaterialPalette palette = mock(IrisMaterialPalette.class);
|
||||
when(edit.getChance()).thenReturn(1F);
|
||||
when(edit.getFind(data)).thenReturn(new KList<>(solid));
|
||||
when(edit.getReplace(any(RNG.class), anyDouble(), anyDouble(), anyDouble(), any(IrisData.class)))
|
||||
.thenReturn(replacement);
|
||||
when(edit.getReplace()).thenReturn(palette);
|
||||
when(palette.getTile(any(RNG.class), anyDouble(), anyDouble(), anyDouble(), any(IrisData.class)))
|
||||
.thenReturn(Optional.empty());
|
||||
RecordingPlacer placer = new RecordingPlacer(null);
|
||||
IrisObjectPlacement placement = placement().setMode(ObjectPlaceMode.CENTER_STILT)
|
||||
.setStiltSettings(new IrisStiltSettings().setExclude(new KList<>("minecraft:calcite")));
|
||||
placement.getEdit().add(edit);
|
||||
|
||||
lineObject(3).place(0, ANCHOR_Y + 10, 0, placer, placement, new RNG(2L), data);
|
||||
|
||||
assertEquals(replacement.key(), 3, placer.writesOf(replacement).size());
|
||||
assertTrue(placer.writesOf(replacement).stream().allMatch(write -> write.y() == ANCHOR_Y + 10));
|
||||
}
|
||||
}
|
||||
|
||||
private IrisObjectPlacement placement() {
|
||||
IrisObjectPlacement placement = new IrisObjectPlacement();
|
||||
placement.setMode(ObjectPlaceMode.CENTER_HEIGHT);
|
||||
|
||||
+21
@@ -139,6 +139,27 @@ public class IrisRendererPerformanceContractTest {
|
||||
assertTrue(samples.get() < 128);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void studioBiomeBaseDoesNotEnterHydrologyPlanning() {
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
IrisBiome biome = mock(IrisBiome.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
ProceduralStream<IrisBiome> base = mock(ProceduralStream.class);
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
when(complex.getBaseBiomeStream()).thenReturn(base);
|
||||
when(base.get(org.mockito.ArgumentMatchers.anyDouble(), org.mockito.ArgumentMatchers.anyDouble()))
|
||||
.thenReturn(biome);
|
||||
when(biome.getColor(engine, RenderType.BIOME)).thenReturn(Color.GREEN);
|
||||
|
||||
BufferedImage image = new IrisRenderer(engine).renderStudioBase(
|
||||
0D, 0D, 256D, 64, RenderType.BIOME, () -> false);
|
||||
|
||||
assertEquals(Color.GREEN.getRGB(), image.getRGB(0, 0));
|
||||
assertEquals(Color.GREEN.getRGB(), image.getRGB(63, 63));
|
||||
verify(complex, never()).getHydrologyRuntime();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void planarStudioHeightPagesInterpolateWithoutPerPixelEngineSampling() {
|
||||
Engine engine = mock(Engine.class);
|
||||
|
||||
@@ -134,6 +134,32 @@ public class IrisRendererRiverTest {
|
||||
verify(runtime).sampleRenderFootprint(12D, -7D, 20D, 1D);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void biomeRefinementCopiesTheBaseAndRetainsTheFullRiverOverlay() {
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
IrisHydrologyRuntime runtime = mock(IrisHydrologyRuntime.class);
|
||||
IrisBiome biome = mock(IrisBiome.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
ProceduralStream<IrisBiome> base = mock(ProceduralStream.class);
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
when(complex.getHydrologyRuntime()).thenReturn(runtime);
|
||||
when(complex.getBaseBiomeStream()).thenReturn(base);
|
||||
when(base.get(12D, -7D)).thenReturn(biome);
|
||||
when(biome.getColor(engine, RenderType.BIOME)).thenReturn(Color.GREEN);
|
||||
when(runtime.sampleRenderFootprint(12D, -7D, 20D, 1D))
|
||||
.thenReturn(sample(HydrologyFeatureType.SURFACE_POOL, false));
|
||||
IrisRenderer renderer = new IrisRenderer(engine);
|
||||
BufferedImage preview = renderer.renderStudioBase(12D, -7D, 8D, 1, RenderType.BIOME, () -> false);
|
||||
|
||||
BufferedImage refined = renderer.refineStudioBiome(12D, -7D, 8D, preview, () -> false);
|
||||
BufferedImage complete = renderer.renderStudio(12D, -7D, 8D, 1, RenderType.BIOME, () -> false);
|
||||
|
||||
assertEquals(Color.GREEN.getRGB(), preview.getRGB(0, 0));
|
||||
assertNotEquals(preview.getRGB(0, 0), refined.getRGB(0, 0));
|
||||
assertEquals(complete.getRGB(0, 0), refined.getRGB(0, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void riverVisionShowsAProjectedCandidateWithoutTreatingItAsAccepted() {
|
||||
Engine engine = mock(Engine.class);
|
||||
|
||||
@@ -11,6 +11,7 @@ import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.BooleanSupplier;
|
||||
@@ -68,7 +69,7 @@ public class VisionRenderControllerTest {
|
||||
public void everyPublishedPageIsFinalResolutionAndIdenticalRequestsAreCacheHits() throws Exception {
|
||||
IrisRenderer renderer = mock(IrisRenderer.class);
|
||||
AtomicInteger calls = new AtomicInteger();
|
||||
when(renderer.renderStudio(
|
||||
when(renderer.renderStudioBase(
|
||||
anyDouble(),
|
||||
anyDouble(),
|
||||
anyDouble(),
|
||||
@@ -84,6 +85,7 @@ public class VisionRenderControllerTest {
|
||||
BufferedImage.TYPE_INT_RGB
|
||||
);
|
||||
});
|
||||
passThroughRefinement(renderer);
|
||||
VisionRenderController controller = controller(2);
|
||||
VisionRenderController.RenderSpec spec = spec(renderer, RenderType.BIOME, 1L, 0D, 0D, 4D, 128, 128);
|
||||
try {
|
||||
@@ -153,6 +155,12 @@ public class VisionRenderControllerTest {
|
||||
latestStarted.countDown();
|
||||
return new BufferedImage(64, 64, BufferedImage.TYPE_INT_RGB);
|
||||
});
|
||||
when(renderer.renderStudioBase(anyDouble(), anyDouble(), anyDouble(), anyInt(),
|
||||
any(RenderType.class), any(BooleanSupplier.class))).thenAnswer(invocation -> {
|
||||
latestStarted.countDown();
|
||||
return new BufferedImage(64, 64, BufferedImage.TYPE_INT_RGB);
|
||||
});
|
||||
passThroughRefinement(renderer);
|
||||
VisionRenderController controller = controller(2);
|
||||
try {
|
||||
controller.request(spec(renderer, RenderType.REGION, 1L, 0D, 0D, 4D, 64, 64));
|
||||
@@ -166,6 +174,103 @@ public class VisionRenderControllerTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void completeBaseIsVisibleBeforeBlockedHydrologyRefinementStarts() throws Exception {
|
||||
IrisRenderer renderer = exactRenderer();
|
||||
CountDownLatch overlayStarted = new CountDownLatch(1);
|
||||
CountDownLatch releaseOverlay = new CountDownLatch(1);
|
||||
AtomicInteger baseCalls = new AtomicInteger();
|
||||
AtomicInteger overlays = new AtomicInteger();
|
||||
when(renderer.renderStudioBase(anyDouble(), anyDouble(), anyDouble(), anyInt(),
|
||||
any(RenderType.class), any(BooleanSupplier.class))).thenAnswer(invocation -> {
|
||||
baseCalls.incrementAndGet();
|
||||
return new BufferedImage(64, 64, BufferedImage.TYPE_INT_RGB);
|
||||
});
|
||||
when(renderer.refineStudioBiome(anyDouble(), anyDouble(), anyDouble(), any(BufferedImage.class),
|
||||
any(BooleanSupplier.class))).thenAnswer(invocation -> {
|
||||
overlays.incrementAndGet();
|
||||
overlayStarted.countDown();
|
||||
assertTrue(releaseOverlay.await(2L, TimeUnit.SECONDS));
|
||||
return invocation.getArgument(3);
|
||||
});
|
||||
VisionRenderController controller = controller(3);
|
||||
try {
|
||||
VisionRenderController.RenderSpec spec = spec(renderer, RenderType.BIOME, 1L, 0D, 0D, 4D, 192, 128);
|
||||
VisionRenderController.Frame frame = controller.request(spec);
|
||||
assertTrue(overlayStarted.await(2L, TimeUnit.SECONDS));
|
||||
assertEquals(frame.tiles().size(), baseCalls.get());
|
||||
assertEquals(1, overlays.get());
|
||||
assertEquals(0, controller.progress(frame).ready());
|
||||
for (VisionRenderController.VisibleTile tile : frame.tiles()) {
|
||||
assertEquals(64, controller.image(frame, tile).getWidth());
|
||||
}
|
||||
|
||||
releaseOverlay.countDown();
|
||||
Await.until("every biome overlay to complete", Duration.ofSeconds(2L),
|
||||
() -> controller.progress(frame).ready() == frame.tiles().size());
|
||||
assertEquals(frame.tiles().size(), overlays.get());
|
||||
|
||||
VisionRenderController.Frame cached = controller.request(spec);
|
||||
assertEquals(frame.tiles().size(), controller.progress(cached).ready());
|
||||
assertEquals(frame.tiles().size(), baseCalls.get());
|
||||
assertEquals(frame.tiles().size(), overlays.get());
|
||||
} finally {
|
||||
releaseOverlay.countDown();
|
||||
controller.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cancelledOverlayKeepsCachedBaseWithoutMarkingItComplete() throws Exception {
|
||||
IrisRenderer renderer = exactRenderer();
|
||||
CountDownLatch staleStarted = new CountDownLatch(1);
|
||||
CountDownLatch latestStarted = new CountDownLatch(1);
|
||||
CountDownLatch releaseLatest = new CountDownLatch(1);
|
||||
AtomicInteger baseCalls = new AtomicInteger();
|
||||
AtomicInteger overlayCalls = new AtomicInteger();
|
||||
when(renderer.renderStudioBase(anyDouble(), anyDouble(), anyDouble(), anyInt(),
|
||||
any(RenderType.class), any(BooleanSupplier.class))).thenAnswer(invocation -> {
|
||||
baseCalls.incrementAndGet();
|
||||
return new BufferedImage(64, 64, BufferedImage.TYPE_INT_RGB);
|
||||
});
|
||||
when(renderer.refineStudioBiome(anyDouble(), anyDouble(), anyDouble(), any(BufferedImage.class),
|
||||
any(BooleanSupplier.class))).thenAnswer(invocation -> {
|
||||
if (overlayCalls.getAndIncrement() == 0) {
|
||||
staleStarted.countDown();
|
||||
BooleanSupplier cancelled = invocation.getArgument(4);
|
||||
while (!cancelled.getAsBoolean()) {
|
||||
Thread.onSpinWait();
|
||||
}
|
||||
throw new CancellationException();
|
||||
}
|
||||
latestStarted.countDown();
|
||||
assertTrue(releaseLatest.await(2L, TimeUnit.SECONDS));
|
||||
return invocation.getArgument(3);
|
||||
});
|
||||
VisionRenderController controller = controller(2);
|
||||
try {
|
||||
VisionRenderController.RenderSpec spec = spec(renderer, RenderType.BIOME, 1L, 0D, 0D, 4D, 128, 128);
|
||||
VisionRenderController.Frame stale = controller.request(spec);
|
||||
assertTrue(staleStarted.await(2L, TimeUnit.SECONDS));
|
||||
|
||||
VisionRenderController.Frame latest = controller.request(spec);
|
||||
assertTrue(latestStarted.await(2L, TimeUnit.SECONDS));
|
||||
assertEquals(stale.tiles().size(), baseCalls.get());
|
||||
assertEquals(0, controller.progress(latest).ready());
|
||||
for (VisionRenderController.VisibleTile tile : latest.tiles()) {
|
||||
assertEquals(64, controller.image(latest, tile).getWidth());
|
||||
}
|
||||
|
||||
releaseLatest.countDown();
|
||||
Await.until("the replacement frame to finish its overlays", Duration.ofSeconds(2L),
|
||||
() -> controller.progress(latest).ready() == latest.tiles().size());
|
||||
assertEquals(0, controller.progress(stale).ready());
|
||||
} finally {
|
||||
releaseLatest.countDown();
|
||||
controller.close();
|
||||
}
|
||||
}
|
||||
|
||||
private static VisionRenderController controller(int workers) {
|
||||
return new VisionRenderController(
|
||||
() -> {
|
||||
@@ -188,9 +293,18 @@ public class VisionRenderControllerTest {
|
||||
invocation.getArgument(3),
|
||||
BufferedImage.TYPE_INT_RGB
|
||||
));
|
||||
when(renderer.renderStudioBase(anyDouble(), anyDouble(), anyDouble(), anyInt(),
|
||||
any(RenderType.class), any(BooleanSupplier.class))).thenAnswer(invocation -> new BufferedImage(
|
||||
invocation.getArgument(3), invocation.getArgument(3), BufferedImage.TYPE_INT_RGB));
|
||||
passThroughRefinement(renderer);
|
||||
return renderer;
|
||||
}
|
||||
|
||||
private static void passThroughRefinement(IrisRenderer renderer) {
|
||||
when(renderer.refineStudioBiome(anyDouble(), anyDouble(), anyDouble(), any(BufferedImage.class),
|
||||
any(BooleanSupplier.class))).thenAnswer(invocation -> invocation.getArgument(3));
|
||||
}
|
||||
|
||||
private static Set<VisionRenderController.TileKey> keys(VisionRenderController.Frame frame) {
|
||||
Set<VisionRenderController.TileKey> keys = new HashSet<>();
|
||||
for (VisionRenderController.VisibleTile tile : frame.tiles()) {
|
||||
|
||||
@@ -37,6 +37,7 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
@@ -45,6 +46,7 @@ import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
|
||||
public class SavedBiomeRuntimeTest {
|
||||
@ClassRule
|
||||
@@ -102,6 +104,39 @@ public class SavedBiomeRuntimeTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void packWithoutDropRulesDoesNotNeedAChunkQueryForTheOnlyEpoch() throws Exception {
|
||||
IrisData current = mock(IrisData.class);
|
||||
when(engine.getData()).thenReturn(current);
|
||||
GenerationManifest manifest = history.manifest();
|
||||
GenerationEpoch active = manifest.epoch("a".repeat(64)).orElseThrow();
|
||||
when(manifest.activeEpoch()).thenReturn(active);
|
||||
when(manifest.epochs()).thenReturn(List.of(active));
|
||||
|
||||
try (SavedBiomeRuntime runtime = new SavedBiomeRuntime(engine, history)) {
|
||||
assertFalse(runtime.mayHaveBlockDropRules("minecraft:stone"));
|
||||
verifyNoInteractions(store);
|
||||
when(current.hasBlockDropRules("minecraft:stone")).thenReturn(true);
|
||||
assertTrue(runtime.mayHaveBlockDropRules("minecraft:stone"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unloadedHistoricalPackRetainsDropChecksWhenTheCurrentPackHasNoRules() throws Exception {
|
||||
when(engine.getData()).thenReturn(mock(IrisData.class));
|
||||
GenerationManifest manifest = history.manifest();
|
||||
GenerationEpoch previous = manifest.epoch("a".repeat(64)).orElseThrow();
|
||||
GenerationEpoch active = mock(GenerationEpoch.class);
|
||||
when(active.epochId()).thenReturn("b".repeat(64));
|
||||
when(manifest.activeEpoch()).thenReturn(active);
|
||||
when(manifest.epochs()).thenReturn(List.of(previous, active));
|
||||
|
||||
try (SavedBiomeRuntime runtime = new SavedBiomeRuntime(engine, history)) {
|
||||
assertTrue(runtime.mayHaveBlockDropRules("minecraft:stone"));
|
||||
verifyNoInteractions(store);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void chunkReadinessDoesNotRequireEveryCellToHaveAResolvedIdentity() throws Exception {
|
||||
when(store.get(0, 0)).thenReturn(Optional.of(unresolvedChunk(1)));
|
||||
|
||||
Reference in New Issue
Block a user