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:
Brian Neumann-Fopiano committed 2026-09-13 17:00:33 -04:00
1 parent e3c6a32968
commit 9a1e4b91d1
25 files changed
+1290 -84

No files matched your search

@@ -78,10 +78,6 @@ final class IrisBiomeLayerGenerator {
for (int i = 0; i < layerCount; i++) { for (int i = 0; i < layerCount; i++) {
IrisBiomePaletteLayer layer = layers.get(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(); IrisSlopeClip sc = layer.getSlopeCondition();
if (!sc.isDefault()) { if (!sc.isDefault()) {
@@ -90,10 +86,13 @@ final class IrisBiomeLayerGenerator {
sampledSlope = true; sampledSlope = true;
} }
if (!sc.isValid(surfaceSlope)) { 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) { if (d <= 0) {
continue; 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) { 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> data = new KList<>();
KList<PlatformBlockState> real = new KList<>();
int maxDepth = Math.min(maxDepthf, biome.getLockLayersMax()); 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; return data;
} }
KList<IrisBiomePaletteLayer> layers = biome.getLayers(); KList<CNG> heightGenerators = getLayerHeightGenerators(biome, random, rdata);
int layerCount = layers.size(); int[] boundaries = new int[layerCount + 1];
double surfaceSlope = 0D;
if (layerCount > 0) { boolean sampledSlope = false;
KList<CNG> heightGenerators = getLayerHeightGenerators(biome, random, rdata); for (int i = 0; i < layerCount; i++) {
double surfaceSlope = 0D; IrisBiomePaletteLayer layer = layers.get(i);
boolean sampledSlope = false; IrisSlopeClip sc = layer.getSlopeCondition();
boundaries[i + 1] = boundaries[i];
for (int i = 0; i < layerCount; i++) { if (!sc.isDefault()) {
IrisBiomePaletteLayer layer = layers.get(i); if (!sampledSlope) {
double zoom = layer.getZoom(); surfaceSlope = resolveSurfaceSlope(complex, slopeStream, wx, height, wz);
CNG hgen = heightGenerators.get(i); sampledSlope = true;
int d = hgen.fit(layer.getMinHeight(), layer.getMaxHeight(), wx / zoom, wz / zoom);
IrisSlopeClip sc = layer.getSlopeCondition();
if (!sc.isDefault()) {
if (!sampledSlope) {
surfaceSlope = resolveSurfaceSlope(complex, slopeStream, wx, height, wz);
sampledSlope = true;
}
if (!sc.isValid(surfaceSlope)) {
d = 0;
}
} }
if (!sc.isValid(surfaceSlope)) {
if (d <= 0) {
continue; 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()) { if (sampledDepth == cycleLength && !data.isEmpty()) {
return real; 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++) { private static int findLayer(int[] boundaries, int position) {
long offset = 512L - height - i; int low = 0;
real.add(data.get(Math.floorMod(offset, data.size()))); 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 low;
return real;
} }
private static double resolveSurfaceSlope( private static double resolveSurfaceSlope(
@@ -3,14 +3,21 @@ package art.arcane.iris.generation.decoration;
import art.arcane.iris.generation.terrain.IrisMaterialPalette; import art.arcane.iris.generation.terrain.IrisMaterialPalette;
import art.arcane.volmlib.util.documentation.Description; 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.MaxNumber;
import art.arcane.iris.pack.schema.annotation.MinNumber; 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.iris.pack.schema.annotation.Snippet;
import art.arcane.volmlib.util.collection.KList;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
@Snippet("stilt-settings") @Snippet("stilt-settings")
@Accessors(chain = true) @Accessors(chain = true)
@NoArgsConstructor @NoArgsConstructor
@@ -32,6 +39,10 @@ public class IrisStiltSettings {
private int overStilt; private int overStilt;
@Description("If defined, stilting will be done using this block palette rather than the last layer of the object.") @Description("If defined, stilting will be done using this block palette rather than the last layer of the object.")
private IrisMaterialPalette palette; 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) @MinNumber(1)
@MaxNumber(256) @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.") @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.") @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; 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.Set;
import java.util.concurrent.CancellationException; import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
@@ -890,8 +890,11 @@ public final class HydrologyTileCache implements AutoCloseable {
private static <T> T awaitPlan(CompletableFuture<T> future) { private static <T> T awaitPlan(CompletableFuture<T> future) {
try { try {
return future.join(); return future.get();
} catch (CompletionException failure) { } catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
throw new CancellationException("Hydrology plan wait interrupted.");
} catch (ExecutionException failure) {
Throwable cause = failure.getCause(); Throwable cause = failure.getCause();
if (cause instanceof RuntimeException runtimeException) { if (cause instanceof RuntimeException runtimeException) {
throw runtimeException; throw runtimeException;
@@ -20,6 +20,8 @@ package art.arcane.iris.generation.noise;
import art.arcane.iris.pack.loading.IrisRegistrant; import art.arcane.iris.pack.loading.IrisRegistrant;
import art.arcane.iris.generation.cache.AtomicCache; 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.iris.pack.schema.annotation.ArrayType;
import art.arcane.volmlib.util.documentation.Description; import art.arcane.volmlib.util.documentation.Description;
import art.arcane.iris.pack.schema.annotation.MaxNumber; 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.interpolation.IrisInterpolation;
import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.noise.CellGenerator; import art.arcane.volmlib.util.noise.CellGenerator;
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
import lombok.AccessLevel;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;
import java.util.List; import java.util.List;
@@ -44,13 +50,24 @@ import java.util.List;
@Data @Data
@EqualsAndHashCode(callSuper = false) @EqualsAndHashCode(callSuper = false)
public class IrisGenerator extends IrisRegistrant { 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<>(); private final transient AtomicCache<CellGenerator> cellGen = new AtomicCache<>();
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private transient volatile SurfaceCache surfaceSamples;
@MinNumber(0.001) @MinNumber(0.001)
@Description("The zoom or frequency.") @Description("The zoom or frequency.")
private double zoom = 1; private double zoom = 1;
@MinNumber(0) @MinNumber(0)
@Description("The opacity, essentially a multiplier on the output.") @Description("The opacity, essentially a multiplier on the output.")
private double opacity = 1; 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") @Description("Multiply the compsites instead of adding them")
private boolean multiplicitive = false; private boolean multiplicitive = false;
@MinNumber(0.001) @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) { 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()) { if (composite.isEmpty()) {
return 0; return 0;
} }
@@ -205,6 +253,47 @@ public class IrisGenerator extends IrisRegistrant {
return v; 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) { public double cell(double rx, double rz, double v, double superSeed) {
getCellGenerator(getSeed() + 46222).setShuffle(getCellFractureShuffle()); getCellGenerator(getSeed() + 46222).setShuffle(getCellFractureShuffle());
double fractureX = rx / getCellFractureZoom(); double fractureX = rx / getCellFractureZoom();
@@ -231,6 +320,7 @@ public class IrisGenerator extends IrisRegistrant {
public IrisGenerator rescale(double scale) { public IrisGenerator rescale(double scale) {
zoom /= scale; zoom /= scale;
surfaceSamples = null;
return this; return this;
} }
@@ -253,4 +343,26 @@ public class IrisGenerator extends IrisRegistrant {
public String getTypeName() { public String getTypeName() {
return "Generator"; 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 @Override
public void start() { public void start() {
getData().prepareBlockDropRules();
super.start(); super.start();
if (!looper.isAlive()) { if (!looper.isAlive()) {
looper.start(); looper.start();
@@ -21,12 +21,14 @@ package art.arcane.iris.generation.runtime;
import art.arcane.iris.pack.loading.IrisData; import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.world.tree.BlockDropRouter; import art.arcane.iris.world.tree.BlockDropRouter;
import art.arcane.iris.world.history.SavedBiomeUnavailableException; 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.generation.biome.IrisBiome;
import art.arcane.iris.world.loot.IrisBlockDrops; import art.arcane.iris.world.loot.IrisBlockDrops;
import art.arcane.iris.world.entity.IrisMarker; import art.arcane.iris.world.entity.IrisMarker;
import art.arcane.iris.generation.terrain.IrisRegion; import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.platform.generation.EngineBukkitOps; import art.arcane.iris.platform.generation.EngineBukkitOps;
import art.arcane.iris.platform.bukkit.BukkitWorldBinding; import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import art.arcane.iris.platform.bukkit.BukkitBlockState;
import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.world.task.J; import art.arcane.iris.world.task.J;
import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KList;
@@ -139,6 +141,9 @@ final class WorldBlockDropRouter {
} }
private List<IrisBlockDrops> resolveDropProviders(BlockBreakEvent event) { 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()); IrisBiome biome = EngineBukkitOps.getBiome(manager.getEngine(), event.getBlock().getLocation());
List<IrisBlockDrops> providers = filterDrops(biome.getBlockDrops(), event, manager.getData()); List<IrisBlockDrops> providers = filterDrops(biome.getBlockDrops(), event, manager.getData());
if (providers.stream().noneMatch(IrisBlockDrops::isSkipParents)) { if (providers.stream().noneMatch(IrisBlockDrops::isSkipParents)) {
@@ -148,4 +153,15 @@ final class WorldBlockDropRouter {
} }
return providers; 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.pack.PackDirectoryResolver;
import art.arcane.iris.generation.cache.AtomicCache; import art.arcane.iris.generation.cache.AtomicCache;
import art.arcane.iris.generation.runtime.Engine; 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.placement.IrisStructureLocator;
import art.arcane.iris.structure.graph.StructureGraphCatalog; import art.arcane.iris.structure.graph.StructureGraphCatalog;
import art.arcane.iris.world.history.GenerationPackFingerprint; import art.arcane.iris.world.history.GenerationPackFingerprint;
@@ -102,6 +105,7 @@ import java.util.Optional;
import java.util.Collections; import java.util.Collections;
import java.util.IdentityHashMap; import java.util.IdentityHashMap;
import java.util.Set; import java.util.Set;
import java.util.HashSet;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@Data @Data
@@ -157,6 +161,9 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
@EqualsAndHashCode.Exclude @EqualsAndHashCode.Exclude
private transient volatile GenerationRegistryContractFactory.CustomBiomeResourceResolver customBiomeResourceResolver; private transient volatile GenerationRegistryContractFactory.CustomBiomeResourceResolver customBiomeResourceResolver;
private transient volatile Engine soleEngine; private transient volatile Engine soleEngine;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private transient volatile Set<String> blockDropMaterials;
private IrisData(File dataFolder) { private IrisData(File dataFolder) {
this(dataFolder, false); this(dataFolder, false);
@@ -651,6 +658,7 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
} }
public synchronized void hotloaded() { public synchronized void hotloaded() {
blockDropMaterials = null;
StructureGraphCatalog.invalidate(this); StructureGraphCatalog.invalidate(this);
IrisObjectScale.invalidate(this); IrisObjectScale.invalidate(this);
contentGate = null; contentGate = null;
@@ -730,6 +738,7 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
} }
public void dump() { public void dump() {
blockDropMaterials = null;
StructureGraphCatalog.invalidate(this); StructureGraphCatalog.invalidate(this);
IrisObjectScale.invalidate(this); IrisObjectScale.invalidate(this);
for (ResourceLoader<?> i : loaders.values()) { for (ResourceLoader<?> i : loaders.values()) {
@@ -771,12 +780,43 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
} }
public void clearLists() { public void clearLists() {
blockDropMaterials = null;
for (ResourceLoader<?> i : loaders.values()) { for (ResourceLoader<?> i : loaders.values()) {
i.clearList(); i.clearList();
} }
possibleSnippets.clear(); 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) { private void invalidateLoader(ResourceLoader<?> loader) {
if (loader == null) { if (loader == null) {
return; return;
@@ -50,6 +50,7 @@ import art.arcane.volmlib.util.matter.MatterMarker;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.stream.StreamSupport; 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.MIN_STILT || config.getMode() == ObjectPlaceMode.FAST_MIN_STILT ||
config.getMode() == ObjectPlaceMode.CENTER_STILT || config.getMode() == ObjectPlaceMode.ERODE_STILT || organic); config.getMode() == ObjectPlaceMode.CENTER_STILT || config.getMode() == ObjectPlaceMode.ERODE_STILT || organic);
boolean eroding = config.getMode() == ObjectPlaceMode.ERODE_STILT; 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; KMap<Position2, Integer> heightmap = config.getSnow() > 0 ? new KMap<>() : null;
int spinx = rng.imax() / 1000; int spinx = rng.imax() / 1000;
int spiny = rng.imax() / 1000; int spiny = rng.imax() / 1000;
@@ -539,7 +543,7 @@ final class IrisObjectPlacementRunner {
i.add(translateOffset); i.add(translateOffset);
} }
if (stilting && IrisObjectShaping.shouldStilt(data)) { if (stilting && IrisObjectShaping.isStiltLayerBlock(data)) {
if (i.getBlockY() < lowest) { if (i.getBlockY() < lowest) {
lowest = i.getBlockY(); lowest = i.getBlockY();
} }
@@ -654,7 +658,7 @@ final class IrisObjectPlacementRunner {
self.readLock.lock(); self.readLock.lock();
try { try {
VectorMap<PlatformBlockState> blocks = self.blocks; VectorMap<PlatformBlockState> blocks = self.blocks;
IrisStiltSettings settings = config.getStiltSettings(); IrisStiltSettings settings = stiltSettings;
double erodeCentroidX = 0; double erodeCentroidX = 0;
double erodeCentroidZ = 0; double erodeCentroidZ = 0;
@@ -670,7 +674,7 @@ final class IrisObjectPlacementRunner {
} }
if (rot.getBlockY() == lowest) { if (rot.getBlockY() == lowest) {
PlatformBlockState bd = centroidCursor.value(); PlatformBlockState bd = centroidCursor.value();
if (bd != null && IrisObjectShaping.shouldStilt(bd)) { if (bd != null && IrisObjectShaping.shouldStilt(bd, excludedStiltMaterials)) {
erodeCentroidX += rot.getX(); erodeCentroidX += rot.getX();
erodeCentroidZ += rot.getZ(); erodeCentroidZ += rot.getZ();
centroidCount++; centroidCount++;
@@ -690,7 +694,7 @@ final class IrisObjectPlacementRunner {
} }
if (rot.getBlockY() == lowest) { if (rot.getBlockY() == lowest) {
PlatformBlockState bd = spreadCursor.value(); PlatformBlockState bd = spreadCursor.value();
if (bd != null && IrisObjectShaping.shouldStilt(bd)) { if (bd != null && IrisObjectShaping.shouldStilt(bd, excludedStiltMaterials)) {
double dx = rot.getX() - erodeCentroidX; double dx = rot.getX() - erodeCentroidX;
double dz = rot.getZ() - erodeCentroidZ; double dz = rot.getZ() - erodeCentroidZ;
double dist = Math.sqrt(dx * dx + dz * dz); double dist = Math.sqrt(dx * dx + dz * dz);
@@ -719,7 +723,7 @@ final class IrisObjectPlacementRunner {
sourceData = IrisObject.States.air(); sourceData = IrisObject.States.air();
} }
if (!IrisObjectShaping.shouldStilt(sourceData)) { if (!IrisObjectShaping.shouldStilt(sourceData, excludedStiltMaterials)) {
continue; continue;
} }
@@ -768,7 +772,7 @@ final class IrisObjectPlacementRunner {
} }
} }
if (d == null || !d.isOccluding()) if (d == null || !IrisObjectShaping.shouldStilt(d, excludedStiltMaterials))
continue; continue;
xx = x + (int) Math.round(i.getX()); 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.format.Form;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import java.util.Set;
/** /**
* Volume shaping for {@link IrisObject}: smart boring, shrinkwrapping, compaction and the shared block * Volume shaping for {@link IrisObject}: smart boring, shrinkwrapping, compaction and the shared block
* classification helpers used by placement. * classification helpers used by placement.
@@ -244,17 +246,23 @@ final class IrisObjectShaping {
self.floatingFootprint.reset(); self.floatingFootprint.reset();
} }
static boolean shouldStilt(PlatformBlockState state) { static boolean isStiltLayerBlock(PlatformBlockState state) {
if (!state.isOccluding()) { 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; return false;
} }
String material = materialKey(state);
if (material.endsWith("_stairs") || material.endsWith("_slab")) { if (material.endsWith("_stairs") || material.endsWith("_slab")) {
return false; return false;
} }
return !material.equals("minecraft:dirt_path"); 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) { static String materialKey(PlatformBlockState state) {
return IrisProceduralBlocks.materialKey(state); return IrisProceduralBlocks.materialKey(state);
} }
@@ -94,7 +94,7 @@ public final class IrisRenderer {
RenderType currentType, RenderType currentType,
BooleanSupplier cancelled BooleanSupplier cancelled
) { ) {
return render(sx, sz, size, resolution, currentType, cancelled, false); return render(sx, sz, size, resolution, currentType, cancelled, false, false);
} }
public BufferedImage renderStudio( public BufferedImage renderStudio(
@@ -105,7 +105,41 @@ public final class IrisRenderer {
RenderType currentType, RenderType currentType,
BooleanSupplier cancelled 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( private BufferedImage render(
@@ -115,7 +149,8 @@ public final class IrisRenderer {
int resolution, int resolution,
RenderType currentType, RenderType currentType,
BooleanSupplier cancelled, BooleanSupplier cancelled,
boolean studio boolean studio,
boolean riverOverlay
) { ) {
if (!Double.isFinite(sx) || !Double.isFinite(sz) || !Double.isFinite(size) || size <= 0D) { if (!Double.isFinite(sx) || !Double.isFinite(sz) || !Double.isFinite(size) || size <= 0D) {
throw new IllegalArgumentException("Vision render coordinates and size must be finite"); throw new IllegalArgumentException("Vision render coordinates and size must be finite");
@@ -142,7 +177,7 @@ public final class IrisRenderer {
} }
if (studio && adaptiveStudioType(currentType)) { if (studio && adaptiveStudioType(currentType)) {
renderAdaptiveAtlas(pixels, resolution, sx, sz, step, shader, cancelled); 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); renderRiverAtlas(pixels, resolution, sx, sz, step, renderer.getComplex(), cancelled, true);
} }
return image; return image;
@@ -136,7 +136,7 @@ final class VisionRenderController implements AutoCloseable {
List<VisibleTile> tiles = visibleTiles(spec); List<VisibleTile> tiles = visibleTiles(spec);
Frame frame = new Frame(viewSequence.incrementAndGet(), spec, tiles); Frame frame = new Frame(viewSequence.incrementAndGet(), spec, tiles);
for (VisibleTile tile : tiles) { for (VisibleTile tile : tiles) {
tile.setImage(cache.get(frame.key(tile))); tile.setResult(cache.get(frame.key(tile)));
} }
CancellationToken token = new CancellationToken(); CancellationToken token = new CancellationToken();
WorkState work = new WorkState(frame, token); WorkState work = new WorkState(frame, token);
@@ -165,7 +165,7 @@ final class VisionRenderController implements AutoCloseable {
} }
int ready = 0; int ready = 0;
for (VisibleTile tile : frame.tiles()) { for (VisibleTile tile : frame.tiles()) {
if (tile.image() != null) { if (tile.complete()) {
ready++; ready++;
} }
} }
@@ -275,14 +275,19 @@ final class VisionRenderController implements AutoCloseable {
if (!isCurrent(work)) { if (!isCurrent(work)) {
return; 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) { while (work.inFlight() < admissionLimit) {
VisibleTile tile = work.nextMissing(); VisibleTile tile = work.nextMissing();
if (tile == null) { if (tile == null) {
if (work.inFlight() == 0 && work.beginRefinement()) {
admissionLimit = 1;
continue;
}
return; return;
} }
boolean refining = work.refining();
work.incrementInFlight(); work.incrementInFlight();
TrackedRenderTask task = new TrackedRenderTask(() -> render(work, tile), activeRenderTasks); TrackedRenderTask task = new TrackedRenderTask(() -> render(work, tile, refining), activeRenderTasks);
activeRenderTasks.add(task); activeRenderTasks.add(task);
try { try {
renderExecutor.execute(task); 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(); Frame frame = work.frame();
CancellationToken token = work.token(); CancellationToken token = work.token();
try { try {
@@ -303,19 +308,26 @@ final class VisionRenderController implements AutoCloseable {
return; return;
} }
double tileSpan = TILE_PIXELS * frame.spec().blocksPerPixel(); double tileSpan = TILE_PIXELS * frame.spec().blocksPerPixel();
BufferedImage image = frame.spec().renderer().renderStudio( double startX = tile.tileX() * tileSpan;
tile.tileX() * tileSpan, double startZ = tile.tileZ() * tileSpan;
tile.tileZ() * tileSpan, IrisRenderer renderer = frame.spec().renderer();
tileSpan, BufferedImage image;
TILE_PIXELS, if (refining) {
frame.spec().type(), image = renderer.refineStudioBiome(startX, startZ, tileSpan, tile.image(),
() -> !isCurrent(frame, token) () -> !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)) { if (!isCurrent(frame, token)) {
return; return;
} }
cache.put(frame.key(tile), image); TileResult result = new TileResult(image, refining || frame.spec().type() != RenderType.BIOME);
tile.setImage(image); cache.put(frame.key(tile), result);
tile.setResult(result);
publish(frame, token); publish(frame, token);
} catch (CancellationException ignored) { } catch (CancellationException ignored) {
} catch (Throwable error) { } catch (Throwable error) {
@@ -446,7 +458,7 @@ final class VisionRenderController implements AutoCloseable {
private final int screenX; private final int screenX;
private final int screenY; private final int screenY;
private final double distanceSquared; private final double distanceSquared;
private volatile BufferedImage image; private volatile TileResult result;
private VisibleTile(long tileX, long tileZ, int screenX, int screenY, double distanceSquared) { private VisibleTile(long tileX, long tileZ, int screenX, int screenY, double distanceSquared) {
this.tileX = tileX; this.tileX = tileX;
@@ -477,11 +489,17 @@ final class VisionRenderController implements AutoCloseable {
} }
BufferedImage image() { BufferedImage image() {
return image; TileResult current = result;
return current == null ? null : current.image();
} }
void setImage(BufferedImage image) { boolean complete() {
this.image = image; 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 final CancellationToken token;
private int index; private int index;
private int inFlight; private int inFlight;
private boolean refining;
private WorkState(Frame frame, CancellationToken token) { private WorkState(Frame frame, CancellationToken token) {
this.frame = frame; this.frame = frame;
@@ -565,13 +584,26 @@ final class VisionRenderController implements AutoCloseable {
VisibleTile nextMissing() { VisibleTile nextMissing() {
while (index < frame.tiles().size()) { while (index < frame.tiles().size()) {
VisibleTile tile = frame.tiles().get(index++); VisibleTile tile = frame.tiles().get(index++);
if (tile.image() == null) { if (refining ? tile.image() != null && !tile.complete() : tile.image() == null) {
return tile; return tile;
} }
} }
return null; 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() { int inFlight() {
return inFlight; return inFlight;
} }
@@ -595,14 +627,15 @@ final class VisionRenderController implements AutoCloseable {
this.entries = new LinkedHashMap<>(128, 0.75F, true); this.entries = new LinkedHashMap<>(128, 0.75F, true);
} }
synchronized BufferedImage get(TileKey key) { synchronized TileResult get(TileKey key) {
CacheEntry entry = entries.get(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; 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) { if (previous != null) {
bytes -= previous.bytes(); 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) { public Optional<BiomeEnvironment> resolveCaveBase(int blockX, int blockZ) {
return resolve(blockX, 0, blockZ, QueryKind.CAVE_BASE); 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), Definitions loaded = new Definitions(data, dimension, NativeBiomeSpawnSelection.retainedDerivatives(data),
resolveFocusRegions(data, dimension)); resolveFocusRegions(data, dimension));
data.prepareBlockDropRules();
definitions.put(epochId, loaded); definitions.put(epochId, loaded);
return loaded; return loaded;
} catch (Throwable failure) { } catch (Throwable failure) {
@@ -24,6 +24,8 @@ import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.same; import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@RunWith(Parameterized.class) @RunWith(Parameterized.class)
@@ -70,6 +72,7 @@ public final class IrisBiomeLayerSlopeSourceTest {
assertEquals(1, ledge.size()); assertEquals(1, ledge.size());
assertSame(block, ledge.get(0)); assertSame(block, ledge.get(0));
assertTrue(cap.isEmpty()); assertTrue(cap.isEmpty());
verify(heightGenerator, times(1)).fit(1, 1, 12D, -8D);
} }
@Test @Test
@@ -109,5 +112,6 @@ public final class IrisBiomeLayerSlopeSourceTest {
assertTrue(hostLayers.isEmpty()); assertTrue(hostLayers.isEmpty());
assertEquals(1, sourceLayers.size()); assertEquals(1, sourceLayers.size());
assertSame(surfaceBlock, sourceLayers.get(0)); assertSame(surfaceBlock, sourceLayers.get(0));
verify(heightGenerator, times(1)).fit(1, 1, 12D, -8D);
} }
} }
@@ -1,14 +1,30 @@
package art.arcane.iris.generation.biome; 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.iris.spi.PlatformBlockState;
import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG; 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.junit.Test;
import org.mockito.MockedStatic;
import java.util.List; import java.util.List;
import static org.junit.Assert.assertEquals; 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.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 { public class IrisBiomeLockedLayerTest {
@Test @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; import static org.mockito.Mockito.when;
public class HydrologyTileCacheTest { 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 @Test
public void closeDrainsBackgroundPlanningAndRejectsNewDemand() throws Exception { public void closeDrainsBackgroundPlanningAndRejectsNewDemand() throws Exception {
HydrologyPlanner planner = mock(HydrologyPlanner.class); HydrologyPlanner planner = mock(HydrologyPlanner.class);
@@ -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.iris.world.task.J;
import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KList;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.GameMode;
import org.bukkit.World; import org.bukkit.World;
import org.bukkit.block.Block; import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData; import org.bukkit.block.data.BlockData;
@@ -41,6 +42,30 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
public class WorldBlockDropRouterTest { 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 @Test
public void coldBiomeCancelsBeforeChangingDropsOrSchedulingWork() { public void coldBiomeCancelsBeforeChangingDropsOrSchedulingWork() {
try (Fixture fixture = new Fixture()) { try (Fixture fixture = new Fixture()) {
@@ -206,6 +231,7 @@ public class WorldBlockDropRouterTest {
when(manager.getEngine()).thenReturn(engine); when(manager.getEngine()).thenReturn(engine);
when(manager.getTarget()).thenReturn(target); when(manager.getTarget()).thenReturn(target);
when(manager.getData()).thenReturn(data); when(manager.getData()).thenReturn(data);
when(data.hasBlockDropRules(anyString())).thenReturn(true);
when(target.getWorld()).thenReturn(irisWorld); when(target.getWorld()).thenReturn(irisWorld);
when(engine.getWorld()).thenReturn(irisWorld); when(engine.getWorld()).thenReturn(irisWorld);
when(irisWorld.minHeight()).thenReturn(-64); when(irisWorld.minHeight()).thenReturn(-64);
@@ -217,6 +243,7 @@ public class WorldBlockDropRouterTest {
when(block.getZ()).thenReturn(32); when(block.getZ()).thenReturn(32);
when(block.getLocation()).thenReturn(location); when(block.getLocation()).thenReturn(location);
when(block.getBlockData()).thenReturn(blockData); when(block.getBlockData()).thenReturn(blockData);
when(blockData.getAsString()).thenReturn("minecraft:stone");
when(biome.getBlockDrops()).thenReturn(new KList<>(drops)); when(biome.getBlockDrops()).thenReturn(new KList<>(drops));
when(region.getBlockDrops()).thenReturn(new KList<>()); when(region.getBlockDrops()).thenReturn(new KList<>());
when(dimension.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.configuration.IrisSettings;
import art.arcane.iris.generation.runtime.PreservationRegistry; import art.arcane.iris.generation.runtime.PreservationRegistry;
import art.arcane.iris.generation.noise.IrisGenerator; 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.spi.IrisServices;
import art.arcane.iris.testsupport.PlatformLeakGuard; import art.arcane.iris.testsupport.PlatformLeakGuard;
import org.junit.After; import org.junit.After;
@@ -11,6 +14,7 @@ import org.junit.ClassRule;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.TemporaryFolder; import org.junit.rules.TemporaryFolder;
import org.mockito.MockedStatic;
import java.io.File; import java.io.File;
import java.nio.file.Files; 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.assertSame;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
public class IrisDataAuthoringCacheTest { public class IrisDataAuthoringCacheTest {
@ClassRule @ClassRule
@@ -105,4 +111,61 @@ public class IrisDataAuthoringCacheTest {
assertSame(keys, entry.getKey().getPossibleKeys()); 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 @ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean(); 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( private static final List<Class<?>> SCHEMA_ROOTS = List.of(
IrisDimension.class, 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.IrisRegistrant;
import art.arcane.iris.pack.loading.ResourceLoader; import art.arcane.iris.pack.loading.ResourceLoader;
import art.arcane.iris.generation.block.IrisBlockData; 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.noise.IrisExpression;
import art.arcane.iris.generation.terrain.IrisDimension; import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.structure.jigsaw.IrisJigsawPiece; 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 @Test
public void registryDependentEnumsMatchLegacyTransforms() { public void registryDependentEnumsMatchLegacyTransforms() {
JSONObject definitions = new SchemaBuilder(RegistryModel.class, (IrisData) null).construct().getJSONObject("definitions"); JSONObject definitions = new SchemaBuilder(RegistryModel.class, (IrisData) null).construct().getJSONObject("definitions");
@@ -1,6 +1,11 @@
package art.arcane.iris.structure.object; package art.arcane.iris.structure.object;
import art.arcane.iris.generation.block.TileData; 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.IrisGeneratorStyle;
import art.arcane.iris.generation.noise.NoiseStyle; import art.arcane.iris.generation.noise.NoiseStyle;
import art.arcane.iris.generation.terrain.IrisMaterialPalette; import art.arcane.iris.generation.terrain.IrisMaterialPalette;
@@ -415,6 +420,194 @@ public class IrisObjectPlacementRunnerRegressionTest {
assertTrue(placer.writes().isEmpty()); 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() { private IrisObjectPlacement placement() {
IrisObjectPlacement placement = new IrisObjectPlacement(); IrisObjectPlacement placement = new IrisObjectPlacement();
placement.setMode(ObjectPlaceMode.CENTER_HEIGHT); placement.setMode(ObjectPlaceMode.CENTER_HEIGHT);
@@ -139,6 +139,27 @@ public class IrisRendererPerformanceContractTest {
assertTrue(samples.get() < 128); 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 @Test
public void planarStudioHeightPagesInterpolateWithoutPerPixelEngineSampling() { public void planarStudioHeightPagesInterpolateWithoutPerPixelEngineSampling() {
Engine engine = mock(Engine.class); Engine engine = mock(Engine.class);
@@ -134,6 +134,32 @@ public class IrisRendererRiverTest {
verify(runtime).sampleRenderFootprint(12D, -7D, 20D, 1D); 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 @Test
public void riverVisionShowsAProjectedCandidateWithoutTreatingItAsAccepted() { public void riverVisionShowsAProjectedCandidateWithoutTreatingItAsAccepted() {
Engine engine = mock(Engine.class); Engine engine = mock(Engine.class);
@@ -11,6 +11,7 @@ import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CancellationException;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BooleanSupplier; import java.util.function.BooleanSupplier;
@@ -68,7 +69,7 @@ public class VisionRenderControllerTest {
public void everyPublishedPageIsFinalResolutionAndIdenticalRequestsAreCacheHits() throws Exception { public void everyPublishedPageIsFinalResolutionAndIdenticalRequestsAreCacheHits() throws Exception {
IrisRenderer renderer = mock(IrisRenderer.class); IrisRenderer renderer = mock(IrisRenderer.class);
AtomicInteger calls = new AtomicInteger(); AtomicInteger calls = new AtomicInteger();
when(renderer.renderStudio( when(renderer.renderStudioBase(
anyDouble(), anyDouble(),
anyDouble(), anyDouble(),
anyDouble(), anyDouble(),
@@ -84,6 +85,7 @@ public class VisionRenderControllerTest {
BufferedImage.TYPE_INT_RGB BufferedImage.TYPE_INT_RGB
); );
}); });
passThroughRefinement(renderer);
VisionRenderController controller = controller(2); VisionRenderController controller = controller(2);
VisionRenderController.RenderSpec spec = spec(renderer, RenderType.BIOME, 1L, 0D, 0D, 4D, 128, 128); VisionRenderController.RenderSpec spec = spec(renderer, RenderType.BIOME, 1L, 0D, 0D, 4D, 128, 128);
try { try {
@@ -153,6 +155,12 @@ public class VisionRenderControllerTest {
latestStarted.countDown(); latestStarted.countDown();
return new BufferedImage(64, 64, BufferedImage.TYPE_INT_RGB); 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); VisionRenderController controller = controller(2);
try { try {
controller.request(spec(renderer, RenderType.REGION, 1L, 0D, 0D, 4D, 64, 64)); 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) { private static VisionRenderController controller(int workers) {
return new VisionRenderController( return new VisionRenderController(
() -> { () -> {
@@ -188,9 +293,18 @@ public class VisionRenderControllerTest {
invocation.getArgument(3), invocation.getArgument(3),
BufferedImage.TYPE_INT_RGB 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; 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) { private static Set<VisionRenderController.TileKey> keys(VisionRenderController.Frame frame) {
Set<VisionRenderController.TileKey> keys = new HashSet<>(); Set<VisionRenderController.TileKey> keys = new HashSet<>();
for (VisionRenderController.VisibleTile tile : frame.tiles()) { for (VisionRenderController.VisibleTile tile : frame.tiles()) {
@@ -37,6 +37,7 @@ import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame; import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue; 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.when;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
public class SavedBiomeRuntimeTest { public class SavedBiomeRuntimeTest {
@ClassRule @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 @Test
public void chunkReadinessDoesNotRequireEveryCellToHaveAResolvedIdentity() throws Exception { public void chunkReadinessDoesNotRequireEveryCellToHaveAResolvedIdentity() throws Exception {
when(store.get(0, 0)).thenReturn(Optional.of(unresolvedChunk(1))); when(store.get(0, 0)).thenReturn(Optional.of(unresolvedChunk(1)));