Added dimension stacking

- Dimension files define upright top-to-bottom terrain layers
- Spacer values set exact air gaps between layers
- Optional noise styles shape blended layer boundaries
- Runtime queries resolve stacked terrain and biome ownership
- Pack validation rejects invalid stack definitions
- Project copy and hotload paths include stack dependencies
- Superflat fixtures cover gaps, blending, clipping, and fluids
- Platform tests cover stacked world generation contracts
This commit is contained in:
Brian Neumann-Fopiano
2026-09-04 14:23:15 -04:00
parent 366033a535
commit 6549b40797
119 changed files with 6874 additions and 743 deletions
@@ -4,6 +4,9 @@ import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import com.mojang.datafixers.util.Pair;
import com.mojang.serialization.MapCodec;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.engine.DimensionStackContext;
import art.arcane.iris.engine.DimensionStackLayout;
import art.arcane.iris.engine.DimensionTerrainContext;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.GenerationSessionException;
@@ -16,6 +19,7 @@ import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.math.RNG;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.Holder;
import net.minecraft.core.QuartPos;
import net.minecraft.core.Registry;
@@ -24,6 +28,8 @@ import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.util.RandomSource;
import net.minecraft.util.Mth;
import net.minecraft.world.level.LevelReader;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeSource;
import net.minecraft.world.level.biome.Climate;
@@ -80,27 +86,31 @@ public class CustomBiomeSource extends BiomeSource {
this.cacheRuntimeId = engine.getCacheID();
}
private static List<Holder<Biome>> getAllBiomes(Registry<Biome> customRegistry, Registry<Biome> registry, Engine engine) {
private static List<Holder<Biome>> getAllBiomes(
Registry<Biome> customRegistry,
Registry<Biome> registry,
Engine engine,
boolean includeDimensionStack
) {
LinkedHashSet<Holder<Biome>> biomes = new LinkedHashSet<>();
for (IrisBiome i : engine.getAllBiomes()) {
for (OwnedBiome ownedBiome : getHostOwnedBiomes(engine)) {
IrisBiome i = ownedBiome.biome();
Holder<Biome> vanillaHolder = resolveBiomeHolder(registry, i.getStructureDerivativeKey());
if (vanillaHolder == null) {
throw new IllegalStateException("Iris structure biome derivative '"
+ i.getStructureDerivativeKey() + "' is not registered for biome '" + i.getLoadKey() + "'");
}
biomes.add(vanillaHolder);
}
if (i.isCustom()) {
for (IrisBiomeCustom j : i.getCustomDerivitives()) {
Holder<Biome> customHolder = resolveCustomBiomeHolder(customRegistry, engine, j.getId());
if (customHolder == null) {
throw new IllegalStateException("Iris custom structure biome '"
+ engine.getDimension().getLoadKey() + ":" + j.getId() + "' is not registered");
}
biomes.add(customHolder);
}
}
if (includeDimensionStack) {
biomes.addAll(getVisibleBiomes(
customRegistry,
registry,
engine,
resolveFallbackBiome(registry, customRegistry)
));
}
if (biomes.isEmpty()) {
@@ -110,6 +120,57 @@ public class CustomBiomeSource extends BiomeSource {
return new ArrayList<>(biomes);
}
private static List<Holder<Biome>> getVisibleBiomes(
Registry<Biome> customRegistry,
Registry<Biome> registry,
Engine engine,
Holder<Biome> fallback
) {
LinkedHashSet<Holder<Biome>> biomes = new LinkedHashSet<>();
boolean fallbackPossible = false;
for (OwnedBiome ownedBiome : getOwnedBiomes(engine)) {
IrisBiome irisBiome = ownedBiome.biome();
if (irisBiome.isCustom()) {
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
Holder<Biome> customHolder = resolveCustomBiomeHolder(
customRegistry, ownedBiome.dimension(), customBiome.getId());
if (customHolder == null) {
throw new IllegalStateException("Iris custom visible biome '"
+ customBiomeKey(ownedBiome.dimension(), customBiome.getId())
+ "' is not registered");
}
biomes.add(customHolder);
}
continue;
}
fallbackPossible |= !addVisibleBiomeHolder(
biomes, registry, irisBiome.getDerivativeKey());
for (String scatter : irisBiome.getBiomeScatter()) {
fallbackPossible |= !addVisibleBiomeHolder(biomes, registry, scatter);
}
for (String scatter : irisBiome.getBiomeSkyScatter()) {
fallbackPossible |= !addVisibleBiomeHolder(biomes, registry, scatter);
}
}
if (fallbackPossible && fallback != null) {
biomes.add(fallback);
}
return new ArrayList<>(biomes);
}
private static boolean addVisibleBiomeHolder(
Set<Holder<Biome>> biomes,
Registry<Biome> registry,
String biomeKey
) {
Holder<Biome> holder = resolveBiomeHolder(registry, biomeKey);
if (holder == null) {
return false;
}
biomes.add(holder);
return true;
}
private static Object getFor(Class<?> type, Object source) {
Object o = fieldFor(type, source);
@@ -175,10 +236,19 @@ public class CustomBiomeSource extends BiomeSource {
@Override
protected Stream<Holder<Biome>> collectPossibleBiomes() {
return possibleStructureBiomes().stream();
return possibleBiomes(true).stream();
}
@Override
public Set<Holder<Biome>> possibleBiomes() {
return possibleBiomes(true);
}
Set<Holder<Biome>> possibleStructureBiomes() {
return possibleBiomes(false);
}
private Set<Holder<Biome>> possibleBiomes(boolean includeDimensionStack) {
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_possible_biomes");
if (lease == null) {
throw new IllegalStateException("Iris possible biome lookup was rejected during an engine transition");
@@ -197,7 +267,8 @@ public class CustomBiomeSource extends BiomeSource {
.lookup(Registries.BIOME).orElse(null);
Registry<Biome> worldRegistry = ((CraftWorld) world).getHandle().registryAccess()
.lookup(Registries.BIOME).orElse(null);
return Set.copyOf(getAllBiomes(customRegistry, worldRegistry, engine));
return Set.copyOf(getAllBiomes(
customRegistry, worldRegistry, engine, includeDimensionStack));
}
}
@@ -207,18 +278,22 @@ public class CustomBiomeSource extends BiomeSource {
return m;
}
for (IrisBiome i : engine.getAllBiomes()) {
for (OwnedBiome ownedBiome : getOwnedBiomes(engine)) {
IrisBiome i = ownedBiome.biome();
if (i.isCustom()) {
for (IrisBiomeCustom j : i.getCustomDerivitives()) {
Holder<Biome> holder = resolveCustomBiomeHolder(customRegistry, engine, j.getId());
String key = customBiomeKey(ownedBiome.dimension(), j.getId());
Holder<Biome> holder = resolveCustomBiomeHolder(
customRegistry, ownedBiome.dimension(), j.getId());
if (holder == null) {
if (fallback != null) {
m.put(j.getId(), fallback);
m.put(key, fallback);
}
IrisLogging.error("Cannot find biome for IrisBiomeCustom " + j.getId() + " from engine " + engine.getName());
IrisLogging.error("Cannot find biome for IrisBiomeCustom " + key
+ " from engine " + engine.getName());
continue;
}
m.put(j.getId(), holder);
m.put(key, holder);
}
}
}
@@ -232,7 +307,8 @@ public class CustomBiomeSource extends BiomeSource {
return Collections.unmodifiableMap(spawnBiomes);
}
for (IrisBiome irisBiome : engine.getAllBiomes()) {
for (OwnedBiome ownedBiome : getOwnedBiomes(engine)) {
IrisBiome irisBiome = ownedBiome.biome();
if (!irisBiome.isCustom()) {
continue;
}
@@ -241,7 +317,8 @@ public class CustomBiomeSource extends BiomeSource {
continue;
}
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
Holder<Biome> customHolder = resolveCustomBiomeHolder(customRegistry, engine, customBiome.getId());
Holder<Biome> customHolder = resolveCustomBiomeHolder(
customRegistry, ownedBiome.dimension(), customBiome.getId());
if (customHolder != null) {
spawnBiomes.putIfAbsent(customHolder.value(), vanillaHolder);
}
@@ -268,6 +345,34 @@ public class CustomBiomeSource extends BiomeSource {
}
}
Holder<Biome> getVisibleSurfaceBiome(int blockX, int blockZ) {
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_surface_spawn_biome");
if (lease == null) {
return null;
}
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
if (!isRuntimeAvailable()) {
return null;
}
ensureCachesCurrent();
int quartX = QuartPos.fromBlock(blockX);
int quartZ = QuartPos.fromBlock(blockZ);
int sampleX = QuartPos.toBlock(quartX);
int sampleZ = QuartPos.toBlock(quartZ);
boolean cacheable = isBiomeCacheable(engine, sampleX, sampleZ);
DimensionStackContext stackContext = engine.getDimensionStackContext();
DimensionStackLayout.Layer layer = stackContext == null
? null
: stackContext.getLayout(sampleX, sampleZ).surfaceLayer();
int internalY = layer == null
? Engine.hostHeight(engine, sampleX, sampleZ, true)
: layer.clippedSurfaceY();
int quartY = QuartPos.fromBlock(internalY + engine.getWorld().minHeight());
return getVisibleNoiseBiomeWithActiveGenerationLease(
quartX, quartY, quartZ, null, null, cacheable);
}
}
private RegistryAccess registry() {
RegistryAccess access = registryAccess.aquire(() -> (RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer()));
@@ -294,28 +399,38 @@ public class CustomBiomeSource extends BiomeSource {
throw new IllegalStateException("Iris structure biome lookup has no active engine runtime");
}
ensureCachesCurrent();
if (isGuaranteedSurfaceBiome(y)) {
return getSurfaceStructureBiomeHolder(x, z);
}
return getStructureNoiseBiomeWithActiveGenerationLease(x, y, z);
}
}
long cacheKey = packNoiseKey(x, y, z);
private Holder<Biome> getStructureNoiseBiomeWithActiveGenerationLease(int x, int y, int z) {
if (isGuaranteedSurfaceBiome(y)) {
return getSurfaceStructureBiomeHolder(x, z);
}
boolean cacheable = isBiomeCacheable(x, z);
long cacheKey = packNoiseKey(x, y, z);
if (cacheable) {
Holder<Biome> cachedHolder = structureBiomeCache.get(cacheKey);
if (cachedHolder != null) {
return cachedHolder;
}
}
Holder<Biome> resolvedHolder = resolveStructureBiomeHolder(x, y, z);
Holder<Biome> existingHolder = structureBiomeCache.putIfAbsent(cacheKey, resolvedHolder);
if (existingHolder != null) {
return existingHolder;
}
if (structureBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
structureBiomeCache.clear();
}
Holder<Biome> resolvedHolder = resolveStructureBiomeHolder(x, y, z);
if (!cacheable) {
return resolvedHolder;
}
Holder<Biome> existingHolder = structureBiomeCache.putIfAbsent(cacheKey, resolvedHolder);
if (existingHolder != null) {
return existingHolder;
}
if (structureBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
structureBiomeCache.clear();
}
return resolvedHolder;
}
@Override
@@ -346,6 +461,62 @@ public class CustomBiomeSource extends BiomeSource {
}
}
@Override
public Pair<BlockPos, Holder<Biome>> findClosestBiome3d(
BlockPos origin,
int searchRadius,
int sampleResolutionHorizontal,
int sampleResolutionVertical,
Predicate<Holder<Biome>> allowed,
Climate.Sampler sampler,
LevelReader level
) {
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_locate_visible_biome");
if (lease == null) {
throw new IllegalStateException("Iris visible biome search was rejected during an engine transition");
}
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
prepareVisibleBiomeBatch();
DimensionStackContext stackContext = engine.getDimensionStackContext();
if (stackContext == null) {
return super.findClosestBiome3d(
origin, searchRadius, sampleResolutionHorizontal, sampleResolutionVertical,
allowed, sampler, level);
}
Set<Holder<Biome>> candidates = new HashSet<>();
for (Holder<Biome> biome : getVisibleBiomes(
biomeCustomRegistry, biomeRegistry, engine, fallbackBiome)) {
if (allowed.test(biome)) {
candidates.add(biome);
}
}
if (candidates.isEmpty()) {
return null;
}
int sampleRadius = Math.floorDiv(searchRadius, sampleResolutionHorizontal);
int[] sampleYs = Mth.outFromOrigin(
origin.getY(), level.getMinY() + 1, level.getMaxY() + 1, sampleResolutionVertical).toArray();
for (BlockPos.MutableBlockPos sampleColumn : BlockPos.spiralAround(
BlockPos.ZERO, sampleRadius, Direction.EAST, Direction.SOUTH)) {
int blockX = origin.getX() + sampleColumn.getX() * sampleResolutionHorizontal;
int blockZ = origin.getZ() + sampleColumn.getZ() * sampleResolutionHorizontal;
int quartX = QuartPos.fromBlock(blockX);
int quartZ = QuartPos.fromBlock(blockZ);
DimensionStackLayout stackLayout = stackContext.sample(
QuartPos.toBlock(quartX), QuartPos.toBlock(quartZ));
for (int blockY : sampleYs) {
int quartY = QuartPos.fromBlock(blockY);
Holder<Biome> visibleBiome = resolveVisibleBiomeHolder(
quartX, quartY, quartZ, null, stackLayout);
if (candidates.contains(visibleBiome)) {
return Pair.of(new BlockPos(blockX, blockY, blockZ), visibleBiome);
}
}
}
return null;
}
}
static int horizontalBiomeSearchQuartStep(int blockY, int searchRadius) {
return blockY == STRONGHOLD_RING_SEARCH_Y && searchRadius == STRONGHOLD_RING_SEARCH_RADIUS
? STRONGHOLD_RING_SEARCH_QUART_STEP
@@ -468,7 +639,7 @@ public class CustomBiomeSource extends BiomeSource {
private Holder<Biome> resolveSurfaceStructureBiomeHolder(int x, int z) {
int blockX = x << 2;
int blockZ = z << 2;
IrisBiome irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
IrisBiome irisBiome = resolveSurfaceStructureBiome(engine, blockX, blockZ);
if (irisBiome == null) {
throw new IllegalStateException("Iris returned no surface structure biome at block "
+ blockX + "," + blockZ);
@@ -532,14 +703,32 @@ public class CustomBiomeSource extends BiomeSource {
int z,
Climate.Sampler sampler,
IrisDimensionCarvingResolver.State resolverState
) {
boolean cacheable = isBiomeCacheable(x, z);
return getVisibleNoiseBiomeWithActiveGenerationLease(
x, y, z, sampler, resolverState, cacheable);
}
private Holder<Biome> getVisibleNoiseBiomeWithActiveGenerationLease(
int x,
int y,
int z,
Climate.Sampler sampler,
IrisDimensionCarvingResolver.State resolverState,
boolean cacheable
) {
long cacheKey = packNoiseKey(x, y, z);
Holder<Biome> cachedHolder = noiseBiomeCache.get(cacheKey);
if (cachedHolder != null) {
return cachedHolder;
if (cacheable) {
Holder<Biome> cachedHolder = noiseBiomeCache.get(cacheKey);
if (cachedHolder != null) {
return cachedHolder;
}
}
Holder<Biome> resolvedHolder = resolveVisibleBiomeHolder(x, y, z, resolverState);
if (!cacheable) {
return resolvedHolder;
}
Holder<Biome> existingHolder = noiseBiomeCache.putIfAbsent(cacheKey, resolvedHolder);
if (existingHolder != null) {
return existingHolder;
@@ -552,6 +741,18 @@ public class CustomBiomeSource extends BiomeSource {
return resolvedHolder;
}
private boolean isBiomeCacheable(int quartX, int quartZ) {
return isBiomeCacheable(
engine,
QuartPos.toBlock(quartX),
QuartPos.toBlock(quartZ)
);
}
static boolean isBiomeCacheable(Engine engine, int blockX, int blockZ) {
return !engine.answersFromNaturalTerrain(blockX, blockZ);
}
private GenerationSessionLease tryAcquireGenerationLease(String operation) {
if (engine.isClosed()) {
return null;
@@ -597,7 +798,7 @@ public class CustomBiomeSource extends BiomeSource {
}
private Holder<Biome> resolveStructureBiomeHolder(int x, int y, int z) {
BiomeResolution resolution = resolveBiomeResolution(x, y, z);
BiomeResolution resolution = resolveStructureBiomeResolution(x, y, z);
if (resolution == null) {
throw new IllegalStateException("Iris returned no structure biome at quart "
+ x + "," + y + "," + z);
@@ -619,11 +820,26 @@ public class CustomBiomeSource extends BiomeSource {
int z,
IrisDimensionCarvingResolver.State resolverState
) {
BiomeResolution resolution = resolveBiomeResolution(x, y, z, resolverState);
return resolveVisibleBiomeHolder(x, y, z, resolverState, null);
}
private Holder<Biome> resolveVisibleBiomeHolder(
int x,
int y,
int z,
IrisDimensionCarvingResolver.State resolverState,
DimensionStackLayout stackLayout
) {
BiomeResolution resolution = resolveBiomeResolution(
x, y, z, resolverState, true, stackLayout);
if (resolution == null) {
return getFallbackBiome();
}
return resolveVisibleBiomeHolder(resolution);
}
private Holder<Biome> resolveVisibleBiomeHolder(BiomeResolution resolution) {
if (resolution.irisBiome.isCustom()) {
return resolveCustomHolder(resolution);
}
@@ -642,7 +858,8 @@ public class CustomBiomeSource extends BiomeSource {
private Holder<Biome> resolveCustomHolder(BiomeResolution resolution) {
IrisBiomeCustom customBiome = resolution.irisBiome.getCustomBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ);
if (customBiome != null) {
Holder<Biome> holder = customBiomes.get(customBiome.getId());
Holder<Biome> holder = customBiomes.get(customBiomeKey(
resolution.dimension, customBiome.getId()));
if (holder != null) {
return holder;
}
@@ -651,8 +868,8 @@ public class CustomBiomeSource extends BiomeSource {
return getFallbackBiome();
}
private BiomeResolution resolveBiomeResolution(int x, int y, int z) {
return resolveBiomeResolution(x, y, z, null);
private BiomeResolution resolveStructureBiomeResolution(int x, int y, int z) {
return resolveBiomeResolution(x, y, z, null, false);
}
private BiomeResolution resolveBiomeResolution(
@@ -660,6 +877,28 @@ public class CustomBiomeSource extends BiomeSource {
int y,
int z,
IrisDimensionCarvingResolver.State resolverState
) {
return resolveBiomeResolution(x, y, z, resolverState, true);
}
private BiomeResolution resolveBiomeResolution(
int x,
int y,
int z,
IrisDimensionCarvingResolver.State resolverState,
boolean includeDimensionStack
) {
return resolveBiomeResolution(
x, y, z, resolverState, includeDimensionStack, null);
}
private BiomeResolution resolveBiomeResolution(
int x,
int y,
int z,
IrisDimensionCarvingResolver.State resolverState,
boolean includeDimensionStack,
DimensionStackLayout stackLayout
) {
if (engine == null || engine.isClosed()) {
return null;
@@ -675,31 +914,63 @@ public class CustomBiomeSource extends BiomeSource {
int worldMinHeight = engine.getWorld().minHeight();
int internalY = blockY - worldMinHeight;
int caveSwitchInternalY = Math.max(-8 - worldMinHeight, 40);
DimensionStackLayout.Layer stackLayer = includeDimensionStack
? stackLayout == null
? resolveDimensionStackLayer(engine, blockX, internalY, blockZ)
: stackLayout.layerAt(internalY)
: null;
IrisDimension owningDimension = stackLayer == null
? engine.getDimension()
: stackLayer.terrainContext().getDimension();
if (stackLayer != null && !stackLayer.terrainContext().isSelfReferencing()) {
IrisBiome stackedBiome = stackLayer.biome();
return stackedBiome == null
? null
: createBiomeResolution(
stackedBiome, false, owningDimension, blockX, blockY, blockZ);
}
boolean deepUnderground = internalY <= caveSwitchInternalY;
boolean underground = false;
IrisBiome irisBiome;
if (deepUnderground) {
int surfaceInternalY = engine.getComplex().getHeightStream().get(blockX, blockZ).intValue();
if (stackLayer != null) {
int surfaceInternalY = stackLayer.surfaceY();
underground = internalY <= surfaceInternalY - 8;
irisBiome = underground
? engine.getCaveBiome(
blockX,
internalY,
blockZ,
resolverState,
stackLayer.biome(),
surfaceInternalY
)
: stackLayer.biome();
} else if (deepUnderground) {
int surfaceInternalY = Engine.hostHeight(engine, blockX, blockZ, true);
underground = internalY <= surfaceInternalY - 8;
irisBiome = underground
? engine.getCaveBiome(blockX, internalY, blockZ, resolverState)
: engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
: engine.getHostSurfaceBiome(blockX, blockZ);
} else {
irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
irisBiome = engine.getHostSurfaceBiome(blockX, blockZ);
}
if (irisBiome == null && underground) {
irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
irisBiome = stackLayer == null
? engine.getHostSurfaceBiome(blockX, blockZ)
: stackLayer.biome();
}
if (irisBiome == null) {
return null;
}
return createBiomeResolution(irisBiome, underground, blockX, blockY, blockZ);
return createBiomeResolution(
irisBiome, underground, owningDimension, blockX, blockY, blockZ);
}
private BiomeResolution createBiomeResolution(
IrisBiome irisBiome,
boolean underground,
IrisDimension dimension,
int blockX,
int blockY,
int blockZ
@@ -708,7 +979,8 @@ public class CustomBiomeSource extends BiomeSource {
^ (((long) blockX) * 341873128712L)
^ (((long) blockY) * 132897987541L)
^ (((long) blockZ) * 42317861L));
return new BiomeResolution(irisBiome, underground, blockX, blockY, blockZ, noiseRng);
return new BiomeResolution(
irisBiome, underground, dimension, blockX, blockY, blockZ, noiseRng);
}
private Holder<Biome> getFallbackBiome() {
@@ -734,15 +1006,20 @@ public class CustomBiomeSource extends BiomeSource {
return ((long) x << 32) ^ ((long) z & 4294967295L);
}
private static Holder<Biome> resolveCustomBiomeHolder(Registry<Biome> customRegistry, Engine engine, String customBiomeId) {
if (customRegistry == null || engine == null || customBiomeId == null || customBiomeId.isBlank()) {
private static Holder<Biome> resolveCustomBiomeHolder(
Registry<Biome> customRegistry,
IrisDimension dimension,
String customBiomeId
) {
if (customRegistry == null || dimension == null
|| customBiomeId == null || customBiomeId.isBlank()) {
return null;
}
Identifier resourceLocation = Identifier.fromNamespaceAndPath(
engine.getDimension().getLoadKey().toLowerCase(java.util.Locale.ROOT),
customBiomeId.toLowerCase(java.util.Locale.ROOT)
);
Identifier resourceLocation = Identifier.tryParse(customBiomeKey(dimension, customBiomeId));
if (resourceLocation == null) {
return null;
}
Biome biome = customRegistry.getValue(resourceLocation);
if (biome == null) {
return null;
@@ -761,6 +1038,63 @@ public class CustomBiomeSource extends BiomeSource {
return optionalReferenceHolder.get();
}
static String customBiomeKey(IrisDimension dimension, String customBiomeId) {
return dimension.getCustomBiomeKey(customBiomeId);
}
private static List<OwnedBiome> getOwnedBiomes(Engine engine) {
ArrayList<OwnedBiome> biomes = new ArrayList<>(getHostOwnedBiomes(engine));
DimensionStackContext stackContext = engine.getDimensionStackContext();
if (stackContext == null) {
return biomes;
}
for (DimensionTerrainContext terrainContext : stackContext.getLayersBottomToTop()) {
if (terrainContext.isSelfReferencing()) {
continue;
}
IrisDimension dimension = terrainContext.getDimension();
addOwnedBiomes(
biomes,
dimension,
dimension.getReachableBiomes(terrainContext)
);
}
return biomes;
}
private static List<OwnedBiome> getHostOwnedBiomes(Engine engine) {
IrisDimension hostDimension = engine.getDimension();
ArrayList<OwnedBiome> biomes = new ArrayList<>();
addOwnedBiomes(biomes, hostDimension, hostDimension.getReachableBiomes(engine));
return biomes;
}
private static void addOwnedBiomes(
List<OwnedBiome> target,
IrisDimension dimension,
Iterable<IrisBiome> biomes
) {
for (IrisBiome biome : biomes) {
if (biome != null) {
target.add(new OwnedBiome(dimension, biome));
}
}
}
private static DimensionStackLayout.Layer resolveDimensionStackLayer(
Engine engine,
int blockX,
int internalY,
int blockZ
) {
DimensionStackContext stackContext = engine.getDimensionStackContext();
return stackContext == null ? null : stackContext.getLayerAt(blockX, internalY, blockZ);
}
private static IrisBiome resolveSurfaceStructureBiome(Engine engine, int blockX, int blockZ) {
return engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
}
private static Holder<Biome> resolveBiomeHolder(Registry<Biome> registry, String biomeKey) {
if (registry == null || biomeKey == null || biomeKey.isBlank()) {
return null;
@@ -809,18 +1143,31 @@ public class CustomBiomeSource extends BiomeSource {
private static final class BiomeResolution {
private final IrisBiome irisBiome;
private final boolean underground;
private final IrisDimension dimension;
private final int blockX;
private final int blockY;
private final int blockZ;
private final RNG rng;
private BiomeResolution(IrisBiome irisBiome, boolean underground, int blockX, int blockY, int blockZ, RNG rng) {
private BiomeResolution(
IrisBiome irisBiome,
boolean underground,
IrisDimension dimension,
int blockX,
int blockY,
int blockZ,
RNG rng
) {
this.irisBiome = irisBiome;
this.underground = underground;
this.dimension = dimension;
this.blockX = blockX;
this.blockY = blockY;
this.blockZ = blockZ;
this.rng = rng;
}
}
private record OwnedBiome(IrisDimension dimension, IrisBiome biome) {
}
}
@@ -1,5 +1,7 @@
package art.arcane.iris.core.nms.v26_2_R1;
import art.arcane.iris.engine.DimensionStackContext;
import art.arcane.iris.engine.DimensionStackLayout;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.NativeFeatureGenerationPolicy;
import art.arcane.iris.engine.object.IrisBiome;
@@ -40,6 +42,9 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.IntBinaryOperator;
import java.util.function.Predicate;
/**
* Bukkit twin of the modded imported-feature stage. Same control, same semantics, same seeds: with
@@ -198,8 +203,7 @@ final class ImportedFeatureStage {
*/
private Set<String> visibleBiomeKeys() {
Set<String> keys = new LinkedHashSet<>();
String namespace = engine.getDimension().getLoadKey().toLowerCase(Locale.ROOT);
for (IrisBiome irisBiome : engine.getAllBiomes()) {
for (IrisBiome irisBiome : engine.getDimension().getReachableBiomes(engine)) {
addKey(keys, irisBiome.getStructureDerivativeKey());
addKey(keys, irisBiome.getDerivativeKey());
addKey(keys, irisBiome.getVanillaDerivativeKey());
@@ -213,7 +217,7 @@ final class ImportedFeatureStage {
continue;
}
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
addKey(keys, namespace + ":" + customBiome.getId());
addKey(keys, engine.getDimension().getCustomBiomeKey(customBiome.getId()));
}
}
return keys;
@@ -226,8 +230,7 @@ final class ImportedFeatureStage {
private Map<String, Holder<Biome>> customBiomeDerivatives(Registry<Biome> registry,
Map<String, Holder<Biome>> byKey) {
Map<String, Holder<Biome>> derivatives = new HashMap<>();
String namespace = engine.getDimension().getLoadKey().toLowerCase(Locale.ROOT);
for (IrisBiome irisBiome : engine.getAllBiomes()) {
for (IrisBiome irisBiome : engine.getDimension().getReachableBiomes(engine)) {
if (!irisBiome.isCustom()) {
continue;
}
@@ -246,7 +249,7 @@ final class ImportedFeatureStage {
continue;
}
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
derivatives.put(namespace + ":" + customBiome.getId().toLowerCase(Locale.ROOT), derivative);
derivatives.put(engine.getDimension().getCustomBiomeKey(customBiome.getId()), derivative);
}
}
return derivatives;
@@ -288,14 +291,24 @@ final class ImportedFeatureStage {
List<FeatureSorter.StepFeatureData> steps = table.steps();
WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed()));
long decorationSeed = random.setDecorationSeed(level.getSeed(), origin.getX(), origin.getZ());
Set<Holder<Biome>> chunkBiomes = chunkBiomes(level, sectionPos, table);
IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData());
int staticMinY = engine.getMinHeight();
WorldGenLevel placementLevel = staticObjects.isEmpty() ? level : NativeStructureWorldgenAccess.create(
DimensionStackContext stackContext = engine.getDimensionStackContext();
Set<Holder<Biome>> chunkBiomes = chunkBiomes(level, sectionPos, table, stackContext);
IntBinaryOperator surfaceFirstFreeY = stackContext == null
? (x, z) -> level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z)
: (x, z) -> Engine.hostHeight(engine, x, z, false) + staticMinY + 1;
IntBinaryOperator floorFirstFreeY = stackContext == null
? (x, z) -> level.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, x, z)
: (x, z) -> Engine.hostHeight(engine, x, z, true) + staticMinY + 1;
WorldGenLevel placementLevel = staticObjects.isEmpty() && stackContext == null
? level
: NativeStructureWorldgenAccess.create(
level, centerPos,
(x, z) -> level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z),
(x, z) -> level.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, x, z),
position -> staticObjects.contains(position.getX(), position.getY() - staticMinY, position.getZ()));
surfaceFirstFreeY,
floorFirstFreeY,
stackContext != null,
importedFeatureProtection(staticObjects, stackContext, staticMinY));
try {
for (int stepIndex = 0; stepIndex < steps.size(); stepIndex++) {
@@ -313,6 +326,30 @@ final class ImportedFeatureStage {
}
}
private static Predicate<BlockPos> importedFeatureProtection(
IrisStaticObjectLayer staticObjects,
DimensionStackContext stackContext,
int minimumY
) {
Map<Long, DimensionStackLayout> layouts = new ConcurrentHashMap<>();
return position -> {
if (!staticObjects.isEmpty() && staticObjects.contains(
position.getX(), position.getY() - minimumY, position.getZ())) {
return true;
}
if (stackContext == null) {
return false;
}
long columnKey = ((long) position.getX() << 32)
^ (position.getZ() & 0xFFFFFFFFL);
DimensionStackLayout layout = layouts.computeIfAbsent(
columnKey,
ignored -> stackContext.sample(position.getX(), position.getZ())
);
return layout.isHostFeatureProtectedY(position.getY() - minimumY);
};
}
private void placeStep(WorldGenLevel level, FeatureTable table, FeatureSorter.StepFeatureData stepData,
Registry<PlacedFeature> featureRegistry, Set<Holder<Biome>> chunkBiomes,
ChunkGenerator owner, WorldgenRandom random, long decorationSeed,
@@ -353,12 +390,37 @@ final class ImportedFeatureStage {
return id == null ? feature.toString() : id.toString();
}
private Set<Holder<Biome>> chunkBiomes(WorldGenLevel level, SectionPos sectionPos, FeatureTable table) {
private Set<Holder<Biome>> chunkBiomes(
WorldGenLevel level,
SectionPos sectionPos,
FeatureTable table,
DimensionStackContext stackContext
) {
List<Holder<Biome>> collected = new ArrayList<>();
ChunkPos.rangeClosed(sectionPos.chunk(), 1).forEach((ChunkPos chunkPos) -> {
ChunkAccess neighbour = level.getChunk(chunkPos.x(), chunkPos.z());
for (LevelChunkSection section : neighbour.getSections()) {
section.getBiomes().getAll(collected::add);
LevelChunkSection[] sections = neighbour.getSections();
for (int sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) {
LevelChunkSection section = sections[sectionIndex];
if (stackContext == null) {
section.getBiomes().getAll(collected::add);
continue;
}
int sectionMinimumY = neighbour.getSectionYFromSectionIndex(sectionIndex) << 4;
for (int quartY = 0; quartY < 4; quartY++) {
int internalY = sectionMinimumY + (quartY << 2) - level.getMinY();
for (int quartX = 0; quartX < 4; quartX++) {
int blockX = chunkPos.getMinBlockX() + (quartX << 2);
for (int quartZ = 0; quartZ < 4; quartZ++) {
int blockZ = chunkPos.getMinBlockZ() + (quartZ << 2);
DimensionStackLayout.Layer layer = stackContext.getLayerAt(
blockX, internalY, blockZ);
if (layer.terrainContext().isSelfReferencing()) {
collected.add(section.getBiomes().get(quartX, quartY, quartZ));
}
}
}
}
}
});
Set<Holder<Biome>> present = new LinkedHashSet<>();
@@ -1,5 +1,7 @@
package art.arcane.iris.core.nms.v26_2_R1;
import art.arcane.iris.engine.DimensionStackContext;
import art.arcane.iris.engine.DimensionStackLayout;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.GenerationSessionException;
import art.arcane.iris.engine.framework.GenerationSessionLease;
@@ -115,6 +117,7 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CompletionException;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.IntBinaryOperator;
import java.util.function.Predicate;
public class IrisChunkGenerator extends CustomChunkGenerator {
private static final WrappedField<ChunkGenerator, BiomeSource> BIOME_SOURCE;
@@ -429,7 +432,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
undergroundStep,
decision.preserveSourceY(),
decision.yBand(),
(x, z) -> engine.getHeight(x, z, true) + engine.getMinHeight());
(x, z) -> Engine.hostHeight(engine, x, z, true) + engine.getMinHeight());
StructureStart wrapped = NativeStructureReferenceEnvelope.wrapForPublication(
start, structure, start.getReferences(),
NativeStructureTerrainIntegrator.resolveNativeTerrain(start, decision.terrain()),
@@ -726,6 +729,14 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
return (x, z) -> engine.getHeight(x, z, true) + runtimeMinY + 1;
}
private IntBinaryOperator hostWorldgenSurfaceHeight() {
return (x, z) -> Engine.hostHeight(engine, x, z, false) + runtimeMinY + 1;
}
private IntBinaryOperator hostWorldgenFloorHeight() {
return (x, z) -> Engine.hostHeight(engine, x, z, true) + runtimeMinY + 1;
}
@Override
public WeightedList<MobSpawnSettings.SpawnerData> getMobsAt(Holder<Biome> holder, StructureManager structuremanager, MobCategory enumcreaturetype, BlockPos blockposition) {
Holder<Biome> vanillaSpawnBiome = customBiomeSource.getVanillaSpawnBiome(holder);
@@ -868,17 +879,22 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
}
try {
WorldgenTerrainHeightmaps.primeStructurePlacement(
world, chunkPos, heightmapStarts, worldgenSurfaceHeight(), worldgenFloorHeight());
world, chunkPos, heightmapStarts,
hostWorldgenSurfaceHeight(), hostWorldgenFloorHeight());
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"heightmap priming", nativeStructureBatchContext(placementGroups),
chunkPos.x(), chunkPos.z(), error);
}
IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData());
int staticMinY = engine.getMinHeight();
WorldGenLevel boundedWorld = staticObjects.isEmpty() ? world : NativeStructureWorldgenAccess.create(
world, chunkPos, worldgenSurfaceHeight(), worldgenFloorHeight(),
position -> staticObjects.contains(position.getX(), position.getY() - staticMinY, position.getZ()));
Predicate<BlockPos> protectedPosition = nativeStructureProtection(staticObjects);
WorldGenLevel boundedWorld = staticObjects.isEmpty()
&& engine.getDimensionStackContext() == null
? world
: NativeStructureWorldgenAccess.create(
world, chunkPos, hostWorldgenSurfaceHeight(), hostWorldgenFloorHeight(),
engine.getDimensionStackContext() != null,
protectedPosition);
try {
NativeStructureVegetationClearer.clearIntersectingVegetation(
boundedWorld, chunk, area, vegetationTargets);
@@ -891,7 +907,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
try {
vacuumFoundationPlan = NativeStructureSurfaceFitter.prepareSurfaceStructures(
boundedWorld, area, terrainTargets,
(x, z) -> engine.getHeight(x, z, true) + engine.getMinHeight());
(x, z) -> Engine.hostHeight(engine, x, z, true) + engine.getMinHeight());
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"terrain integration", nativeStructureBatchContext(placementGroups),
@@ -945,17 +961,45 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
BoundingBox area, ChunkPos chunkPos, String structureId, StructureStart start,
IrisNativeStructureDecision decision) {
WorldGenLevel boundedWorld = world instanceof NativeStructureWorldgenAccess ? world : NativeStructureWorldgenAccess.create(
world, chunkPos, worldgenSurfaceHeight(), worldgenFloorHeight(), position -> false);
world,
chunkPos,
hostWorldgenSurfaceHeight(),
hostWorldgenFloorHeight(),
engine.getDimensionStackContext() != null,
nativeStructureProtection(
engine.getDimension().getStaticObjectLayer(engine.getData())));
world.setCurrentlyGenerating(() -> "Iris native structure " + structureId);
try {
NativeStructurePostProcessor.place(boundedWorld, structureManager, this, random, area, chunkPos,
structureId, start, decision, this::resolvePaletteBlock,
(x, z) -> engine.getHeight(x, z, true) + engine.getMinHeight());
(x, z) -> Engine.hostHeight(engine, x, z, true) + engine.getMinHeight());
} finally {
world.setCurrentlyGenerating(null);
}
}
private Predicate<BlockPos> nativeStructureProtection(IrisStaticObjectLayer staticObjects) {
DimensionStackContext stackContext = engine.getDimensionStackContext();
int minimumY = engine.getMinHeight();
Map<Long, DimensionStackLayout> layouts = new ConcurrentHashMap<>();
return position -> {
if (!staticObjects.isEmpty() && staticObjects.contains(
position.getX(), position.getY() - minimumY, position.getZ())) {
return true;
}
if (stackContext == null) {
return false;
}
long columnKey = ((long) position.getX() << 32)
^ (position.getZ() & 0xFFFFFFFFL);
DimensionStackLayout layout = layouts.computeIfAbsent(
columnKey,
ignored -> stackContext.sample(position.getX(), position.getZ())
);
return layout.isHostFeatureProtectedY(position.getY() - minimumY);
};
}
private List<List<Structure>> structuresByStep(Registry<Structure> registry) {
StructureStepCache cached = structureStepCache;
if (cached != null && cached.registry() == registry) {
@@ -1041,7 +1085,17 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
@Override
public void spawnOriginalMobs(WorldGenRegion region) {
ChunkPos center = region.getCenter();
Holder<Biome> visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY()));
Holder<Biome> visibleBiome;
if (engine.getDimensionStackContext() == null) {
visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY()));
} else {
visibleBiome = customBiomeSource.getVisibleSurfaceBiome(
center.getMinBlockX() + 8,
center.getMinBlockZ() + 8);
if (visibleBiome == null) {
visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY()));
}
}
Holder<Biome> vanillaBiome = customBiomeSource.getVanillaSpawnBiome(visibleBiome);
WorldgenRandom random = new WorldgenRandom(new LegacyRandomSource(RandomSupport.generateUniqueSeed()));
random.setDecorationSeed(region.getSeed(), center.getMinBlockX(), center.getMinBlockZ());
@@ -1081,9 +1135,12 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
public int getBaseHeight(int i, int j, Heightmap.Types heightmap_type, LevelHeightAccessor levelheightaccessor, RandomState randomstate) {
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_base_height");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
boolean ignoreFluid = !heightmap_type.isOpaque().test(Blocks.WATER.defaultBlockState());
int height = engine.getDimensionStackContext() == null
? engine.getHeight(i, j, ignoreFluid)
: Engine.hostHeight(engine, i, j, ignoreFluid);
return levelheightaccessor.getMinY()
+ engine.getHeight(i, j, !heightmap_type.isOpaque().test(Blocks.WATER.defaultBlockState()))
+ 1;
+ height + 1;
} catch (GenerationSessionException e) {
throw new IllegalStateException("Iris base height query could not acquire its engine runtime.", e);
}
@@ -1093,8 +1150,13 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
public NoiseColumn getBaseColumn(int i, int j, LevelHeightAccessor levelheightaccessor, RandomState randomstate) {
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_base_column");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
int block = engine.getHeight(i, j, true);
int water = engine.getHeight(i, j, false);
boolean dimensionStack = engine.getDimensionStackContext() != null;
int block = dimensionStack
? Engine.hostHeight(engine, i, j, true)
: engine.getHeight(i, j, true);
int water = dimensionStack
? Engine.hostHeight(engine, i, j, false)
: engine.getHeight(i, j, false);
BlockState[] column = new BlockState[levelheightaccessor.getHeight()];
for (int k = 0; k < column.length; k++) {
if (k <= block) {
@@ -824,6 +824,11 @@ public class NMSBinding implements INMSBinding {
return NativeStructureVolumeIndex.volumes(engine, minX, minZ, maxX, maxZ);
}
@Override
public void invalidateNativeStructureVolumeIndex(Engine engine) {
NativeStructureVolumeIndex.invalidate(engine);
}
@Override
public int getBiomeId(Biome biome) {
for (World i : Bukkit.getWorlds()) {
@@ -64,6 +64,7 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
private final ChunkPos generationCenter;
private final IntBinaryOperator surfaceFirstFreeY;
private final IntBinaryOperator floorFirstFreeY;
private final boolean virtualizeHeights;
private final Predicate<BlockPos> protectedPosition;
private final Holder<Biome> fallbackBiome;
private final BiomeManager biomeManager;
@@ -81,6 +82,7 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
boundary.surfaceFirstFreeY(), "Native structure world access requires a surface resolver");
this.floorFirstFreeY = Objects.requireNonNull(
boundary.floorFirstFreeY(), "Native structure world access requires an ocean-floor resolver");
this.virtualizeHeights = boundary.virtualizeHeights();
this.protectedPosition = Objects.requireNonNull(
boundary.protectedPosition(), "World access requires a protected-position predicate");
BlockPos biomeSample = generationCenter.getMiddleBlockPosition(delegate.getSeaLevel());
@@ -103,9 +105,10 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
static NativeStructureWorldgenAccess create(WorldGenLevel delegate, ChunkPos generationCenter,
IntBinaryOperator surfaceFirstFreeY,
IntBinaryOperator floorFirstFreeY,
boolean virtualizeHeights,
Predicate<BlockPos> protectedPosition) {
return new NativeStructureWorldgenAccess(delegate, new Boundary(
generationCenter, surfaceFirstFreeY, floorFirstFreeY, protectedPosition));
generationCenter, surfaceFirstFreeY, floorFirstFreeY, virtualizeHeights, protectedPosition));
}
@Override
@@ -250,7 +253,7 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
@Override
public int getHeight(Heightmap.Types type, int x, int z) {
if (isInsideGenerationRegion(x >> 4, z >> 4)) {
if (!virtualizeHeights && isInsideGenerationRegion(x >> 4, z >> 4)) {
return delegate.getHeight(type, x, z);
}
return height(type, x, z);
@@ -549,6 +552,7 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
private record Boundary(ChunkPos generationCenter,
IntBinaryOperator surfaceFirstFreeY,
IntBinaryOperator floorFirstFreeY,
boolean virtualizeHeights,
Predicate<BlockPos> protectedPosition) {
}
@@ -1,15 +1,29 @@
package art.arcane.iris.core.nms.v26_2_R1;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisDimension;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import org.junit.BeforeClass;
import org.junit.Test;
import java.io.IOException;
import java.lang.reflect.Proxy;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
public class CustomBiomeSourceStructureContractTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void nativeStructuresUseTerrainSafeDerivativeAtEveryBiomeBoundary() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource")));
@@ -69,14 +83,53 @@ public class CustomBiomeSourceStructureContractTest {
String realResolution = source.substring(realResolutionStart, naturalResolutionStart);
String naturalResolution = source.substring(naturalResolutionStart, resolutionEnd);
assertTrue(realResolution.contains("engine.getComplex().getTrueBiomeStream().get(blockX, blockZ)"));
assertTrue(realResolution.contains("resolveSurfaceStructureBiome(engine, blockX, blockZ)"));
assertFalse(realResolution.contains("getNaturalTrueBiomeStream()"));
assertTrue(naturalResolution.contains(
"engine.getComplex().getNaturalTrueBiomeStream().get(blockX, blockZ)"));
assertFalse(naturalResolution.contains("getTrueBiomeStream()"));
int helperStart = source.indexOf("private static IrisBiome resolveSurfaceStructureBiome(");
int helperEnd = source.indexOf("private static Holder<Biome> resolveBiomeHolder(", helperStart);
String helper = source.substring(helperStart, helperEnd);
assertTrue(helper.contains("engine.getComplex().getTrueBiomeStream().get(blockX, blockZ)"));
assertFalse(helper.contains("getDimensionStackContext()"));
assertFalse(helper.contains("getSurfaceBiome("));
assertFalse(source.contains("studioBootstrapSurfaceStructureBiomeCache"));
}
@Test
public void stackedCustomBiomesUseTheirOwningDimensionNamespace() {
IrisDimension host = new IrisDimension();
host.setLoadKey("Host");
IrisDimension upper = new IrisDimension();
upper.setLoadKey("Layers/Upper");
assertEquals("layers:upper/aurora", CustomBiomeSource.customBiomeKey(upper, "Aurora"));
assertNotEquals(
CustomBiomeSource.customBiomeKey(host, "Aurora"),
CustomBiomeSource.customBiomeKey(upper, "Aurora")
);
}
@Test
public void visibleBiomeResolutionSelectsStackOwnershipBeforeHostCaves() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource")));
int resolutionStart = source.indexOf("private BiomeResolution resolveBiomeResolution(");
int resolutionEnd = source.indexOf("private BiomeResolution createBiomeResolution(", resolutionStart);
assertTrue(resolutionStart >= 0);
assertTrue(resolutionEnd > resolutionStart);
String resolution = source.substring(resolutionStart, resolutionEnd);
assertTrue(resolution.contains("resolveDimensionStackLayer("));
assertTrue(resolution.contains("!stackLayer.terrainContext().isSelfReferencing()"));
assertTrue(resolution.indexOf("resolveDimensionStackLayer(")
< resolution.indexOf("boolean deepUnderground"));
assertTrue(resolution.contains("includeDimensionStack"));
assertTrue(source.contains("resolveBiomeResolution(x, y, z, null, false)"));
assertTrue(source.contains("resolution.dimension, customBiome.getId()"));
}
@Test
public void visibleBiomeBatchReusesTheCallersGenerationLease() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource")));
@@ -94,4 +147,126 @@ public class CustomBiomeSourceStructureContractTest {
< lookup.indexOf("getVisibleNoiseBiomeWithActiveGenerationLease(x, y, z, sampler)"));
assertFalse(source.contains("studioBootstrapNoiseBiomeCache"));
}
@Test
public void stackBiomesAreVisibleButExcludedFromStructureReachability() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource")));
assertTrue(source.contains("return possibleBiomes(true).stream()"));
assertTrue(source.contains("public Set<Holder<Biome>> possibleBiomes()"));
assertTrue(source.contains("irisBiome.getDerivativeKey()"));
assertTrue(source.contains("irisBiome.getBiomeScatter()"));
assertTrue(source.contains("irisBiome.getBiomeSkyScatter()"));
assertTrue(source.contains("return possibleBiomes(false)"));
assertTrue(source.contains("for (OwnedBiome ownedBiome : getHostOwnedBiomes(engine))"));
assertTrue(source.contains("for (OwnedBiome ownedBiome : getOwnedBiomes(engine))"));
}
@Test
public void biomeLocatorSearchesTheVisibleStack() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource")));
int start = source.indexOf("public Pair<BlockPos, Holder<Biome>> findClosestBiome3d(");
int end = source.indexOf("static int horizontalBiomeSearchQuartStep", start);
String method = source.substring(start, end);
assertTrue(method.contains("bukkit_locate_visible_biome"));
assertTrue(method.contains("return super.findClosestBiome3d("));
assertTrue(method.contains("getVisibleBiomes("));
assertTrue(method.contains("QuartPos.toBlock(quartX), QuartPos.toBlock(quartZ)"));
assertTrue(method.contains("resolveVisibleBiomeHolder("));
assertFalse(method.contains("getStructureNoiseBiomeWithActiveGenerationLease("));
assertTrue(method.indexOf("tryAcquireGenerationLease(\"bukkit_locate_visible_biome\")")
< method.indexOf("engine.getDimensionStackContext()"));
}
@Test
public void surfaceSpawnBiomeUsesTheVisibleQuartCell() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource")));
int start = source.indexOf("Holder<Biome> getVisibleSurfaceBiome(");
int end = source.indexOf("private RegistryAccess registry()", start);
String method = source.substring(start, end);
assertTrue(method.contains("QuartPos.fromBlock(blockX)"));
assertTrue(method.contains("QuartPos.toBlock(quartX)"));
assertTrue(method.contains("QuartPos.fromBlock(internalY + engine.getWorld().minHeight())"));
assertTrue(method.contains("stackContext.getLayout(sampleX, sampleZ).surfaceLayer()"));
assertTrue(method.contains("getVisibleNoiseBiomeWithActiveGenerationLease("));
assertTrue(method.contains("quartX, quartY, quartZ, null, null, cacheable"));
assertTrue(method.indexOf("boolean cacheable = isBiomeCacheable(engine, sampleX, sampleZ)")
< method.indexOf("Engine.hostHeight(engine, sampleX, sampleZ, true)"));
}
@Test
public void temporaryNaturalBiomeAnswersAreNotMemoized() throws IOException {
Engine temporary = engineAnsweringNaturalFallback(true, 12, -8);
Engine stable = engineAnsweringNaturalFallback(false, 12, -8);
assertFalse(CustomBiomeSource.isBiomeCacheable(temporary, 12, -8));
assertTrue(CustomBiomeSource.isBiomeCacheable(stable, 12, -8));
String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource")));
int structureStart = source.indexOf(
"private Holder<Biome> getStructureNoiseBiomeWithActiveGenerationLease(");
int structureEnd = source.indexOf("@Override\n public Pair<BlockPos, Holder<Biome>> findBiomeHorizontal(",
structureStart);
assertCacheGuarded(source.substring(structureStart, structureEnd), "structureBiomeCache");
int firstVisible = source.indexOf(
"Holder<Biome> getVisibleNoiseBiomeWithActiveGenerationLease(");
int visibleStart = source.indexOf(
"Holder<Biome> getVisibleNoiseBiomeWithActiveGenerationLease(", firstVisible + 1);
int visibleEnd = source.indexOf("private boolean isBiomeCacheable(", visibleStart);
assertCacheGuarded(source.substring(visibleStart, visibleEnd), "noiseBiomeCache");
String predicate = source.substring(
visibleEnd,
source.indexOf("private GenerationSessionLease tryAcquireGenerationLease(", visibleEnd)
);
assertTrue(predicate.contains("QuartPos.toBlock(quartX)"));
assertTrue(predicate.contains("QuartPos.toBlock(quartZ)"));
assertTrue(predicate.contains("return !engine.answersFromNaturalTerrain(blockX, blockZ)"));
assertFalse(predicate.contains("getDimensionStackContext()"));
}
@Test
public void importedFeaturesCollectOnlyHostOwnedStackCells() throws IOException {
Path biomeSource = Path.of(System.getProperty("iris.customBiomeSource"));
String source = Files.readString(biomeSource.getParent().resolve("ImportedFeatureStage.java"));
assertTrue(source.contains("stackContext.getLayerAt("));
assertTrue(source.contains("layer.terrainContext().isSelfReferencing()"));
assertTrue(source.contains("section.getBiomes().get(quartX, quartY, quartZ)"));
}
private static void assertCacheGuarded(String method, String cacheName) {
int predicate = method.indexOf("boolean cacheable = isBiomeCacheable(");
int read = method.indexOf(cacheName + ".get(", predicate);
int readGuard = method.lastIndexOf("if (cacheable)", read);
int bypass = method.indexOf("if (!cacheable)", read);
int write = method.indexOf(cacheName + ".putIfAbsent(", bypass);
assertTrue(predicate >= 0);
assertTrue(read > predicate);
assertTrue(readGuard >= predicate);
assertTrue(bypass > read);
assertTrue(write > bypass);
}
private static Engine engineAnsweringNaturalFallback(
boolean naturalFallback,
int expectedX,
int expectedZ
) {
return (Engine) Proxy.newProxyInstance(
Engine.class.getClassLoader(),
new Class<?>[]{Engine.class},
(proxy, method, arguments) -> {
if (!method.getName().equals("answersFromNaturalTerrain")) {
throw new UnsupportedOperationException(method.getName());
}
assertEquals(Integer.valueOf(expectedX), arguments[0]);
assertEquals(Integer.valueOf(expectedZ), arguments[1]);
return naturalFallback;
}
);
}
}
@@ -78,6 +78,8 @@ public class IrisChunkGeneratorFailureContractTest {
< placement.indexOf("for (NativePlacementGroup group"));
assertTrue(placement.indexOf("repairVacuumFoundations")
> placement.indexOf("for (NativePlacementGroup group"));
assertTrue(placement.contains("nativeStructureProtection(staticObjects)"));
assertTrue(placement.contains("Engine.hostHeight(engine, x, z, true)"));
assertFalse(placement.contains("IrisLogging.reportError"));
}
@@ -542,8 +544,8 @@ public class IrisChunkGeneratorFailureContractTest {
String spawn = source.substring(spawnStart, spawnEnd);
assertTrue(spawn.contains("customBiomeSource.getVanillaSpawnBiome(visibleBiome)"));
assertTrue(spawn.contains("customBiomeSource.getVisibleSurfaceBiome("));
assertTrue(spawn.contains("NaturalSpawner.spawnMobsForChunkGeneration("));
assertTrue(spawn.contains("region.getBiome(center.getWorldPosition().atY(region.getMaxY()))"));
assertTrue(spawn.contains("new LegacyRandomSource(RandomSupport.generateUniqueSeed())"));
assertTrue(spawn.contains("random.setDecorationSeed(region.getSeed()"));
assertFalse(spawn.contains("delegate.spawnOriginalMobs"));
@@ -135,6 +135,24 @@ public class NativeStructureWorldgenAccessTest {
assertEquals(1, recording.blockTicks.count());
}
@Test
public void virtualizedHeightsHideInRegionTerrainFromTheDelegate() {
RecordingDelegate recording = new RecordingDelegate();
ChunkPos generationCenter = generationCenter();
NativeStructureWorldgenAccess access = NativeStructureWorldgenAccess.create(
recording.world(), generationCenter,
(x, z) -> SURFACE_FIRST_FREE_Y,
(x, z) -> FLOOR_FIRST_FREE_Y,
true,
position -> false);
int x = generationCenter.getMiddleBlockX();
int z = generationCenter.getMiddleBlockZ();
assertEquals(SURFACE_FIRST_FREE_Y, access.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z));
assertEquals(FLOOR_FIRST_FREE_Y, access.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, x, z));
assertEquals(0, recording.heightReads);
}
@Test
public void protectedCellsRemainReadableButRejectWritesTilesAndTicks() {
RecordingDelegate recording = new RecordingDelegate();
@@ -142,7 +160,9 @@ public class NativeStructureWorldgenAccessTest {
NativeStructureWorldgenAccess access = NativeStructureWorldgenAccess.create(
recording.world(), generationCenter(),
(x, z) -> SURFACE_FIRST_FREE_Y,
(x, z) -> FLOOR_FIRST_FREE_Y, position::equals);
(x, z) -> FLOOR_FIRST_FREE_Y,
false,
position::equals);
assertSame(Blocks.DIRT, access.getBlockState(position).getBlock());
assertFalse(access.ensureCanWrite(position));
@@ -211,7 +231,9 @@ public class NativeStructureWorldgenAccessTest {
return NativeStructureWorldgenAccess.create(
recording.world(), generationCenter(),
(x, z) -> SURFACE_FIRST_FREE_Y,
(x, z) -> FLOOR_FIRST_FREE_Y, position -> false);
(x, z) -> FLOOR_FIRST_FREE_Y,
false,
position -> false);
}
private static ChunkPos generationCenter() {
@@ -332,6 +332,18 @@ public class NativeStructureFactoryTest {
assertTrue(failureStart < source.indexOf("NativeStructureOwnershipStore.record("));
}
@Test
public void manualNativeStructureGenerationUsesTheRootTerrainHeight() throws Exception {
String source = Files.readString(
Path.of(System.getProperty("iris.nativeStructureStartInjectorSource")),
StandardCharsets.UTF_8);
assertTrue(source.contains(
"Engine.hostHeight(context.engine(), x, z, true)"));
assertFalse(source.contains(
"context.engine().getHeight(x, z, true)"));
}
@Test
public void failedManualStartPublicationClearsRecordedOwnershipAndPreservesTheFailure() throws Exception {
String source = Files.readString(
@@ -84,6 +84,11 @@ public final class BukkitEnginePlatformHooks implements EnginePlatformHooks {
"iris");
}
@Override
public void prepareRuntimeHotload(Engine engine) {
INMS.get().invalidateNativeStructureVolumeIndex(engine);
}
@Override
public void applyWorldBoundary(Engine engine) {
IrisDimension expectedDimension = engine.getDimension();
@@ -163,7 +163,7 @@ public final class NativeStructureOwnershipRecovery {
level,
biome -> true,
generator.getSeaLevel(),
(x, z) -> engine.getHeight(x, z, true) + engine.getMinHeight()
(x, z) -> Engine.hostHeight(engine, x, z, true) + engine.getMinHeight()
);
return NativeStructureFactory.generate(generationContext, holder, plan, references);
}
@@ -90,7 +90,7 @@ public final class NativeStructureStartInjector {
chunk,
biome -> true,
context.generator().getSeaLevel(),
(x, z) -> context.engine().getHeight(x, z, true)
(x, z) -> Engine.hostHeight(context.engine(), x, z, true)
+ context.engine().getMinHeight()
);
StructureStart generated = NativeStructureFactory.generate(
@@ -89,6 +89,18 @@ public final class NativeStructureVolumeIndex {
}
}
public static void invalidate(Engine engine) {
if (engine == null) {
return;
}
synchronized (INDEXES) {
NativeStructureVolumeIndex current = INDEXES.get(engine);
if (current != null) {
INDEXES.put(engine, current.fresh());
}
}
}
public static KList<NativeStructureVolume> volumes(Engine engine, int minX, int minZ, int maxX, int maxZ) {
NativeStructureVolumeIndex index = engine == null ? null : INDEXES.get(engine);
return index == null ? NativeStructureVolume.NONE : index.resolve(engine, minX, minZ, maxX, maxZ);
@@ -102,6 +114,12 @@ public final class NativeStructureVolumeIndex {
return ORIGIN_REACH_CHUNKS;
}
private NativeStructureVolumeIndex fresh() {
return context == null
? new NativeStructureVolumeIndex(origins)
: new NativeStructureVolumeIndex(context);
}
KList<NativeStructureVolume> resolve(Engine engine, int minX, int minZ, int maxX, int maxZ) {
int fromChunkX = Math.min(minX, maxX) >> 4;
int toChunkX = Math.max(minX, maxX) >> 4;
@@ -323,7 +341,7 @@ public final class NativeStructureVolumeIndex {
}
private static IntBinaryOperator surfaceHeight(Engine engine) {
return (x, z) -> engine.getHeight(x, z, true) + engine.getMinHeight();
return (x, z) -> Engine.hostHeight(engine, x, z, true) + engine.getMinHeight();
}
static KList<NativeStructureVolume> appendPieces(KList<NativeStructureVolume> volumes,
@@ -18,16 +18,22 @@
package art.arcane.iris.modded;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.DimensionStackContext;
import art.arcane.iris.engine.DimensionStackLayout;
import art.arcane.iris.engine.DimensionTerrainContext;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.GenerationSessionException;
import art.arcane.iris.engine.framework.GenerationSessionLease;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.volmlib.util.math.RNG;
import com.mojang.datafixers.util.Pair;
import com.mojang.serialization.MapCodec;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.QuartPos;
@@ -37,12 +43,15 @@ import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.util.RandomSource;
import net.minecraft.util.Mth;
import net.minecraft.world.level.LevelReader;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeSource;
import net.minecraft.world.level.biome.Climate;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureSet;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
@@ -196,11 +205,16 @@ final class IrisModdedBiomeSource extends BiomeSource {
private List<Holder<Biome>> collectPossibleBiomeHolders() {
BiomeKeySets keys = biomeKeySets();
Set<String> generatedBiomeKeys = requireConfiguredStructureBiomeKeys(keys.required());
Set<String> visibleBiomeKeys = keys.visibleOnly();
LinkedHashSet<String> generatedBiomeKeys = new LinkedHashSet<>(
requireConfiguredStructureBiomeKeys(keys.structureRequired()));
generatedBiomeKeys.addAll(keys.visibleRequired());
LinkedHashSet<String> visibleBiomeKeys = new LinkedHashSet<>(keys.visibleOnly());
Registry<Biome> registry = biomeRegistry();
LinkedHashSet<Holder<Biome>> possible = new LinkedHashSet<>();
if (registry == null) {
if (keys.visibleFallbackPossible()) {
visibleBiomeKeys.add("minecraft:plains");
}
for (Holder<Biome> biome : serializedSource.possibleBiomes()) {
String key = holderKey(biome);
if (isGeneratedBiomeKey(key, generatedBiomeKeys)
@@ -215,6 +229,9 @@ final class IrisModdedBiomeSource extends BiomeSource {
throw new IllegalStateException("Iris structure biomes are not registered: "
+ missingBiomeKeys);
}
if (needsVisibleFallback(keys, registry)) {
visibleBiomeKeys.add("minecraft:plains");
}
// Registry-ordered, never a hash-ordered walk: see possibleBiomes().
registry.listElements().forEach((Holder.Reference<Biome> reference) -> {
String key = holderKey(reference);
@@ -223,7 +240,7 @@ final class IrisModdedBiomeSource extends BiomeSource {
possible.add(reference);
}
});
warnUnregisteredVisibleBiomes(registry, visibleBiomeKeys);
warnUnregisteredVisibleBiomes(registry, keys.visibleOnly());
}
if (possible.isEmpty()) {
String phase = registry == null ? "serialized biome bootstrap" : "biome registry";
@@ -233,6 +250,36 @@ final class IrisModdedBiomeSource extends BiomeSource {
return List.copyOf(possible);
}
private List<Holder<Biome>> collectVisibleBiomeHolders() {
BiomeKeySets keys = biomeKeySets();
Set<String> visibleBiomeKeys = keys.visibleEmittable();
Registry<Biome> registry = biomeRegistry();
boolean includeFallback = registry == null
? keys.visibleFallbackPossible()
: needsVisibleFallback(keys, registry);
ArrayList<Holder<Biome>> visible = new ArrayList<>();
for (Holder<Biome> biome : resolvePossibleBiomes().ordered()) {
String key = holderKey(biome);
if (isGeneratedBiomeKey(key, visibleBiomeKeys)
|| includeFallback && "minecraft:plains".equals(key)) {
visible.add(biome);
}
}
return List.copyOf(visible);
}
private static boolean needsVisibleFallback(BiomeKeySets keys, Registry<Biome> registry) {
if (keys.visibleFallbackPossible()) {
return true;
}
for (String key : keys.visibleEmittable()) {
if (resolveHolder(registry, key) == null) {
return true;
}
}
return false;
}
/**
* Scatter and derivative keys are advisory: a typo there must not stop a world from loading the way a
* missing structure biome does, so they are reported once and skipped.
@@ -273,19 +320,91 @@ final class IrisModdedBiomeSource extends BiomeSource {
}
}
@Override
public Pair<BlockPos, Holder<Biome>> findClosestBiome3d(
BlockPos origin,
int searchRadius,
int sampleResolutionHorizontal,
int sampleResolutionVertical,
Predicate<Holder<Biome>> allowed,
Climate.Sampler sampler,
LevelReader level
) {
Engine engine = engineOrNull();
if (engine == null) {
return serializedSource.findClosestBiome3d(
origin, searchRadius, sampleResolutionHorizontal, sampleResolutionVertical,
allowed, sampler, level);
}
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_locate_visible_biome");
if (lease == null) {
throw new IllegalStateException("Iris visible biome search was rejected during an engine transition");
}
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
if (!isReady(engine)) {
throw new IllegalStateException("Iris visible biome search has no active engine runtime");
}
DimensionStackContext stackContext = engine.getDimensionStackContext();
if (stackContext == null) {
return super.findClosestBiome3d(
origin, searchRadius, sampleResolutionHorizontal, sampleResolutionVertical,
allowed, sampler, level);
}
Set<Holder<Biome>> candidates = new HashSet<>();
for (Holder<Biome> biome : collectVisibleBiomeHolders()) {
if (allowed.test(biome)) {
candidates.add(biome);
}
}
if (candidates.isEmpty()) {
return null;
}
int sampleRadius = Math.floorDiv(searchRadius, sampleResolutionHorizontal);
int[] sampleYs = Mth.outFromOrigin(
origin.getY(), level.getMinY() + 1, level.getMaxY() + 1, sampleResolutionVertical).toArray();
for (BlockPos.MutableBlockPos sampleColumn : BlockPos.spiralAround(
BlockPos.ZERO, sampleRadius, Direction.EAST, Direction.SOUTH)) {
int blockX = origin.getX() + sampleColumn.getX() * sampleResolutionHorizontal;
int blockZ = origin.getZ() + sampleColumn.getZ() * sampleResolutionHorizontal;
int quartX = QuartPos.fromBlock(blockX);
int quartZ = QuartPos.fromBlock(blockZ);
DimensionStackLayout stackLayout = stackContext.sample(
QuartPos.toBlock(quartX), QuartPos.toBlock(quartZ));
for (int blockY : sampleYs) {
int quartY = QuartPos.fromBlock(blockY);
Holder<Biome> visibleBiome = resolveVisibleBiome(
engine, quartX, quartY, quartZ, sampler, stackLayout);
if (candidates.contains(visibleBiome)) {
return Pair.of(new BlockPos(blockX, blockY, blockZ), visibleBiome);
}
}
}
return null;
}
}
private Holder<Biome> getNoiseBiome(Engine engine, int quartX, int quartY, int quartZ,
Climate.Sampler sampler) {
if (isGuaranteedSurfaceBiome(quartY, engine.getMinHeight())) {
return getSurfaceStructureBiome(engine, quartX, quartZ, sampler);
}
boolean cacheable = isBiomeCacheable(
engine,
QuartPos.toBlock(quartX),
QuartPos.toBlock(quartZ)
);
long key = packNoiseKey(quartX, quartY, quartZ);
BiomeHolderTable cache = structureBiomeCache;
Holder<Biome> cached = cache.get(key);
if (cached != null) {
return cached;
if (cacheable) {
Holder<Biome> cached = cache.get(key);
if (cached != null) {
return cached;
}
}
Holder<Biome> resolved = resolveStructureBiome(engine, quartX, quartY, quartZ, sampler);
cache.put(key, resolved);
if (cacheable) {
cache.put(key, resolved);
}
return resolved;
}
@@ -302,14 +421,81 @@ final class IrisModdedBiomeSource extends BiomeSource {
if (!isReady(engine)) {
throw new IllegalStateException("Iris visible biome lookup has no active engine runtime");
}
long key = packNoiseKey(quartX, quartY, quartZ);
BiomeHolderTable cache = visibleBiomeCache;
return getVisibleNoiseBiomeWithActiveGenerationLease(
engine, quartX, quartY, quartZ, sampler);
}
}
private Holder<Biome> getVisibleNoiseBiomeWithActiveGenerationLease(
Engine engine,
int quartX,
int quartY,
int quartZ,
Climate.Sampler sampler
) {
boolean cacheable = isBiomeCacheable(
engine,
QuartPos.toBlock(quartX),
QuartPos.toBlock(quartZ)
);
long key = packNoiseKey(quartX, quartY, quartZ);
BiomeHolderTable cache = visibleBiomeCache;
if (cacheable) {
Holder<Biome> cached = cache.get(key);
if (cached != null) {
return cached;
}
Holder<Biome> resolved = resolveVisibleBiome(engine, quartX, quartY, quartZ, sampler);
}
Holder<Biome> resolved = resolveVisibleBiome(engine, quartX, quartY, quartZ, sampler);
if (cacheable) {
cache.put(key, resolved);
}
return resolved;
}
Holder<Biome> getVisibleSurfaceBiome(int blockX, int blockZ) {
Engine engine = engineOrNull();
if (engine == null) {
return null;
}
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_surface_spawn_biome");
if (lease == null) {
return null;
}
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
if (!isReady(engine)) {
return null;
}
Registry<Biome> registry = biomeRegistry();
if (registry == null) {
return null;
}
int quartX = QuartPos.fromBlock(blockX);
int quartZ = QuartPos.fromBlock(blockZ);
int sampleX = QuartPos.toBlock(quartX);
int sampleZ = QuartPos.toBlock(quartZ);
boolean cacheable = isBiomeCacheable(engine, sampleX, sampleZ);
DimensionStackContext stackContext = engine.getDimensionStackContext();
DimensionStackLayout.Layer layer = stackContext == null
? null
: stackContext.getLayout(sampleX, sampleZ).surfaceLayer();
int internalY = layer == null
? Engine.hostHeight(engine, sampleX, sampleZ, true)
: layer.clippedSurfaceY();
int quartY = QuartPos.fromBlock(internalY + engine.getMinHeight());
long key = packNoiseKey(quartX, quartY, quartZ);
BiomeHolderTable cache = visibleBiomeCache;
if (cacheable) {
Holder<Biome> cached = cache.get(key);
if (cached != null) {
return cached;
}
}
Holder<Biome> resolved = resolveVisibleBiome(
engine, quartX, quartY, quartZ, null);
if (cacheable) {
cache.put(key, resolved);
}
return resolved;
}
}
@@ -395,7 +581,9 @@ final class IrisModdedBiomeSource extends BiomeSource {
if (registry == null) {
throw new IllegalStateException("Iris structure biome lookup has no biome registry");
}
IrisBiome irisBiome = engine.getComplex().getTrueBiomeStream().get(quartX << 2, quartZ << 2);
int blockX = quartX << 2;
int blockZ = quartZ << 2;
IrisBiome irisBiome = resolveSurfaceStructureBiome(engine, blockX, blockZ);
if (irisBiome == null) {
throw new IllegalStateException("Iris returned no surface structure biome at quart "
+ quartX + "," + quartZ);
@@ -412,7 +600,8 @@ final class IrisModdedBiomeSource extends BiomeSource {
private Holder<Biome> resolveStructureBiome(Engine engine, int quartX, int quartY, int quartZ,
Climate.Sampler sampler) {
Registry<Biome> registry = biomeRegistry();
BiomeResolution resolution = resolveBiomeResolution(engine, quartX, quartY, quartZ);
BiomeResolution resolution = resolveStructureBiomeResolution(
engine, quartX, quartY, quartZ);
if (resolution == null) {
throw new IllegalStateException("Iris returned no structure biome at quart "
+ quartX + "," + quartY + "," + quartZ);
@@ -431,11 +620,36 @@ final class IrisModdedBiomeSource extends BiomeSource {
private Holder<Biome> resolveVisibleBiome(Engine engine, int quartX, int quartY, int quartZ,
Climate.Sampler sampler) {
return resolveVisibleBiome(engine, quartX, quartY, quartZ, sampler, null);
}
private Holder<Biome> resolveVisibleBiome(
Engine engine,
int quartX,
int quartY,
int quartZ,
Climate.Sampler sampler,
DimensionStackLayout stackLayout
) {
Registry<Biome> registry = biomeRegistry();
BiomeResolution resolution = resolveBiomeResolution(engine, quartX, quartY, quartZ);
BiomeResolution resolution = resolveBiomeResolution(
engine, quartX, quartY, quartZ, true, stackLayout);
if (resolution == null || registry == null) {
return serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler);
}
return resolveVisibleBiome(
engine, registry, resolution, quartX, quartY, quartZ, sampler);
}
private Holder<Biome> resolveVisibleBiome(
Engine engine,
Registry<Biome> registry,
BiomeResolution resolution,
int quartX,
int quartY,
int quartZ,
Climate.Sampler sampler
) {
String biomeKey;
if (resolution.irisBiome().isCustom()) {
IrisBiomeCustom customBiome = resolution.irisBiome().getCustomBiome(
@@ -444,7 +658,8 @@ final class IrisModdedBiomeSource extends BiomeSource {
return fallbackBiome(registry, "custom derivative of '"
+ resolution.irisBiome().getLoadKey() + "'", quartX, quartY, quartZ, sampler);
}
biomeKey = ModdedWorldgenIds.biomeRef(engine, customBiome.getId());
biomeKey = customBiomeRef(
resolution.packName(), resolution.dimension(), customBiome.getId());
} else if (resolution.underground()) {
biomeKey = resolution.irisBiome().getGroundBiomeKey(
resolution.rng(), engine, resolution.blockX(), resolution.blockY(), resolution.blockZ());
@@ -459,6 +674,37 @@ final class IrisModdedBiomeSource extends BiomeSource {
}
private BiomeResolution resolveBiomeResolution(Engine engine, int quartX, int quartY, int quartZ) {
return resolveBiomeResolution(engine, quartX, quartY, quartZ, true);
}
private BiomeResolution resolveStructureBiomeResolution(
Engine engine,
int quartX,
int quartY,
int quartZ
) {
return resolveBiomeResolution(engine, quartX, quartY, quartZ, false);
}
private BiomeResolution resolveBiomeResolution(
Engine engine,
int quartX,
int quartY,
int quartZ,
boolean includeDimensionStack
) {
return resolveBiomeResolution(
engine, quartX, quartY, quartZ, includeDimensionStack, null);
}
private BiomeResolution resolveBiomeResolution(
Engine engine,
int quartX,
int quartY,
int quartZ,
boolean includeDimensionStack,
DimensionStackLayout stackLayout
) {
if (!isReady(engine)) {
return null;
}
@@ -467,27 +713,75 @@ final class IrisModdedBiomeSource extends BiomeSource {
int blockZ = quartZ << 2;
int internalY = blockY - engine.getMinHeight();
int caveSwitchY = Math.max(-8 - engine.getMinHeight(), 40);
DimensionStackLayout.Layer stackLayer = includeDimensionStack
? stackLayout == null
? resolveDimensionStackLayer(engine, blockX, internalY, blockZ)
: stackLayout.layerAt(internalY)
: null;
IrisDimension owningDimension = stackLayer == null
? engine.getDimension()
: stackLayer.terrainContext().getDimension();
IrisData owningData = stackLayer == null
? engine.getData()
: stackLayer.terrainContext().getData();
if (stackLayer != null && !stackLayer.terrainContext().isSelfReferencing()) {
IrisBiome stackedBiome = stackLayer.biome();
if (stackedBiome == null) {
return null;
}
return createBiomeResolution(
engine,
stackedBiome,
false,
owningDimension,
owningData,
blockX,
blockY,
blockZ
);
}
boolean underground = false;
IrisBiome irisBiome;
if (internalY <= caveSwitchY) {
int surfaceY = engine.getComplex().getHeightStream().get(blockX, blockZ).intValue();
if (stackLayer != null) {
int surfaceY = stackLayer.surfaceY();
underground = isUnderground(internalY, surfaceY);
irisBiome = underground
? engine.getCaveBiome(
blockX,
internalY,
blockZ,
null,
stackLayer.biome(),
surfaceY
)
: stackLayer.biome();
} else if (internalY <= caveSwitchY) {
int surfaceY = Engine.hostHeight(engine, blockX, blockZ, true);
underground = isUnderground(internalY, surfaceY);
irisBiome = underground
? engine.getCaveBiome(blockX, internalY, blockZ)
: engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
: engine.getHostSurfaceBiome(blockX, blockZ);
} else {
irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
irisBiome = engine.getHostSurfaceBiome(blockX, blockZ);
}
if (irisBiome == null && underground) {
irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
irisBiome = stackLayer == null
? engine.getHostSurfaceBiome(blockX, blockZ)
: stackLayer.biome();
}
if (irisBiome == null) {
return null;
}
IrisModdedChunkGenerator current = generator;
long worldSeed = current == null ? engine.getWorld().getRawWorldSeed() : current.visibleBiomeSeed();
long seed = biomeResolutionSeed(worldSeed, blockX, blockY, blockZ);
return new BiomeResolution(irisBiome, underground, blockX, blockY, blockZ, new RNG(seed));
return createBiomeResolution(
engine,
irisBiome,
underground,
owningDimension,
owningData,
blockX,
blockY,
blockZ
);
}
private Holder<Biome> resolveRequiredStructureBiome(int quartX, int quartY, int quartZ) {
@@ -510,9 +804,10 @@ final class IrisModdedBiomeSource extends BiomeSource {
}
IrisBiome irisBiome;
if (isGuaranteedSurfaceBiome(quartY, engine.getMinHeight())) {
irisBiome = engine.getComplex().getTrueBiomeStream().get(quartX << 2, quartZ << 2);
irisBiome = resolveSurfaceStructureBiome(engine, quartX << 2, quartZ << 2);
} else {
BiomeResolution resolution = resolveBiomeResolution(engine, quartX, quartY, quartZ);
BiomeResolution resolution = resolveStructureBiomeResolution(
engine, quartX, quartY, quartZ);
irisBiome = resolution == null ? null : resolution.irisBiome();
}
if (irisBiome == null) {
@@ -557,6 +852,10 @@ final class IrisModdedBiomeSource extends BiomeSource {
return internalY <= surfaceY - 8;
}
static boolean isBiomeCacheable(Engine engine, int blockX, int blockZ) {
return !engine.answersFromNaturalTerrain(blockX, blockZ);
}
static boolean isMonumentSurfaceBiomeQuery(int blockY, int radius, int minHeight, int fluidHeight) {
return radius == 29 && blockY == minHeight + fluidHeight;
}
@@ -612,7 +911,7 @@ final class IrisModdedBiomeSource extends BiomeSource {
}
private Set<String> exactStructureBiomeKeys() {
return biomeKeySets().required();
return biomeKeySets().structureRequired();
}
/**
@@ -636,11 +935,12 @@ final class IrisModdedBiomeSource extends BiomeSource {
private BiomeKeySets collectBiomeKeySets(long generation) {
IrisModdedChunkGenerator current = generator;
if (current == null) {
return new BiomeKeySets(generation, Set.of(), Set.of());
return new BiomeKeySets(generation, Set.of(), Set.of(), Set.of(), Set.of(), false);
}
Engine engine = current.structureEngineOrNull();
if (engine == null) {
return new BiomeKeySets(generation, current.configuredStructureBiomeKeys(), Set.of());
return new BiomeKeySets(
generation, current.configuredStructureBiomeKeys(), Set.of(), Set.of(), Set.of(), false);
}
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_structure_biome_keys");
if (lease == null) {
@@ -650,36 +950,154 @@ final class IrisModdedBiomeSource extends BiomeSource {
if (!isReady(engine)) {
throw new IllegalStateException("Iris structure biome key lookup has no active engine runtime");
}
LinkedHashSet<String> required = new LinkedHashSet<>();
LinkedHashSet<String> visible = new LinkedHashSet<>();
for (IrisBiome irisBiome : engine.getAllBiomes()) {
String derivative = normalizeKey(irisBiome.getStructureDerivativeKey());
if (derivative != null) {
required.add(derivative);
}
Set<String> structureRequired = collectStructureBiomeKeys(
engine.getDimension(), engine.getData());
LinkedHashSet<String> visibleRequired = new LinkedHashSet<>();
LinkedHashSet<String> visibleEmittable = new LinkedHashSet<>();
boolean visibleFallbackPossible = false;
for (OwnedBiome ownedBiome : getOwnedBiomes(engine)) {
IrisBiome irisBiome = ownedBiome.biome();
if (irisBiome.isCustom()) {
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
required.add(ModdedWorldgenIds.biomeRef(engine, customBiome.getId()));
String customKey = customBiomeRef(
ownedBiome.data().getDataFolder().getName(),
ownedBiome.dimension(),
customBiome.getId()
);
visibleRequired.add(customKey);
visibleEmittable.add(customKey);
}
continue;
}
addVisibleBiomeKey(visible, irisBiome.getDerivativeKey());
visibleFallbackPossible |= !addVisibleBiomeKey(
visibleEmittable, irisBiome.getDerivativeKey());
for (String scatter : irisBiome.getBiomeScatter()) {
addVisibleBiomeKey(visible, scatter);
visibleFallbackPossible |= !addVisibleBiomeKey(visibleEmittable, scatter);
}
for (String scatter : irisBiome.getBiomeSkyScatter()) {
addVisibleBiomeKey(visible, scatter);
visibleFallbackPossible |= !addVisibleBiomeKey(visibleEmittable, scatter);
}
}
visible.removeAll(required);
return new BiomeKeySets(generation, Set.copyOf(required), Set.copyOf(visible));
LinkedHashSet<String> visible = new LinkedHashSet<>(visibleEmittable);
visible.removeAll(structureRequired);
visible.removeAll(visibleRequired);
return new BiomeKeySets(
generation,
Set.copyOf(structureRequired),
Set.copyOf(visibleRequired),
Set.copyOf(visible),
Set.copyOf(visibleEmittable),
visibleFallbackPossible
);
}
}
private static void addVisibleBiomeKey(Set<String> target, String key) {
String normalized = normalizeKey(key);
if (normalized != null) {
target.add(normalized);
static Set<String> collectStructureBiomeKeys(IrisDimension dimension, IrisData data) {
LinkedHashSet<String> keys = new LinkedHashSet<>();
for (IrisBiome irisBiome : dimension.getReachableBiomes(() -> data)) {
String derivative = normalizeKey(irisBiome.getStructureDerivativeKey());
if (derivative != null) {
keys.add(derivative);
}
}
return Set.copyOf(keys);
}
private static boolean addVisibleBiomeKey(Set<String> target, String key) {
String normalized = normalizeKey(key);
if (normalized == null) {
return false;
}
target.add(normalized);
return true;
}
static String customBiomeRef(String packName, IrisDimension dimension, String customBiomeId) {
return ModdedWorldgenIds.biomeRef(packName, dimension.getLoadKey(), customBiomeId);
}
private BiomeResolution createBiomeResolution(
Engine engine,
IrisBiome irisBiome,
boolean underground,
IrisDimension dimension,
IrisData data,
int blockX,
int blockY,
int blockZ
) {
IrisModdedChunkGenerator current = generator;
long worldSeed = current == null
? engine.getWorld().getRawWorldSeed()
: current.visibleBiomeSeed();
long seed = biomeResolutionSeed(worldSeed, blockX, blockY, blockZ);
return new BiomeResolution(
irisBiome,
underground,
dimension,
data.getDataFolder().getName(),
blockX,
blockY,
blockZ,
new RNG(seed)
);
}
private static List<OwnedBiome> getOwnedBiomes(Engine engine) {
ArrayList<OwnedBiome> biomes = new ArrayList<>();
IrisDimension hostDimension = engine.getDimension();
IrisData hostData = engine.getData();
addOwnedBiomes(
biomes,
hostDimension,
hostData,
hostDimension.getReachableBiomes(engine)
);
DimensionStackContext stackContext = engine.getDimensionStackContext();
if (stackContext == null) {
return biomes;
}
for (DimensionTerrainContext terrainContext : stackContext.getLayersBottomToTop()) {
if (terrainContext.isSelfReferencing()) {
continue;
}
IrisDimension dimension = terrainContext.getDimension();
IrisData data = terrainContext.getData();
addOwnedBiomes(
biomes,
dimension,
data,
dimension.getReachableBiomes(terrainContext)
);
}
return biomes;
}
private static void addOwnedBiomes(
List<OwnedBiome> target,
IrisDimension dimension,
IrisData data,
Iterable<IrisBiome> biomes
) {
for (IrisBiome biome : biomes) {
if (biome != null) {
target.add(new OwnedBiome(dimension, data, biome));
}
}
}
private static DimensionStackLayout.Layer resolveDimensionStackLayer(
Engine engine,
int blockX,
int internalY,
int blockZ
) {
DimensionStackContext stackContext = engine.getDimensionStackContext();
return stackContext == null ? null : stackContext.getLayerAt(blockX, internalY, blockZ);
}
private static IrisBiome resolveSurfaceStructureBiome(Engine engine, int blockX, int blockZ) {
return engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
}
private static Set<String> registeredBiomeKeys(Registry<Biome> registry) {
@@ -726,7 +1144,13 @@ final class IrisModdedBiomeSource extends BiomeSource {
Holder<Biome> plains = resolveHolder(registry, "minecraft:plains");
warnUnresolvedBiome(unresolvedKey, plains == null ? "the serialized biome source" : "minecraft:plains",
quartX, quartY, quartZ);
return plains == null ? serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler) : plains;
if (plains != null) {
return plains;
}
if (sampler == null) {
throw new IllegalStateException("Iris cannot resolve a registered fallback biome");
}
return serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler);
}
private void warnUnresolvedBiome(String unresolvedKey, String fallback,
@@ -753,11 +1177,29 @@ final class IrisModdedBiomeSource extends BiomeSource {
return ((long) x << 32) ^ ((long) z & 4294967295L);
}
private record BiomeResolution(IrisBiome irisBiome, boolean underground, int blockX, int blockY,
int blockZ, RNG rng) {
private record BiomeResolution(
IrisBiome irisBiome,
boolean underground,
IrisDimension dimension,
String packName,
int blockX,
int blockY,
int blockZ,
RNG rng
) {
}
private record BiomeKeySets(long generation, Set<String> required, Set<String> visibleOnly) {
private record OwnedBiome(IrisDimension dimension, IrisData data, IrisBiome biome) {
}
private record BiomeKeySets(
long generation,
Set<String> structureRequired,
Set<String> visibleRequired,
Set<String> visibleOnly,
Set<String> visibleEmittable,
boolean visibleFallbackPossible
) {
}
private record PossibleBiomes(long generation, List<Holder<Biome>> ordered, Set<Holder<Biome>> set) {
@@ -30,6 +30,7 @@ import art.arcane.iris.engine.object.IrisImportedStructureControl;
import art.arcane.iris.nativegen.NativeStructureStartInjector;
import art.arcane.iris.nativegen.NativeStructureReferenceRepair;
import art.arcane.iris.nativegen.NativeStructureVanillaLocator;
import art.arcane.iris.nativegen.NativeStructureVolumeIndex;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState;
@@ -89,7 +90,9 @@ import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -181,11 +184,18 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
}
requireGlobalStructureGeneration(
level.getServer().getWorldGenSettings().options().generateStructures(), dimensionKey);
Engine activeEngine = engineIfBound();
Engine replacement = ModdedWorldEngines.prepareReplacement(level, pack, packDimensionKey, seed);
try {
if (activeEngine != null) {
requireStructureBiomeUniverseCompatible(
activeEngine.getDimension(), replacement.getDimension());
}
nativeStructures.installVolumeIndex(level, replacement);
ModdedWorldEngines.installReplacement(level, replacement);
replacement.getPlatformHooks().applyWorldBoundary(replacement);
} catch (Throwable error) {
NativeStructureVolumeIndex.uninstall(replacement);
try {
ModdedWorldEngines.closeUnregistered(replacement);
} catch (Throwable cleanupError) {
@@ -203,17 +213,14 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
this.activePack = pack;
this.activeDimensionKey = packDimensionKey;
this.seedOverride = seed;
this.engine = replacement;
this.configuredStructureBiomeKeys = null;
this.configuredPack = null;
this.heightMetadata = engineHeights(replacement);
this.engineBinding.reset();
this.engineBinding.complete(replacement);
this.announced.set(false);
this.structureBiomeSource.clearCaches();
this.importedFeatures.invalidate();
this.nativeStructures.clearWorldCheckStructureShifts();
this.spawnTables.resetVanillaSpawnBiomes();
resetRuntimeCaches();
this.engine = replacement;
this.engineBinding.complete(replacement);
// Bind time: a feature-order cycle in the new pack is reported here, once, and degrades to features-off.
// Never waits on a build owned by another thread: this method owns the generator monitor and the build
// path can need it.
@@ -751,7 +758,38 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
return lastChunkGenAt;
}
public void onHotload() {
public void prepareRuntimeHotload(ServerLevel level, Engine current) {
if (this.engine != current || level.getChunkSource().getGenerator() != this) {
throw new IllegalStateException("Iris generator '" + dimensionKey
+ "' cannot prepare caches for an unrelated engine hotload");
}
resetRuntimeCaches();
nativeStructures.installVolumeIndex(level, current);
}
public static void requireStructureBiomeUniverseCompatible(
IrisDimension active,
IrisDimension replacement
) {
IrisData activeData = Objects.requireNonNull(active.getLoader(),
"Active Iris dimension has no pack loader");
IrisData replacementData = Objects.requireNonNull(replacement.getLoader(),
"Replacement Iris dimension has no pack loader");
requireStructureBiomeUniverseCompatible(
IrisModdedBiomeSource.collectStructureBiomeKeys(active, activeData),
IrisModdedBiomeSource.collectStructureBiomeKeys(replacement, replacementData));
}
static void requireStructureBiomeUniverseCompatible(Set<String> active, Set<String> replacement) {
if (active.equals(replacement)) {
return;
}
throw new IllegalArgumentException("Iris cannot hotload a changed host structure-biome derivative set "
+ "from " + new TreeSet<>(active) + " to " + new TreeSet<>(replacement)
+ ". Restart the server so Minecraft can rebuild its immutable structure state.");
}
private void resetRuntimeCaches() {
configuredStructureBiomeKeys = null;
structureBiomeSource.clearCaches();
importedFeatures.invalidate();
@@ -986,7 +1024,17 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
Registry<Biome> registry = region.registryAccess().lookupOrThrow(Registries.BIOME);
spawnTables.initializeVanillaSpawnBiomes(registry);
ChunkPos center = region.getCenter();
Holder<Biome> visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY()));
Holder<Biome> visibleBiome;
if (engine == null || engine.getDimensionStackContext() == null) {
visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY()));
} else {
visibleBiome = structureBiomeSource.getVisibleSurfaceBiome(
center.getMinBlockX() + 8,
center.getMinBlockZ() + 8);
if (visibleBiome == null) {
visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY()));
}
}
Holder<Biome> vanillaBiome = spawnTables.vanillaSpawnBiome(visibleBiome.value());
WorldgenRandom random = new WorldgenRandom(new LegacyRandomSource(RandomSupport.generateUniqueSeed()));
random.setDecorationSeed(region.getSeed(), center.getMinBlockX(), center.getMinBlockZ());
@@ -1029,7 +1077,10 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
boolean ignoreFluid = !type.isOpaque().test(Blocks.WATER.defaultBlockState());
try (GenerationSessionLease lease = current.acquireGenerationLease("modded_base_height");
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
return heightAccessor.getMinY() + current.getHeight(x, z, ignoreFluid) + 1;
int height = current.getDimensionStackContext() == null
? current.getHeight(x, z, ignoreFluid)
: Engine.hostHeight(current, x, z, ignoreFluid);
return heightAccessor.getMinY() + height + 1;
} catch (GenerationSessionException e) {
throw new IllegalStateException("Iris base height query could not acquire its engine runtime.", e);
}
@@ -1043,8 +1094,13 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
try (GenerationSessionLease lease = current.acquireGenerationLease("modded_base_column");
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
BlockState airState = Blocks.AIR.defaultBlockState();
int surface = current.getHeight(x, z, true);
int fluid = current.getHeight(x, z, false);
boolean dimensionStack = current.getDimensionStackContext() != null;
int surface = dimensionStack
? Engine.hostHeight(current, x, z, true)
: current.getHeight(x, z, true);
int fluid = dimensionStack
? Engine.hostHeight(current, x, z, false)
: current.getHeight(x, z, false);
BlockState stone = Blocks.STONE.defaultBlockState();
BlockState water = Blocks.WATER.defaultBlockState();
int solidEnd = Math.max(0, Math.min(states.length, surface + 1));
@@ -21,6 +21,7 @@ package art.arcane.iris.modded;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBiomeWriter;
import art.arcane.iris.util.project.context.IrisContext;
@@ -97,7 +98,7 @@ public final class ModdedBiomeWriter implements PlatformBiomeWriter {
return key;
}
Engine engine = context.getEngine();
String prefix = engine.getDimension().getLoadKey() + ":";
String prefix = engine.getDimension().getCustomBiomeKeyPrefix();
if (!key.regionMatches(true, 0, prefix, 0, prefix.length())) {
return key;
}
@@ -138,13 +139,7 @@ public final class ModdedBiomeWriter implements PlatformBiomeWriter {
}
private int idForDerivative(Registry<Biome> registry, String key) {
int colon = key.indexOf(':');
if (colon <= 0 || colon >= key.length() - 1) {
return -1;
}
String dimensionLoadKey = key.substring(0, colon);
String customBiomeId = key.substring(colon + 1);
IrisBiome owner = findCustomBiomeOwner(dimensionLoadKey, customBiomeId);
IrisBiome owner = findCustomBiomeOwner(key);
if (owner == null) {
return -1;
}
@@ -155,20 +150,18 @@ public final class ModdedBiomeWriter implements PlatformBiomeWriter {
return idForKey(registry, derivativeKey);
}
private IrisBiome findCustomBiomeOwner(String dimensionLoadKey, String customBiomeId) {
private IrisBiome findCustomBiomeOwner(String key) {
for (Engine engine : ModdedWorldEngines.activeEngines()) {
if (engine == null || engine.isClosed()) {
continue;
}
if (!dimensionLoadKey.equalsIgnoreCase(engine.getDimension().getLoadKey())) {
continue;
}
for (IrisBiome biome : engine.getDimension().getAllBiomes(engine)) {
IrisDimension dimension = engine.getDimension();
for (IrisBiome biome : dimension.getAllBiomes(engine)) {
if (!biome.isCustom()) {
continue;
}
for (IrisBiomeCustom custom : biome.getCustomDerivitives()) {
if (customBiomeId.equals(custom.getId())) {
if (key.equalsIgnoreCase(dimension.getCustomBiomeKey(custom.getId()))) {
return biome;
}
}
@@ -258,7 +258,7 @@ public final class ModdedBlockBreakHandler {
if (skipsParents(providers)) {
return providers;
}
IrisRegion region = engine.getRegion(position.getX(), position.getZ());
IrisRegion region = engine.getRegion(position.getX(), relativeY, position.getZ());
if (region != null) {
addMatching(providers, region.getBlockDrops(), brokenState, data);
}
@@ -62,7 +62,6 @@ final class ModdedDimensionMetadata {
static Set<String> collectConfiguredBiomeKeys(Iterable<IrisBiome> biomes, String dimensionLoadKey) {
LinkedHashSet<String> keys = new LinkedHashSet<>();
String namespace = dimensionLoadKey.toLowerCase(Locale.ROOT);
for (IrisBiome irisBiome : biomes) {
if (irisBiome == null) {
continue;
@@ -75,7 +74,7 @@ final class ModdedDimensionMetadata {
continue;
}
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
keys.add(namespace + ":" + customBiome.getId().toLowerCase(Locale.ROOT));
keys.add(IrisDimension.customBiomeKey(dimensionLoadKey, customBiome.getId()));
}
}
return Set.copyOf(keys);
@@ -226,7 +226,7 @@ public final class ModdedEngineEffects extends EngineAssignedComponent implement
int blockY = floor(player.getY()) - getEngine().getWorld().minHeight();
int blockZ = floor(player.getZ());
state.biome(getEngine().getBiome(blockX, blockY, blockZ));
state.region(getEngine().getRegion(blockX, blockZ));
state.region(getEngine().getRegion(blockX, blockY, blockZ));
}
private void applyEffects(ServerLevel level, ServerPlayer player, Iterable<IrisEffect> effects) {
@@ -52,7 +52,6 @@ import java.nio.file.attribute.BasicFileAttributes;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HexFormat;
import java.util.LinkedHashMap;
@@ -514,18 +513,13 @@ public final class ModdedForcedDatapack {
if (!dimensionsDirectory.isDirectory()) {
return false;
}
File[] dimensionFiles = dimensionsDirectory.listFiles(
(File file) -> file.isFile() && file.getName().endsWith(".json"));
if (dimensionFiles == null) {
throw new IOException("Iris could not read dimensions for pack '" + packName + "'");
}
if (dimensionFiles.length == 0) {
IrisData data = IrisData.get(packFolder);
String[] dimensionKeys = data.getDimensionLoader().getPossibleKeys();
if (dimensionKeys == null || dimensionKeys.length == 0) {
return false;
}
Arrays.sort(dimensionFiles, Comparator.comparing(File::getName));
IrisData data = IrisData.get(packFolder);
for (File dimensionFile : dimensionFiles) {
String dimensionKey = dimensionFile.getName().substring(0, dimensionFile.getName().length() - ".json".length());
List<String> sortedDimensionKeys = Stream.of(dimensionKeys).sorted().toList();
for (String dimensionKey : sortedDimensionKeys) {
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey);
if (dimension == null) {
throw new IllegalStateException("Iris pack '" + packName + "' dimension '"
@@ -18,6 +18,8 @@
package art.arcane.iris.modded;
import art.arcane.iris.engine.DimensionStackContext;
import art.arcane.iris.engine.DimensionStackLayout;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.GenerationSessionLease;
import art.arcane.iris.engine.framework.NativeFeatureGenerationPolicy;
@@ -57,7 +59,10 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.IntBinaryOperator;
import java.util.function.Predicate;
/**
* Native placed-feature passthrough for one Iris dimension, gated on {@code importedFeatures.enabled}.
@@ -219,7 +224,14 @@ final class ModdedImportedFeatureStage {
private FeatureTable buildTable(Engine engine, IrisImportedFeatureControl control, long generation) {
// Registry-ordered biome list. FeatureSorter's cycle detection walks it, so an unordered list makes
// detection depend on JVM hash order and turns a real cycle into an intermittent one.
List<Holder<Biome>> biomes = biomeSource.orderedPossibleBiomes();
Set<String> hostBiomeKeys = hostBiomeKeys(engine);
List<Holder<Biome>> biomes = new ArrayList<>();
for (Holder<Biome> biome : biomeSource.orderedPossibleBiomes()) {
String key = holderKey(biome);
if (key != null && hostBiomeKeys.contains(key)) {
biomes.add(biome);
}
}
if (biomes.isEmpty()) {
ModdedIrisLog.error("Iris importedFeatures is on but {} exposes no biomes; features off",
dimensionKey(engine));
@@ -262,7 +274,7 @@ final class ModdedImportedFeatureStage {
*/
private Map<String, Holder<Biome>> customBiomeDerivatives(Engine engine, Map<String, Holder<Biome>> byKey) {
Map<String, Holder<Biome>> derivatives = new HashMap<>();
for (IrisBiome irisBiome : engine.getAllBiomes()) {
for (IrisBiome irisBiome : engine.getDimension().getReachableBiomes(engine)) {
if (!irisBiome.isCustom()) {
continue;
}
@@ -322,14 +334,24 @@ final class ModdedImportedFeatureStage {
List<FeatureSorter.StepFeatureData> steps = table.steps();
WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed()));
long decorationSeed = random.setDecorationSeed(level.getSeed(), origin.getX(), origin.getZ());
Set<Holder<Biome>> chunkBiomes = chunkBiomes(level, sectionPos, table);
IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData());
int staticMinY = engine.getMinHeight();
WorldGenLevel placementLevel = staticObjects.isEmpty() ? level : ModdedNativeStructureWorldgenAccess.create(
DimensionStackContext stackContext = engine.getDimensionStackContext();
Set<Holder<Biome>> chunkBiomes = chunkBiomes(level, sectionPos, table, stackContext);
IntBinaryOperator surfaceFirstFreeY = stackContext == null
? (x, z) -> level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z)
: (x, z) -> Engine.hostHeight(engine, x, z, false) + staticMinY + 1;
IntBinaryOperator floorFirstFreeY = stackContext == null
? (x, z) -> level.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, x, z)
: (x, z) -> Engine.hostHeight(engine, x, z, true) + staticMinY + 1;
WorldGenLevel placementLevel = staticObjects.isEmpty() && stackContext == null
? level
: ModdedNativeStructureWorldgenAccess.create(
level, centerPos,
(x, z) -> level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z),
(x, z) -> level.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, x, z),
position -> staticObjects.contains(position.getX(), position.getY() - staticMinY, position.getZ()));
surfaceFirstFreeY,
floorFirstFreeY,
stackContext != null,
importedFeatureProtection(staticObjects, stackContext, staticMinY));
try {
for (int stepIndex = 0; stepIndex < steps.size(); stepIndex++) {
@@ -349,6 +371,58 @@ final class ModdedImportedFeatureStage {
WorldCheckFeaturePlacement.recordPlacementPass();
}
private static Set<String> hostBiomeKeys(Engine engine) {
Set<String> keys = new LinkedHashSet<>();
for (IrisBiome irisBiome : engine.getDimension().getReachableBiomes(engine)) {
addKey(keys, irisBiome.getStructureDerivativeKey());
addKey(keys, irisBiome.getDerivativeKey());
for (String scatter : irisBiome.getBiomeScatter()) {
addKey(keys, scatter);
}
for (String scatter : irisBiome.getBiomeSkyScatter()) {
addKey(keys, scatter);
}
if (!irisBiome.isCustom()) {
continue;
}
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
addKey(keys, ModdedWorldgenIds.biomeRef(engine, customBiome.getId()));
}
}
return keys;
}
private static void addKey(Set<String> keys, String key) {
String normalized = normalizeKey(key);
if (normalized != null) {
keys.add(normalized);
}
}
private static Predicate<BlockPos> importedFeatureProtection(
IrisStaticObjectLayer staticObjects,
DimensionStackContext stackContext,
int minimumY
) {
Map<Long, DimensionStackLayout> layouts = new ConcurrentHashMap<>();
return position -> {
if (!staticObjects.isEmpty() && staticObjects.contains(
position.getX(), position.getY() - minimumY, position.getZ())) {
return true;
}
if (stackContext == null) {
return false;
}
long columnKey = ((long) position.getX() << 32)
^ (position.getZ() & 0xFFFFFFFFL);
DimensionStackLayout layout = layouts.computeIfAbsent(
columnKey,
ignored -> stackContext.sample(position.getX(), position.getZ())
);
return layout.isHostFeatureProtectedY(position.getY() - minimumY);
};
}
private void placeStep(WorldGenLevel level, FeatureTable table, FeatureSorter.StepFeatureData stepData,
Registry<PlacedFeature> featureRegistry, Set<Holder<Biome>> chunkBiomes,
IrisModdedChunkGenerator owner, WorldgenRandom random, long decorationSeed,
@@ -394,12 +468,37 @@ final class ModdedImportedFeatureStage {
* shape as vanilla, which drops any section biome its biome source does not claim. Holders are canonicalised
* back onto the table's own holders so the index mapping cannot be handed a holder it never saw.
*/
private Set<Holder<Biome>> chunkBiomes(WorldGenLevel level, SectionPos sectionPos, FeatureTable table) {
private Set<Holder<Biome>> chunkBiomes(
WorldGenLevel level,
SectionPos sectionPos,
FeatureTable table,
DimensionStackContext stackContext
) {
List<Holder<Biome>> collected = new ArrayList<>();
ChunkPos.rangeClosed(sectionPos.chunk(), 1).forEach((ChunkPos chunkPos) -> {
ChunkAccess neighbour = level.getChunk(chunkPos.x(), chunkPos.z());
for (LevelChunkSection section : neighbour.getSections()) {
section.getBiomes().getAll(collected::add);
LevelChunkSection[] sections = neighbour.getSections();
for (int sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) {
LevelChunkSection section = sections[sectionIndex];
if (stackContext == null) {
section.getBiomes().getAll(collected::add);
continue;
}
int sectionMinimumY = neighbour.getSectionYFromSectionIndex(sectionIndex) << 4;
for (int quartY = 0; quartY < 4; quartY++) {
int internalY = sectionMinimumY + (quartY << 2) - level.getMinY();
for (int quartX = 0; quartX < 4; quartX++) {
int blockX = chunkPos.getMinBlockX() + (quartX << 2);
for (int quartZ = 0; quartZ < 4; quartZ++) {
int blockZ = chunkPos.getMinBlockZ() + (quartZ << 2);
DimensionStackLayout.Layer layer = stackContext.getLayerAt(
blockX, internalY, blockZ);
if (layer.terrainContext().isSelfReferencing()) {
collected.add(section.getBiomes().get(quartX, quartY, quartZ));
}
}
}
}
}
});
Set<Holder<Biome>> present = new LinkedHashSet<>();
@@ -18,6 +18,8 @@
package art.arcane.iris.modded;
import art.arcane.iris.engine.DimensionStackContext;
import art.arcane.iris.engine.DimensionStackLayout;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
@@ -78,6 +80,7 @@ import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiConsumer;
import java.util.function.IntBinaryOperator;
import java.util.function.Predicate;
/**
* Native (vanilla registry) structure stage for {@link IrisModdedChunkGenerator}. The generator keeps the
@@ -266,7 +269,7 @@ final class ModdedNativeStructureStage {
undergroundStep,
decision.preserveSourceY(),
decision.yBand(),
(x, z) -> current.getHeight(x, z, true) + current.getMinHeight());
(x, z) -> Engine.hostHeight(current, x, z, true) + current.getMinHeight());
StructureStart wrapped = NativeStructureReferenceEnvelope.wrapForPublication(
start, structure, start.getReferences(),
NativeStructureTerrainIntegrator.resolveNativeTerrain(start, decision.terrain()),
@@ -367,10 +370,14 @@ final class ModdedNativeStructureStage {
chunkPos.x(), chunkPos.z(), error);
}
IrisStaticObjectLayer staticObjects = current.getDimension().getStaticObjectLayer(current.getData());
int staticMinY = current.getMinHeight();
WorldGenLevel boundedWorld = staticObjects.isEmpty() ? world : ModdedNativeStructureWorldgenAccess.create(
Predicate<BlockPos> protectedPosition = nativeStructureProtection(current, staticObjects);
WorldGenLevel boundedWorld = staticObjects.isEmpty()
&& current.getDimensionStackContext() == null
? world
: ModdedNativeStructureWorldgenAccess.create(
world, chunkPos, worldgenSurfaceHeight(current, world.getMinY()), worldgenFloorHeight(current, world.getMinY()),
position -> staticObjects.contains(position.getX(), position.getY() - staticMinY, position.getZ()));
current.getDimensionStackContext() != null,
protectedPosition);
try {
NativeStructureVegetationClearer.clearIntersectingVegetation(
boundedWorld, chunk, area, vegetationTargets);
@@ -383,7 +390,7 @@ final class ModdedNativeStructureStage {
try {
vacuumFoundationPlan = NativeStructureSurfaceFitter.prepareSurfaceStructures(
boundedWorld, area, terrainTargets,
(x, z) -> current.getHeight(x, z, true) + current.getMinHeight());
(x, z) -> Engine.hostHeight(current, x, z, true) + current.getMinHeight());
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"terrain integration", nativeStructureBatchContext(placementGroups),
@@ -443,18 +450,50 @@ final class ModdedNativeStructureStage {
IrisNativeStructureDecision decision) {
Engine current = generator.engine();
WorldGenLevel boundedWorld = world instanceof ModdedNativeStructureWorldgenAccess ? world : ModdedNativeStructureWorldgenAccess.create(
world, chunkPos, worldgenSurfaceHeight(current, world.getMinY()), worldgenFloorHeight(current, world.getMinY()), position -> false);
world,
chunkPos,
worldgenSurfaceHeight(current, world.getMinY()),
worldgenFloorHeight(current, world.getMinY()),
current.getDimensionStackContext() != null,
nativeStructureProtection(
current,
current.getDimension().getStaticObjectLayer(current.getData())));
world.setCurrentlyGenerating(() -> "Iris native structure " + structureId);
try {
NativeStructurePostProcessor.place(
boundedWorld, structureManager, generator, random, area, chunkPos,
structureId, start, decision, this::resolvePaletteBlock,
(x, z) -> current.getHeight(x, z, true) + current.getMinHeight());
(x, z) -> Engine.hostHeight(current, x, z, true) + current.getMinHeight());
} finally {
world.setCurrentlyGenerating(null);
}
}
private static Predicate<BlockPos> nativeStructureProtection(
Engine engine,
IrisStaticObjectLayer staticObjects
) {
DimensionStackContext stackContext = engine.getDimensionStackContext();
int minimumY = engine.getMinHeight();
Map<Long, DimensionStackLayout> layouts = new ConcurrentHashMap<>();
return position -> {
if (!staticObjects.isEmpty() && staticObjects.contains(
position.getX(), position.getY() - minimumY, position.getZ())) {
return true;
}
if (stackContext == null) {
return false;
}
long columnKey = ((long) position.getX() << 32)
^ (position.getZ() & 0xFFFFFFFFL);
DimensionStackLayout layout = layouts.computeIfAbsent(
columnKey,
ignored -> stackContext.sample(position.getX(), position.getZ())
);
return layout.isHostFeatureProtectedY(position.getY() - minimumY);
};
}
private List<List<Structure>> structuresByStep(Registry<Structure> registry) {
StructureStepCache cached = structureStepCache;
if (cached != null && cached.registry() == registry) {
@@ -523,11 +562,11 @@ final class ModdedNativeStructureStage {
}
private IntBinaryOperator worldgenSurfaceHeight(Engine generationEngine, int runtimeMinY) {
return (x, z) -> generationEngine.getHeight(x, z, false) + runtimeMinY + 1;
return (x, z) -> Engine.hostHeight(generationEngine, x, z, false) + runtimeMinY + 1;
}
private IntBinaryOperator worldgenFloorHeight(Engine generationEngine, int runtimeMinY) {
return (x, z) -> generationEngine.getHeight(x, z, true) + runtimeMinY + 1;
return (x, z) -> Engine.hostHeight(generationEngine, x, z, true) + runtimeMinY + 1;
}
private record NativeStructureStartKey(String structureId, long chunkPosition) {
@@ -63,6 +63,7 @@ final class ModdedNativeStructureWorldgenAccess implements WorldGenLevel {
private final ChunkPos generationCenter;
private final IntBinaryOperator surfaceFirstFreeY;
private final IntBinaryOperator floorFirstFreeY;
private final boolean virtualizeHeights;
private final Predicate<BlockPos> protectedPosition;
private final Holder<Biome> fallbackBiome;
private final BiomeManager biomeManager;
@@ -80,6 +81,7 @@ final class ModdedNativeStructureWorldgenAccess implements WorldGenLevel {
boundary.surfaceFirstFreeY(), "Native structure world access requires a surface resolver");
this.floorFirstFreeY = Objects.requireNonNull(
boundary.floorFirstFreeY(), "Native structure world access requires an ocean-floor resolver");
this.virtualizeHeights = boundary.virtualizeHeights();
this.protectedPosition = Objects.requireNonNull(
boundary.protectedPosition(), "World access requires a protected-position predicate");
BlockPos biomeSample = generationCenter.getMiddleBlockPosition(delegate.getSeaLevel());
@@ -102,9 +104,10 @@ final class ModdedNativeStructureWorldgenAccess implements WorldGenLevel {
static ModdedNativeStructureWorldgenAccess create(WorldGenLevel delegate, ChunkPos generationCenter,
IntBinaryOperator surfaceFirstFreeY,
IntBinaryOperator floorFirstFreeY,
boolean virtualizeHeights,
Predicate<BlockPos> protectedPosition) {
return new ModdedNativeStructureWorldgenAccess(delegate, new Boundary(
generationCenter, surfaceFirstFreeY, floorFirstFreeY, protectedPosition));
generationCenter, surfaceFirstFreeY, floorFirstFreeY, virtualizeHeights, protectedPosition));
}
@Override
@@ -241,7 +244,7 @@ final class ModdedNativeStructureWorldgenAccess implements WorldGenLevel {
@Override
public int getHeight(Heightmap.Types type, int x, int z) {
if (isInsideGenerationRegion(x >> 4, z >> 4)) {
if (!virtualizeHeights && isInsideGenerationRegion(x >> 4, z >> 4)) {
return delegate.getHeight(type, x, z);
}
return height(type, x, z);
@@ -526,6 +529,7 @@ final class ModdedNativeStructureWorldgenAccess implements WorldGenLevel {
private record Boundary(ChunkPos generationCenter,
IntBinaryOperator surfaceFirstFreeY,
IntBinaryOperator floorFirstFreeY,
boolean virtualizeHeights,
Predicate<BlockPos> protectedPosition) {
}
@@ -108,12 +108,17 @@ public final class ModdedScheduler implements PlatformScheduler {
scheduler.drain();
}
public static boolean isMainThread() {
Thread running = mainThread;
return running != null && Thread.currentThread() == running;
}
@Override
public void global(Runnable task) {
if (task == null) {
return;
}
if (onMainThread()) {
if (isMainThread()) {
runGuarded(task);
return;
}
@@ -250,11 +255,6 @@ public final class ModdedScheduler implements PlatformScheduler {
}
}
private boolean onMainThread() {
Thread main = mainThread;
return main != null && Thread.currentThread() == main;
}
private void runGuarded(Runnable task) {
try {
task.run();
@@ -18,10 +18,14 @@
package art.arcane.iris.modded;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.DimensionStackContext;
import art.arcane.iris.engine.DimensionTerrainContext;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.GenerationSessionLease;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.util.project.context.IrisContext;
import net.minecraft.core.Holder;
import net.minecraft.core.Registry;
@@ -31,32 +35,24 @@ import net.minecraft.world.entity.MobCategory;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.MobSpawnSettings;
import java.util.Locale;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
/**
* Vanilla-derivative spawn table state for {@link IrisModdedChunkGenerator}. Every mutator locks the
* generator monitor because the repoint/bind/reset paths already hold it while resetting this state.
*/
final class ModdedSpawnTableMerger {
private final IrisModdedChunkGenerator generator;
private final ConcurrentHashMap<Biome, Holder<Biome>> vanillaSpawnBiomes = new ConcurrentHashMap<>();
private final ConcurrentHashMap<SpawnTableKey, WeightedList<MobSpawnSettings.SpawnerData>> mergedSpawnTables = new ConcurrentHashMap<>();
private volatile boolean vanillaSpawnBiomesInitialized;
private volatile SpawnTableState state = new SpawnTableState();
ModdedSpawnTableMerger(IrisModdedChunkGenerator generator) {
this.generator = generator;
}
void initializeVanillaSpawnBiomes(Registry<Biome> registry) {
// Volatile fast path BEFORE the monitor: this runs per mob category per spawn attempt
// on the server thread, and the generator monitor is contended by binds/hotloads.
if (vanillaSpawnBiomesInitialized) {
SpawnTableState active = state;
if (active.initialized) {
return;
}
synchronized (generator) {
if (vanillaSpawnBiomesInitialized) {
synchronized (active) {
if (active != state || active.initialized) {
return;
}
Engine current = generator.engineOrNull();
@@ -66,29 +62,70 @@ final class ModdedSpawnTableMerger {
try (GenerationSessionLease lease = generator.requireGenerationLease(current, "modded_spawn_biomes");
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
String namespace = current.getDimension().getLoadKey().toLowerCase(Locale.ROOT);
for (IrisBiome irisBiome : current.getDimension().getReachableBiomes(current)) {
if (irisBiome == null || !irisBiome.isCustom()) {
continue;
}
Holder<Biome> vanillaHolder = resolveBiomeHolder(registry, irisBiome.getVanillaDerivativeKey());
if (vanillaHolder == null) {
continue;
}
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
Holder<Biome> customHolder = resolveBiomeHolder(registry, namespace + ":" + customBiome.getId());
if (customHolder != null) {
vanillaSpawnBiomes.putIfAbsent(customHolder.value(), vanillaHolder);
IrisDimension hostDimension = current.getDimension();
addVanillaSpawnBiomes(
registry,
hostDimension,
current.getData(),
hostDimension.getReachableBiomes(current),
active
);
DimensionStackContext stackContext = current.getDimensionStackContext();
if (stackContext != null) {
for (DimensionTerrainContext terrainContext : stackContext.getLayersBottomToTop()) {
if (terrainContext.isSelfReferencing()) {
continue;
}
IrisDimension dimension = terrainContext.getDimension();
addVanillaSpawnBiomes(
registry,
dimension,
terrainContext.getData(),
dimension.getReachableBiomes(terrainContext),
active
);
}
}
vanillaSpawnBiomesInitialized = true;
active.initialized = true;
}
}
}
private void addVanillaSpawnBiomes(
Registry<Biome> registry,
IrisDimension dimension,
IrisData data,
Iterable<IrisBiome> biomes,
SpawnTableState state
) {
for (IrisBiome irisBiome : biomes) {
if (irisBiome == null || !irisBiome.isCustom()) {
continue;
}
Holder<Biome> vanillaHolder = resolveBiomeHolder(
registry, irisBiome.getVanillaDerivativeKey());
if (vanillaHolder == null) {
continue;
}
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
Holder<Biome> customHolder = resolveBiomeHolder(
registry,
ModdedWorldgenIds.biomeRef(
data.getDataFolder().getName(),
dimension.getLoadKey(),
customBiome.getId()
)
);
if (customHolder == null) {
continue;
}
state.vanillaSpawnBiomes.putIfAbsent(customHolder.value(), vanillaHolder);
}
}
}
Holder<Biome> vanillaSpawnBiome(Biome biome) {
return vanillaSpawnBiomes.get(biome);
return state.vanillaSpawnBiomes.get(biome);
}
WeightedList<MobSpawnSettings.SpawnerData> mergedSpawnTable(
@@ -96,7 +133,8 @@ final class ModdedSpawnTableMerger {
WeightedList<MobSpawnSettings.SpawnerData> vanillaSpawns,
WeightedList<MobSpawnSettings.SpawnerData> explicitSpawns) {
SpawnTableKey key = new SpawnTableKey(biome, category);
return mergedSpawnTables.computeIfAbsent(key, ignored -> NativeSpawnTableMerger.merge(vanillaSpawns, explicitSpawns));
return state.mergedSpawnTables.computeIfAbsent(
key, ignored -> NativeSpawnTableMerger.merge(vanillaSpawns, explicitSpawns));
}
private Holder<Biome> resolveBiomeHolder(Registry<Biome> registry, String key) {
@@ -112,13 +150,16 @@ final class ModdedSpawnTableMerger {
}
void resetVanillaSpawnBiomes() {
synchronized (generator) {
vanillaSpawnBiomes.clear();
mergedSpawnTables.clear();
vanillaSpawnBiomesInitialized = false;
}
state = new SpawnTableState();
}
private record SpawnTableKey(Biome biome, MobCategory category) {
}
private static final class SpawnTableState {
private final ConcurrentHashMap<Biome, Holder<Biome>> vanillaSpawnBiomes = new ConcurrentHashMap<>();
private final ConcurrentHashMap<SpawnTableKey, WeightedList<MobSpawnSettings.SpawnerData>> mergedSpawnTables =
new ConcurrentHashMap<>();
private volatile boolean initialized;
}
}
@@ -84,7 +84,8 @@ final class ModdedEditCommands {
}
BlockPos pos = player.blockPosition();
try {
region = engine.getRegion(pos.getX(), pos.getZ());
region = engine.getRegion(
pos.getX(), pos.getY() - engine.getMinHeight(), pos.getZ());
} catch (Throwable e) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_REGION_LOOKUP_FAILED, MessageArgument.untrusted("value", e.getClass().getSimpleName())));
return 0;
@@ -73,6 +73,8 @@ import org.zeroturnaround.zip.ZipUtil;
import java.awt.Desktop;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
@@ -85,6 +87,7 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.ModdedCommandMessages;
@@ -891,21 +894,30 @@ public final class ModdedStudioCommands {
LinkedHashSet<String> objectKeys = new LinkedHashSet<>();
LinkedHashSet<String> markerKeys = new LinkedHashSet<>();
LinkedHashSet<String> structureKeys = new LinkedHashSet<>();
List<IrisDimension> dimensions = new ArrayList<>();
dimension.getAllRegions(() -> dm).forEach((IrisRegion region) -> {
if (region != null && region.getLoadKey() != null) {
regionKeys.add(region.getLoadKey());
for (String dimensionKey : PackExportClosure.collectDimensionKeys(dimension).stream().sorted().toList()) {
IrisDimension exportedDimension = dm.getDimensionLoader().load(dimensionKey);
if (exportedDimension != null) {
dimensions.add(exportedDimension);
}
});
dimension.getReachableBiomes(() -> dm).forEach((IrisBiome biome) -> {
if (biome != null && biome.getLoadKey() != null) {
biomeKeys.add(biome.getLoadKey());
}
});
lootKeys.addAll(dimension.getLoot().getTables());
spawnerKeys.addAll(dimension.getEntitySpawners());
collectStructureKeys(structureKeys, dimension.getStructures());
objectKeys.addAll(PackExportClosure.collectStaticObjectKeys(dimension.getStaticObjects()));
}
for (IrisDimension exportedDimension : dimensions) {
exportedDimension.getAllRegions(() -> dm).forEach((IrisRegion region) -> {
if (region != null && region.getLoadKey() != null) {
regionKeys.add(region.getLoadKey());
}
});
exportedDimension.getReachableBiomes(() -> dm).forEach((IrisBiome biome) -> {
if (biome != null && biome.getLoadKey() != null) {
biomeKeys.add(biome.getLoadKey());
}
});
lootKeys.addAll(exportedDimension.getLoot().getTables());
spawnerKeys.addAll(exportedDimension.getEntitySpawners());
collectStructureKeys(structureKeys, exportedDimension.getStructures());
objectKeys.addAll(PackExportClosure.collectStaticObjectKeys(exportedDimension.getStaticObjects()));
}
for (String regionKey : regionKeys) {
IrisRegion region = dm.getRegionLoader().load(regionKey);
@@ -975,10 +987,17 @@ public final class ModdedStudioCommands {
}
}
hashes.append(copyJson(folder, "dimensions", dimension.getLoadKey(), dm.getDimensionLoader().findFile(dimension.getLoadKey())));
for (IrisDimension exportedDimension : dimensions) {
String key = exportedDimension.getLoadKey();
hashes.append(copyJson(folder, "dimensions", key, dm.getDimensionLoader().findFile(key)));
}
for (String key : generatorKeys) {
hashes.append(copyJson(folder, "generators", key, dm.getGeneratorLoader().findFile(key)));
}
for (String key : Stream.of(dm.getExpressionLoader().getPossibleKeys()).sorted().toList()) {
hashes.append(copyJson(folder, "expressions", key, dm.getExpressionLoader().findFile(key)));
}
hashes.append(copyStyleSnippets(packFolder, folder));
for (String key : regionKeys) {
hashes.append(copyJson(folder, "regions", key, dm.getRegionLoader().findFile(key)));
}
@@ -1041,6 +1060,25 @@ public final class ModdedStudioCommands {
}
}
private static String copyStyleSnippets(File packFolder, File folder) throws IOException {
Path sourceRoot = new File(packFolder, "snippet/style").toPath();
if (!Files.isDirectory(sourceRoot)) {
return "";
}
StringBuilder hashes = new StringBuilder();
try (Stream<Path> files = Files.walk(sourceRoot)) {
for (Path source : files.filter(Files::isRegularFile)
.filter(path -> path.getFileName().toString().endsWith(".json"))
.sorted()
.toList()) {
String relative = sourceRoot.relativize(source).toString().replace(File.separatorChar, '/');
String key = relative.substring(0, relative.length() - ".json".length());
hashes.append(copyJson(folder, "snippet/style", key, source.toFile()));
}
}
return hashes.toString();
}
private static int regions(CommandSourceStack source, int radius) {
ServerPlayer player = source.getPlayer();
if (player == null) {
@@ -216,7 +216,8 @@ public final class ModdedWhatCommands {
int centerX = (pos.getX() & ~15) + 8;
int centerZ = (pos.getZ() & ~15) + 8;
try {
IrisRegion region = engine.getRegion(centerX, centerZ);
IrisRegion region = engine.getRegion(
centerX, pos.getY() - engine.getMinHeight(), centerZ);
IrisModdedCommands.ok(source, IrisLanguage.plain(
RuntimeUiMessages.WHAT_IRIS_REGION,
MessageArgument.untrusted("region", region.getLoadKey()),
@@ -32,6 +32,7 @@ import art.arcane.iris.engine.object.IrisWorldBoundary;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.modded.ModdedDimensionManager;
import art.arcane.iris.modded.ModdedForcedDatapack;
import art.arcane.iris.modded.ModdedScheduler;
import art.arcane.iris.modded.ModdedWorkspaceGenerator;
import art.arcane.iris.nativegen.NativeStructureVolumeIndex;
import art.arcane.iris.modded.command.ModdedPregenJob;
@@ -100,6 +101,11 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
return NativeStructureVolumeIndex.volumes(engine, minX, minZ, maxX, maxZ);
}
@Override
public boolean isMainThread() {
return ModdedScheduler.isMainThread();
}
@Override
public void refreshDatapackWorkspace(Engine engine) {
IrisData data = engine.getData();
@@ -121,6 +127,20 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
engine.getDimension(),
replacement,
"irisworldgen");
IrisModdedChunkGenerator.requireStructureBiomeUniverseCompatible(
engine.getDimension(), replacement);
}
@Override
public void prepareRuntimeHotload(Engine engine) {
IrisWorld world = engine.getWorld();
if (world == null || !world.hasPlatformWorld()
|| !(world.platformWorld().nativeHandle() instanceof ServerLevel level)
|| !(level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator generator)) {
NativeStructureVolumeIndex.invalidate(engine);
return;
}
generator.prepareRuntimeHotload(level, engine);
}
@Override
@@ -285,7 +305,6 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
long start = System.currentTimeMillis();
try {
engine.hotloadSilently();
generator.onHotload();
ModdedIrisLog.info("Iris studio hotload {} pack={} {}ms", dimensionId, engine.getDimension().getLoadKey(), System.currentTimeMillis() - start);
} catch (Throwable e) {
ModdedIrisLog.error("Iris studio hotload failed for {}", dimensionId, e);
@@ -59,6 +59,7 @@ public class IrisModdedChunkGeneratorSpawnTest {
String spawn = source.substring(spawnStart, spawnEnd);
assertTrue(spawn.contains("spawnTables.initializeVanillaSpawnBiomes(registry)"));
assertTrue(spawn.contains("structureBiomeSource.getVisibleSurfaceBiome("));
assertTrue(spawn.contains("spawnTables.vanillaSpawnBiome(visibleBiome.value())"));
assertTrue(spawn.contains("NaturalSpawner.spawnMobsForChunkGeneration("));
assertTrue(spawn.contains("new LegacyRandomSource(RandomSupport.generateUniqueSeed())"));
@@ -5,6 +5,7 @@ import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisRange;
import art.arcane.iris.engine.object.InferredType;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import net.minecraft.SharedConstants;
@@ -29,6 +30,8 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisModdedStructureParityTest {
@BeforeClass
@@ -110,6 +113,144 @@ public class IrisModdedStructureParityTest {
assertEquals(expected, IrisModdedBiomeSource.biomeResolutionSeed(worldSeed, blockX, blockY, blockZ));
}
@Test
public void stackedCustomBiomesUseTheirOwningDimensionNamespace() {
IrisDimension host = new IrisDimension();
host.setLoadKey("Host");
IrisDimension upper = new IrisDimension();
upper.setLoadKey("Upper");
assertEquals(
ModdedWorldgenIds.biomeRef("pack", "Upper", "Aurora"),
IrisModdedBiomeSource.customBiomeRef("pack", upper, "Aurora")
);
assertFalse(IrisModdedBiomeSource.customBiomeRef("pack", host, "Aurora")
.equals(IrisModdedBiomeSource.customBiomeRef("pack", upper, "Aurora")));
}
@Test
public void visibleBiomeResolutionSelectsStackOwnershipBeforeHostCaves() throws IOException {
String source = Files.readString(Path.of(
System.getProperty("iris.moddedCommonSources"),
"art/arcane/iris/modded/IrisModdedBiomeSource.java"));
int resolutionStart = source.indexOf("private BiomeResolution resolveBiomeResolution(");
int resolutionEnd = source.indexOf("private Holder<Biome> resolveRequiredStructureBiome(", resolutionStart);
assertTrue(resolutionStart >= 0);
assertTrue(resolutionEnd > resolutionStart);
String resolution = source.substring(resolutionStart, resolutionEnd);
assertTrue(resolution.contains("resolveDimensionStackLayer("));
assertTrue(resolution.contains("!stackLayer.terrainContext().isSelfReferencing()"));
assertTrue(resolution.indexOf("resolveDimensionStackLayer(")
< resolution.indexOf("boolean underground"));
assertTrue(resolution.contains("includeDimensionStack"));
assertTrue(resolution.contains(
"return resolveBiomeResolution(engine, quartX, quartY, quartZ, false)"));
assertTrue(source.contains("resolution.packName(), resolution.dimension(), customBiome.getId()"));
int helperStart = source.indexOf("private static IrisBiome resolveSurfaceStructureBiome(");
int helperEnd = source.indexOf("private static Set<String> registeredBiomeKeys(", helperStart);
String helper = source.substring(helperStart, helperEnd);
assertTrue(helper.contains("engine.getComplex().getTrueBiomeStream().get(blockX, blockZ)"));
assertFalse(helper.contains("getDimensionStackContext()"));
assertFalse(helper.contains("getSurfaceBiome("));
}
@Test
public void stackBiomesAreVisibleButExcludedFromStructureReachability() throws IOException {
String source = Files.readString(Path.of(
System.getProperty("iris.moddedCommonSources"),
"art/arcane/iris/modded/IrisModdedBiomeSource.java"));
assertTrue(source.contains("return biomeKeySets().structureRequired()"));
assertTrue(source.contains("generatedBiomeKeys.addAll(keys.visibleRequired())"));
assertTrue(source.contains("collectStructureBiomeKeys("));
assertTrue(source.contains("dimension.getReachableBiomes(() -> data)"));
}
@Test
public void biomeLocatorSearchesTheVisibleStack() throws IOException {
String source = Files.readString(Path.of(
System.getProperty("iris.moddedCommonSources"),
"art/arcane/iris/modded/IrisModdedBiomeSource.java"));
int start = source.indexOf("public Pair<BlockPos, Holder<Biome>> findClosestBiome3d(");
int end = source.indexOf("private Holder<Biome> getNoiseBiome(", start);
String method = source.substring(start, end);
assertTrue(method.contains("modded_locate_visible_biome"));
assertTrue(method.contains("return super.findClosestBiome3d("));
assertTrue(method.contains("collectVisibleBiomeHolders()"));
assertTrue(method.contains("QuartPos.toBlock(quartX), QuartPos.toBlock(quartZ)"));
assertTrue(method.contains("resolveVisibleBiome("));
assertFalse(method.contains("Holder<Biome> structureBiome = getNoiseBiome("));
assertTrue(method.indexOf("tryAcquireGenerationLease(engine, \"modded_locate_visible_biome\")")
< method.indexOf("engine.getDimensionStackContext()"));
}
@Test
public void surfaceSpawnBiomeUsesTheVisibleQuartCell() throws IOException {
String source = Files.readString(Path.of(
System.getProperty("iris.moddedCommonSources"),
"art/arcane/iris/modded/IrisModdedBiomeSource.java"));
int start = source.indexOf("Holder<Biome> getVisibleSurfaceBiome(");
int end = source.indexOf("boolean isStructureReachable(", start);
String method = source.substring(start, end);
assertTrue(method.contains("QuartPos.fromBlock(blockX)"));
assertTrue(method.contains("QuartPos.toBlock(quartX)"));
assertTrue(method.contains("QuartPos.fromBlock(internalY + engine.getMinHeight())"));
assertTrue(method.contains("stackContext.getLayout(sampleX, sampleZ).surfaceLayer()"));
assertTrue(method.contains("visibleBiomeCache"));
assertTrue(method.contains("isBiomeCacheable(engine, sampleX, sampleZ)"));
assertTrue(method.indexOf("boolean cacheable = isBiomeCacheable(engine, sampleX, sampleZ)")
< method.indexOf("Engine.hostHeight(engine, sampleX, sampleZ, true)"));
assertCacheGuarded(method, "visibleBiomeCache");
}
@Test
public void temporaryNaturalBiomeAnswersAreNotMemoized() throws IOException {
Engine engine = mock(Engine.class);
when(engine.answersFromNaturalTerrain(12, -8)).thenReturn(true);
assertFalse(IrisModdedBiomeSource.isBiomeCacheable(engine, 12, -8));
when(engine.answersFromNaturalTerrain(12, -8)).thenReturn(false);
assertTrue(IrisModdedBiomeSource.isBiomeCacheable(engine, 12, -8));
String source = Files.readString(Path.of(
System.getProperty("iris.moddedCommonSources"),
"art/arcane/iris/modded/IrisModdedBiomeSource.java"));
int structureStart = source.indexOf("private Holder<Biome> getNoiseBiome(Engine engine");
int structureEnd = source.indexOf("Holder<Biome> getVisibleNoiseBiome(", structureStart);
String structure = source.substring(structureStart, structureEnd);
assertTrue(structure.contains("QuartPos.toBlock(quartX)"));
assertTrue(structure.contains("QuartPos.toBlock(quartZ)"));
assertCacheGuarded(structure, "structureBiomeCache");
int visibleStart = source.indexOf(
"private Holder<Biome> getVisibleNoiseBiomeWithActiveGenerationLease(");
int visibleEnd = source.indexOf("Holder<Biome> getVisibleSurfaceBiome(", visibleStart);
String visible = source.substring(visibleStart, visibleEnd);
assertTrue(visible.contains("QuartPos.toBlock(quartX)"));
assertTrue(visible.contains("QuartPos.toBlock(quartZ)"));
assertCacheGuarded(visible, "visibleBiomeCache");
int predicateStart = source.indexOf("static boolean isBiomeCacheable(");
int predicateEnd = source.indexOf("static boolean isMonumentSurfaceBiomeQuery(", predicateStart);
String predicate = source.substring(predicateStart, predicateEnd);
assertTrue(predicate.contains("return !engine.answersFromNaturalTerrain(blockX, blockZ)"));
assertFalse(predicate.contains("getDimensionStackContext()"));
}
@Test
public void importedFeaturesCollectOnlyHostOwnedStackCells() throws IOException {
String source = Files.readString(Path.of(
System.getProperty("iris.moddedCommonSources"),
"art/arcane/iris/modded/ModdedImportedFeatureStage.java"));
assertTrue(source.contains("stackContext.getLayerAt("));
assertTrue(source.contains("layer.terrainContext().isSelfReferencing()"));
assertTrue(source.contains("section.getBiomes().get(quartX, quartY, quartZ)"));
}
@Test
public void configuredBiomeKeysContainOnlyPackDerivativesAndCustomBiomes() {
IrisBiome ocean = new IrisBiome()
@@ -132,6 +273,21 @@ public class IrisModdedStructureParityTest {
"minecraft:the_void", "overworld:aurora"), keys);
assertFalse(keys.contains("minecraft:desert"));
assertFalse(keys.contains("minecraft:plains"));
Set<String> recursiveKeys = ModdedDimensionMetadata.collectConfiguredBiomeKeys(
List.of(custom), "Layers/Sky");
assertTrue(recursiveKeys.contains("layers:sky/aurora"));
}
@Test
public void biomeWriterDerivativeFallbackMatchesCanonicalRecursiveKeys() throws IOException {
String source = Files.readString(Path.of(
System.getProperty("iris.moddedCommonSources"),
"art/arcane/iris/modded/ModdedBiomeWriter.java"));
assertTrue(source.contains(
"key.equalsIgnoreCase(dimension.getCustomBiomeKey(custom.getId()))"));
assertFalse(source.contains("String dimensionLoadKey = key.substring(0, colon)"));
}
@Test
@@ -159,6 +315,19 @@ public class IrisModdedStructureParityTest {
assertSame(configured, IrisModdedBiomeSource.requireConfiguredStructureBiomeKeys(configured));
}
@Test
public void liveReplacementRequiresTheSameStructureBiomeUniverse() {
IrisModdedChunkGenerator.requireStructureBiomeUniverseCompatible(
Set.of("minecraft:plains", "minecraft:beach"),
Set.of("minecraft:beach", "minecraft:plains"));
IllegalArgumentException error = assertThrows(IllegalArgumentException.class,
() -> IrisModdedChunkGenerator.requireStructureBiomeUniverseCompatible(
Set.of("minecraft:plains"), Set.of("minecraft:desert")));
assertTrue(error.getMessage().contains("Restart the server"));
}
@Test
public void configuredDimensionMetadataIsExactBeforeEngineBinding() {
IrisDimension dimension = new IrisDimension()
@@ -240,4 +409,20 @@ public class IrisModdedStructureParityTest {
assertSame(MantleFlag.INITIAL_SPAWNED_MARKER, ModdedWorldManager.INITIAL_SPAWN_COMPLETION_FLAG);
}
private static void assertCacheGuarded(String method, String cacheName) {
int predicate = method.indexOf("boolean cacheable = isBiomeCacheable(");
int cache = method.indexOf("BiomeHolderTable cache = " + cacheName, predicate);
int read = method.indexOf("cache.get(", predicate);
int readGuard = method.lastIndexOf("if (cacheable)", read);
int write = method.indexOf("cache.put(", read);
int writeGuard = method.lastIndexOf("if (cacheable)", write);
assertTrue(predicate >= 0);
assertTrue(cache >= 0);
assertTrue(read > predicate);
assertTrue(readGuard >= predicate);
assertTrue(write > read);
assertTrue(writeGuard > read);
}
}
@@ -135,6 +135,24 @@ public class ModdedNativeStructureWorldgenAccessTest {
assertEquals(1, recording.blockTicks.count());
}
@Test
public void virtualizedHeightsHideInRegionTerrainFromTheDelegate() {
RecordingDelegate recording = new RecordingDelegate();
ChunkPos generationCenter = generationCenter();
ModdedNativeStructureWorldgenAccess access = ModdedNativeStructureWorldgenAccess.create(
recording.world(), generationCenter,
(x, z) -> SURFACE_FIRST_FREE_Y,
(x, z) -> FLOOR_FIRST_FREE_Y,
true,
position -> false);
int x = generationCenter.getMiddleBlockX();
int z = generationCenter.getMiddleBlockZ();
assertEquals(SURFACE_FIRST_FREE_Y, access.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z));
assertEquals(FLOOR_FIRST_FREE_Y, access.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, x, z));
assertEquals(0, recording.heightReads);
}
@Test
public void protectedCellsRemainReadableButRejectWritesTilesAndTicks() {
RecordingDelegate recording = new RecordingDelegate();
@@ -142,7 +160,9 @@ public class ModdedNativeStructureWorldgenAccessTest {
ModdedNativeStructureWorldgenAccess access = ModdedNativeStructureWorldgenAccess.create(
recording.world(), generationCenter(),
(x, z) -> SURFACE_FIRST_FREE_Y,
(x, z) -> FLOOR_FIRST_FREE_Y, position::equals);
(x, z) -> FLOOR_FIRST_FREE_Y,
false,
position::equals);
assertSame(Blocks.DIRT, access.getBlockState(position).getBlock());
assertFalse(access.ensureCanWrite(position));
@@ -211,7 +231,9 @@ public class ModdedNativeStructureWorldgenAccessTest {
return ModdedNativeStructureWorldgenAccess.create(
recording.world(), generationCenter(),
(x, z) -> SURFACE_FIRST_FREE_Y,
(x, z) -> FLOOR_FIRST_FREE_Y, position -> false);
(x, z) -> FLOOR_FIRST_FREE_Y,
false,
position -> false);
}
private static ChunkPos generationCenter() {
@@ -95,8 +95,9 @@ public class NativeStructureFailureContractTest {
assertTrue(placement.contains("heightmapStarts.add(start);"));
assertTrue(placement.indexOf("WorldgenTerrainHeightmaps.primeStructurePlacement(")
< placement.indexOf("prepareSurfaceStructures"));
assertTrue(source.contains("generationEngine.getHeight(x, z, false) + runtimeMinY + 1"));
assertTrue(source.contains("generationEngine.getHeight(x, z, true) + runtimeMinY + 1"));
assertTrue(source.contains("Engine.hostHeight(generationEngine, x, z, false) + runtimeMinY + 1"));
assertTrue(source.contains("Engine.hostHeight(generationEngine, x, z, true) + runtimeMinY + 1"));
assertTrue(placement.contains("current.getDimensionStackContext() != null"));
assertTrue(source.contains("int minY = chunk.getMinY() + 1;"));
}
@@ -108,6 +109,8 @@ public class NativeStructureFailureContractTest {
String placement = source.substring(placementStart, placementEnd);
assertTrue(placement.contains("ModdedNativeStructureWorldgenAccess.create("));
assertTrue(placement.contains("nativeStructureProtection(current, staticObjects)"));
assertTrue(placement.contains("Engine.hostHeight(current, x, z, true)"));
assertTrue(placement.contains("placeVanillaStructure(boundedWorld, structureManager"));
assertFalse(placement.contains("NativeStructurePostProcessor.place(world,"));
assertTrue(placement.contains("world.setCurrentlyGenerating(null);"));
@@ -1,10 +1,12 @@
package art.arcane.iris.modded.service;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.modded.ModdedScheduler;
import art.arcane.volmlib.util.collection.KList;
import org.junit.Test;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -26,4 +28,21 @@ public class ModdedStudioHotloadServiceTest {
assertFalse(ModdedStudioHotloadService.hasDatapackImports(null));
assertFalse(ModdedStudioHotloadService.hasDatapackImports(List.of(empty)));
}
@Test
public void reportsTheCapturedServerThread() throws InterruptedException {
ModdedScheduler scheduler = new ModdedScheduler();
scheduler.reset();
try {
ModdedStudioHotloadService service = new ModdedStudioHotloadService();
assertTrue(service.isMainThread());
AtomicBoolean offThread = new AtomicBoolean(true);
Thread worker = new Thread(() -> offThread.set(service.isMainThread()));
worker.start();
worker.join();
assertFalse(offThread.get());
} finally {
scheduler.shutdown();
}
}
}
@@ -222,9 +222,45 @@ public final class IrisDatapackCompiler {
new LinkedHashSet<>(),
requiredDimension,
requiredFixer);
collectDimensionStackRegistryRequirements(
requirements,
requiredDimension,
requiredFixer);
return Map.copyOf(requirements);
}
private static void collectDimensionStackRegistryRequirements(
Map<String, String> requirements,
IrisDimension hostDimension,
IDataFixer fixer
) throws IOException {
if (!hostDimension.hasDimensionStack()) {
return;
}
IrisData data = hostDimension.getLoader();
if (data == null) {
throw new IOException("Dimension stack host '" + hostDimension.getLoadKey()
+ "' has no pack loader");
}
LinkedHashSet<String> visited = new LinkedHashSet<>();
visited.add(hostDimension.getLoadKey());
for (String dimensionKey : hostDimension.getDimensionStack().getDimensions()) {
if (dimensionKey == null || dimensionKey.isBlank() || !visited.add(dimensionKey)) {
continue;
}
IrisDimension sourceDimension = data.getDimensionLoader().load(dimensionKey);
if (sourceDimension == null) {
throw new IOException("Dimension stack host '" + hostDimension.getLoadKey()
+ "' references missing dimension '" + dimensionKey + "'");
}
collectRegistryRequirements(
requirements,
new LinkedHashSet<>(),
sourceDimension,
fixer);
}
}
private static void collectRegistryRequirements(
Map<String, String> requirements,
Set<String> installedBiomeKeys,
@@ -235,7 +271,6 @@ public final class IrisDatapackCompiler {
"dimension_type/iris:" + dimension.getDimensionTypeKey(),
fingerprintContent(dimension.getDimensionType().toJson(fixer)));
String namespace = dimension.getLoadKey().toLowerCase(Locale.ROOT);
ContentGate contentGate = dimension.getLoader() == null ? null : dimension.getLoader().getContentGate();
for (IrisBiome biome : dimension.getAllBiomes(dimension::getLoader)) {
if (biome == null || !biome.isCustom()) {
@@ -246,7 +281,7 @@ public final class IrisDatapackCompiler {
if (customBiome == null) {
continue;
}
String biomeKey = namespace + ":" + customBiome.getId();
String biomeKey = dimension.getCustomBiomeKey(customBiome.getId());
if (!installedBiomeKeys.add(biomeKey)) {
continue;
}
@@ -533,7 +568,7 @@ public final class IrisDatapackCompiler {
|| !Files.isDirectory(dimensions, LinkOption.NOFOLLOW_LINKS)) {
return false;
}
try (Stream<Path> stream = Files.list(dimensions)) {
try (Stream<Path> stream = Files.walk(dimensions)) {
return stream.anyMatch(path -> !Files.isSymbolicLink(path)
&& Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)
&& path.getFileName().toString().endsWith(".json"));
@@ -1121,7 +1121,7 @@ public class ServerConfigurator {
for (IrisBiome i : dimension.getAllBiomes(dimension::getLoader)) {
if (i.isCustom()) {
for (IrisBiomeCustom j : i.getCustomDerivitives()) {
keys.add(dimension.getLoadKey() + ":" + j.getId());
keys.add(dimension.getCustomBiomeKey(j.getId()));
}
}
}
@@ -134,7 +134,8 @@ public class MythicMobsDataProvider extends ExternalDataProvider implements List
if (access == null) return false;
Engine engine = access.getEngine();
if (engine == null) return false;
IrisRegion region = engine.getRegion(target.getBlockX(), target.getBlockZ());
IrisRegion region = engine.getRegion(
target.getBlockX(), target.getBlockY() - engine.getMinHeight(), target.getBlockZ());
return regions.contains(region.getLoadKey());
}
}
@@ -156,6 +156,9 @@ public interface INMSBinding {
return NativeStructureVolume.NONE;
}
default void invalidateNativeStructureVolumeIndex(Engine engine) {
}
int getBiomeId(Biome biome);
MCABiomeContainer newBiomeContainer(int min, int max, int[] data);
@@ -26,7 +26,9 @@ import art.arcane.volmlib.util.json.JSONObject;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
final class PackDimensionValidator {
private PackDimensionValidator() {
@@ -35,9 +37,14 @@ final class PackDimensionValidator {
static void validateDimensions(File packFolder, File[] dimensionFiles, List<String> blockingErrors, List<String> warnings) {
File regionsFolder = new File(packFolder, "regions");
File biomesFolder = new File(packFolder, "biomes");
File dimensionsFolder = new File(packFolder, PackValidator.DIMENSIONS_FOLDER);
Set<String> dimensionKeys = new LinkedHashSet<>();
for (File dimensionFile : dimensionFiles) {
dimensionKeys.add(PackValidationIo.deriveKey(dimensionsFolder, dimensionFile));
}
for (File dimFile : dimensionFiles) {
String dimensionKey = PackValidationIo.stripExtension(dimFile.getName());
String dimensionKey = PackValidationIo.deriveKey(dimensionsFolder, dimFile);
JSONObject dimJson;
try {
dimJson = new JSONObject(Files.readString(dimFile.toPath(), StandardCharsets.UTF_8));
@@ -50,6 +57,7 @@ final class PackDimensionValidator {
validateDimensionHeights(packFolder, dimensionKey, dimJson, blockingErrors);
validateWorldBoundary(dimensionKey, dimJson, blockingErrors);
validateStaticObjects(packFolder, dimensionKey, dimJson, blockingErrors);
validateDimensionStack(packFolder, dimensionKey, dimJson, dimensionKeys, blockingErrors);
JSONArray regionsArray = dimJson.optJSONArray("regions");
if (regionsArray == null || regionsArray.length() == 0) {
@@ -94,6 +102,108 @@ final class PackDimensionValidator {
}
}
static void validateDimensionStack(File packFolder, String dimensionKey, JSONObject dimension, Set<String> dimensionKeys,
List<String> blockingErrors) {
if (!dimension.has("dimensionStack")) {
return;
}
String context = "Dimension '" + dimensionKey + "' dimensionStack";
JSONObject stack = dimension.optJSONObject("dimensionStack");
if (stack == null) {
blockingErrors.add(context + " must be an object.");
return;
}
String upperDimension = dimension.optString("upperDimension", "none");
if (upperDimension != null && !upperDimension.isEmpty()
&& !upperDimension.equalsIgnoreCase("none")) {
blockingErrors.add(context + " cannot be used with upperDimension.");
}
JSONArray dimensions = stack.optJSONArray("dimensions");
if (dimensions == null) {
blockingErrors.add(context + ".dimensions must be an array.");
} else {
int hostOccurrences = 0;
if (dimensions.length() < 2) {
blockingErrors.add(context + ".dimensions must contain at least two dimension keys.");
}
for (int index = 0; index < dimensions.length(); index++) {
Object rawKey = dimensions.opt(index);
if (!(rawKey instanceof String key) || key.isBlank()) {
blockingErrors.add(context + ".dimensions[" + index + "] must be a nonblank dimension key.");
continue;
}
if (!dimensionKeys.contains(key)) {
blockingErrors.add(context + ".dimensions[" + index + "] references missing dimension '"
+ key + "'.");
}
if (dimensionKey.equals(key)) {
hostOccurrences++;
}
}
if (hostOccurrences != 1) {
blockingErrors.add(context + ".dimensions must contain its host dimension key '"
+ dimensionKey + "' exactly once.");
}
if (dimensions.length() > 0) {
Object rawLast = dimensions.opt(dimensions.length() - 1);
if (rawLast instanceof String lastKey && !dimensionKey.equals(lastKey)) {
blockingErrors.add(context + ".dimensions must end with its host dimension key '"
+ dimensionKey + "'.");
}
}
}
validateInteger(stack, "spacer", 0, 256, context, blockingErrors);
validateDimensionStackBlend(packFolder, context, stack, blockingErrors);
}
private static void validateDimensionStackBlend(File packFolder, String context, JSONObject stack,
List<String> blockingErrors) {
if (!stack.has("blend")) {
return;
}
JSONObject blend = stack.optJSONObject("blend");
if (blend == null) {
blockingErrors.add(context + ".blend must be an object.");
return;
}
if (blend.has("amplitude")) {
validateInteger(blend, "amplitude", 0, 256, context + ".blend", blockingErrors);
}
if (blend.has("style")) {
Object rawStyle = blend.opt("style");
boolean styleObject = rawStyle instanceof JSONObject;
boolean styleSnippet = rawStyle instanceof String reference
&& reference.startsWith("snippet/style/");
if (!styleObject && !styleSnippet) {
blockingErrors.add(context + ".blend.style must be an object or style snippet reference.");
return;
}
if (styleSnippet) {
validateDimensionStackStyleSnippet(
packFolder, context + ".blend.style", (String) rawStyle, blockingErrors);
}
}
}
private static void validateDimensionStackStyleSnippet(File packFolder, String context, String reference,
List<String> blockingErrors) {
try {
File snippetRoot = new File(packFolder, "snippet/style").getCanonicalFile();
File snippetFile = new File(packFolder, reference + ".json").getCanonicalFile();
if (!snippetFile.toPath().startsWith(snippetRoot.toPath()) || !snippetFile.isFile()) {
blockingErrors.add(context + " references missing style snippet '" + reference + "'.");
return;
}
new JSONObject(Files.readString(snippetFile.toPath(), StandardCharsets.UTF_8));
} catch (Throwable error) {
blockingErrors.add(context + " references invalid style snippet '" + reference + "': "
+ error.getMessage());
}
}
static void validateStaticObjects(File packFolder, String dimensionKey, JSONObject dimension,
List<String> blockingErrors) {
if (!dimension.has("staticObjects")) {
@@ -18,11 +18,16 @@
package art.arcane.iris.core.pack;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisDimensionStack;
import art.arcane.iris.engine.object.IrisObjectMarker;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisStaticObject;
import art.arcane.volmlib.util.collection.KSet;
import java.util.ArrayDeque;
import java.util.function.Function;
/**
* Shared key collection for the pack packagers. Both the Bukkit re-serializing compiler and the
* modded verbatim-copy compiler must export the complete ambient-spawning graph: object placements
@@ -33,6 +38,48 @@ public final class PackExportClosure {
private PackExportClosure() {
}
public static KSet<String> collectDimensionKeys(IrisDimension dimension) {
return collectDimensionKeys(dimension, key -> dimension == null || dimension.getLoader() == null
? null
: dimension.getLoader().getDimensionLoader().load(key));
}
static KSet<String> collectDimensionKeys(
IrisDimension dimension,
Function<String, IrisDimension> resolver
) {
KSet<String> dimensionKeys = new KSet<>();
if (dimension == null || dimension.getLoadKey() == null || dimension.getLoadKey().isBlank()) {
return dimensionKeys;
}
ArrayDeque<IrisDimension> pending = new ArrayDeque<>();
pending.add(dimension);
while (!pending.isEmpty()) {
IrisDimension current = pending.removeFirst();
String currentKey = current.getLoadKey();
if (currentKey == null || currentKey.isBlank() || !dimensionKeys.add(currentKey)) {
continue;
}
IrisDimensionStack stack = current.getDimensionStack();
if (stack == null || stack.getDimensions() == null) {
continue;
}
for (String key : stack.getDimensions()) {
if (key == null || key.isBlank() || dimensionKeys.contains(key)) {
continue;
}
IrisDimension referenced = resolver.apply(key);
if (referenced == null) {
dimensionKeys.add(key);
} else {
pending.addLast(referenced);
}
}
}
return dimensionKeys;
}
public static KSet<String> collectMarkerKeys(Iterable<IrisObjectPlacement> placements) {
KSet<String> markerKeys = new KSet<>();
if (placements == null) {
@@ -80,12 +80,13 @@ final class PackHydrologyValidator {
List<File> sortedDimensions = new ArrayList<>(List.of(dimensionFiles));
sortedDimensions.sort(Comparator.comparing(File::getPath));
File dimensionsFolder = new File(packFolder, PackValidator.DIMENSIONS_FOLDER);
for (File dimensionFile : sortedDimensions) {
JSONObject dimension = PackValidationIo.readJson(dimensionFile);
if (dimension == null) {
continue;
}
String dimensionKey = PackValidationIo.stripExtension(dimensionFile.getName());
String dimensionKey = PackValidationIo.deriveKey(dimensionsFolder, dimensionFile);
String dimensionPath = "Dimension '" + dimensionKey + "'";
JSONObject dimensionPolicy = validatePolicy(
dimensionPath,
@@ -7,6 +7,7 @@ import art.arcane.iris.engine.image.IrisImageMapValidationException;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeGeneratorLink;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisDimensionStack;
import art.arcane.iris.engine.object.IrisExpression;
import art.arcane.iris.engine.object.IrisGenerator;
import art.arcane.iris.engine.object.IrisImage;
@@ -217,11 +218,12 @@ final class PackImageMapValidator {
Set<String> warnings
) {
File[] sortedFiles = Arrays.copyOf(dimensionFiles, dimensionFiles.length);
Arrays.sort(sortedFiles, (File first, File second) -> first.getName().compareTo(second.getName()));
Arrays.sort(sortedFiles, (File first, File second) -> first.getPath().compareTo(second.getPath()));
Map<String, IrisDimension> dimensions = new LinkedHashMap<>();
Set<String> bindingReadyDimensions = new LinkedHashSet<>();
File dimensionsFolder = new File(packFolder, PackValidator.DIMENSIONS_FOLDER);
for (File dimensionFile : sortedFiles) {
String dimensionKey = PackValidationIo.stripExtension(dimensionFile.getName());
String dimensionKey = PackValidationIo.deriveKey(dimensionsFolder, dimensionFile);
JSONObject raw = PackValidationIo.readJson(dimensionFile);
if (raw == null) {
continue;
@@ -247,19 +249,35 @@ final class PackImageMapValidator {
validateGeneratorStyleReferences(dimensionKey, dimension, data, resources, blockingErrors);
}
Map<String, List<Map.Entry<String, IrisDimension>>> upperParents = new TreeMap<>();
Map<String, List<Map.Entry<String, IrisDimension>>> stackParents = new TreeMap<>();
for (Map.Entry<String, IrisDimension> entry : dimensions.entrySet()) {
String upperKey = normalized(entry.getValue().getUpperDimension());
if (upperKey == null || upperKey.equalsIgnoreCase("none")) {
if (upperKey != null && !upperKey.equalsIgnoreCase("none")) {
upperParents.computeIfAbsent(upperKey, ignored -> new ArrayList<>()).add(entry);
}
IrisDimensionStack stack = entry.getValue().getDimensionStack();
if (stack == null || stack.getDimensions() == null) {
continue;
}
upperParents.computeIfAbsent(upperKey, ignored -> new ArrayList<>()).add(entry);
Set<String> stackKeys = new LinkedHashSet<>();
for (String stackKey : stack.getDimensions()) {
String normalizedKey = normalized(stackKey);
if (normalizedKey == null || normalizedKey.equals(entry.getKey())
|| !stackKeys.add(normalizedKey)) {
continue;
}
stackParents.computeIfAbsent(normalizedKey, ignored -> new ArrayList<>()).add(entry);
}
}
for (Map.Entry<String, IrisDimension> entry : dimensions.entrySet()) {
if (!bindingReadyDimensions.contains(entry.getKey())) {
continue;
}
List<Map.Entry<String, IrisDimension>> parents = upperParents.get(entry.getKey());
if (parents == null || parents.isEmpty()) {
List<Map.Entry<String, IrisDimension>> stackHosts = stackParents.get(entry.getKey());
boolean hasUpperParents = parents != null && !parents.isEmpty();
boolean hasStackHosts = stackHosts != null && !stackHosts.isEmpty();
if (!hasUpperParents && !hasStackHosts) {
validateBindings(packFolder, "Dimension '" + entry.getKey() + "'", entry.getValue(),
entry.getValue().getWorldBoundary(), data, resources, registries, blockingErrors, warnings);
continue;
@@ -268,12 +286,23 @@ final class PackImageMapValidator {
validateBindings(packFolder, "Dimension '" + entry.getKey() + "'", entry.getValue(),
entry.getValue().getWorldBoundary(), data, resources, registries, blockingErrors, warnings);
}
parents.sort(Map.Entry.comparingByKey());
for (Map.Entry<String, IrisDimension> parent : parents) {
validateBindings(packFolder,
"Dimension '" + parent.getKey() + "' upper dimension '" + entry.getKey() + "'",
entry.getValue(), parent.getValue().getWorldBoundary(), data, resources, registries,
blockingErrors, warnings);
if (hasUpperParents) {
parents.sort(Map.Entry.comparingByKey());
for (Map.Entry<String, IrisDimension> parent : parents) {
validateBindings(packFolder,
"Dimension '" + parent.getKey() + "' upper dimension '" + entry.getKey() + "'",
entry.getValue(), parent.getValue().getWorldBoundary(), data, resources, registries,
blockingErrors, warnings);
}
}
if (hasStackHosts) {
stackHosts.sort(Map.Entry.comparingByKey());
for (Map.Entry<String, IrisDimension> host : stackHosts) {
validateBindings(packFolder,
"Dimension '" + host.getKey() + "' dimension stack layer '" + entry.getKey() + "'",
entry.getValue(), host.getValue().getWorldBoundary(), data, resources, registries,
blockingErrors, warnings);
}
}
}
}
@@ -111,11 +111,13 @@ public final class PackValidator {
return new PackValidationResult(packName, blockingErrors, warnings, validatedAt);
}
File[] dimensionFiles = dimensionsFolder.listFiles(f -> f.isFile() && f.getName().endsWith(".json"));
if (dimensionFiles == null || dimensionFiles.length == 0) {
List<File> discoveredDimensions = PackValidationIo.listJsonRecursive(dimensionsFolder);
if (discoveredDimensions.isEmpty()) {
blockingErrors.add("No dimension JSON files under dimensions/.");
return new PackValidationResult(packName, blockingErrors, warnings, validatedAt);
}
discoveredDimensions.sort((first, second) -> first.getPath().compareTo(second.getPath()));
File[] dimensionFiles = discoveredDimensions.toArray(File[]::new);
PackDimensionValidator.validateDimensions(packFolder, dimensionFiles, blockingErrors, warnings);
PackImageMapValidator.Validation imageMaps = PackImageMapValidator.validate(
@@ -195,7 +197,7 @@ public final class PackValidator {
if (!data.getContentGate().ready()) {
return List.of();
}
boolean dimensionExcluded = loadEverything(data, dimensionFiles);
boolean dimensionExcluded = loadEverything(packFolder, data, dimensionFiles);
List<CompatFinding> findings = data.getCompatReport().findings();
applyCompatFindings(minecraftVersion, dimensionExcluded, findings, blockingErrors, warnings);
return findings;
@@ -219,11 +221,12 @@ public final class PackValidator {
*
* @return true when any dimension in the pack cannot generate on this server
*/
private static boolean loadEverything(IrisData data, File[] dimensionFiles) {
private static boolean loadEverything(File packFolder, IrisData data, File[] dimensionFiles) {
boolean dimensionExcluded = false;
File dimensionsFolder = new File(packFolder, DIMENSIONS_FOLDER);
for (File dimensionFile : dimensionFiles) {
IrisDimension dimension = data.getDimensionLoader().load(
PackValidationIo.stripExtension(dimensionFile.getName()), false);
PackValidationIo.deriveKey(dimensionsFolder, dimensionFile), false);
if (dimension == null) {
continue;
}
@@ -30,6 +30,7 @@ import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBlockData;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisEntity;
import art.arcane.iris.engine.object.IrisExpression;
import art.arcane.iris.engine.object.IrisGenerator;
import art.arcane.iris.engine.object.IrisLootTable;
import art.arcane.iris.core.pack.PackExportClosure;
@@ -106,11 +107,19 @@ public class IrisPackageCompiler {
throw new IllegalStateException("Failed to create structure package staging folder " + folder.getAbsolutePath());
}
IrisLogging.info("Packaging Dimension " + dimension.getName() + " " + (obfuscate ? "(Obfuscated)" : ""));
KList<IrisDimension> dimensions = new KList<>();
for (String dimensionKey : PackExportClosure.collectDimensionKeys(dimension).stream().sorted().toList()) {
IrisDimension exportedDimension = dm.getDimensionLoader().load(dimensionKey);
if (exportedDimension != null) {
dimensions.add(exportedDimension);
}
}
KSet<IrisRegion> regions = new KSet<>();
KSet<IrisBiome> biomes = new KSet<>();
KSet<IrisEntity> entities = new KSet<>();
KSet<IrisSpawner> spawners = new KSet<>();
KSet<IrisGenerator> generators = new KSet<>();
KSet<IrisExpression> expressions = new KSet<>();
KSet<IrisLootTable> loot = new KSet<>();
KSet<IrisBlockData> blocks = new KSet<>();
@@ -119,22 +128,30 @@ public class IrisPackageCompiler {
for (String i : dm.getBlockLoader().getPossibleKeys()) {
addIfPresent(blocks, dm.getBlockLoader().load(i));
}
for (String key : dm.getExpressionLoader().getPossibleKeys()) {
addIfPresent(expressions, dm.getExpressionLoader().load(key));
}
dimension.getAllRegions(() -> dm).forEach((region) -> addIfPresent(regions, region));
dimension.getLoot().getTables().forEach((i) -> addIfPresent(loot, dm.getLootLoader().load(i)));
for (IrisDimension exportedDimension : dimensions) {
exportedDimension.getAllRegions(() -> dm).forEach((region) -> addIfPresent(regions, region));
exportedDimension.getLoot().getTables().forEach((i) -> addIfPresent(loot, dm.getLootLoader().load(i)));
exportedDimension.getReachableBiomes(() -> dm).forEach((biome) -> addIfPresent(biomes, biome));
exportedDimension.getEntitySpawners().forEach((sp) ->
addIfPresent(spawners, dm.getSpawnerLoader().load(sp)));
}
regions.forEach((i) -> biomes.addAll(i.getAllBiomes(() -> dm)));
dimension.getReachableBiomes(() -> dm).forEach((biome) -> addIfPresent(biomes, biome));
regions.forEach((r) -> r.getLoot().getTables().forEach((i) -> addIfPresent(loot, dm.getLootLoader().load(i))));
regions.forEach((r) -> r.getEntitySpawners().forEach((sp) -> addIfPresent(spawners, dm.getSpawnerLoader().load(sp))));
dimension.getEntitySpawners().forEach((sp) -> addIfPresent(spawners, dm.getSpawnerLoader().load(sp)));
biomes.forEach((i) -> i.getGenerators().forEach((j) -> addIfPresent(generators, j.getCachedGenerator(() -> dm))));
biomes.forEach((r) -> r.getLoot().getTables().forEach((i) -> addIfPresent(loot, dm.getLootLoader().load(i))));
biomes.forEach((r) -> r.getEntitySpawners().forEach((sp) -> addIfPresent(spawners, dm.getSpawnerLoader().load(sp))));
KList<IrisObjectPlacement> allPlacements = new KList<>();
regions.forEach((r) -> allPlacements.addAll(r.getObjects()));
biomes.forEach((i) -> allPlacements.addAll(i.getObjects()));
for (IrisStaticObject placement : dimension.getStaticObjects()) {
allPlacements.add(placement.toPlacement());
for (IrisDimension exportedDimension : dimensions) {
for (IrisStaticObject placement : exportedDimension.getStaticObjects()) {
allPlacements.add(placement.toPlacement());
}
}
KSet<IrisMarker> markers = new KSet<>();
for (String markerKey : PackExportClosure.collectMarkerKeys(allPlacements)) {
@@ -148,7 +165,9 @@ public class IrisPackageCompiler {
collectSpawnerEntityKeys(spawners).forEach((i) -> addIfPresent(entities, dm.getEntityLoader().load(i)));
entities.forEach((e) -> e.getLoot().getTables().forEach((i) -> addIfPresent(loot, dm.getLootLoader().load(i))));
Set<String> structureKeys = new LinkedHashSet<>();
collectStructureKeys(structureKeys, dimension.getStructures());
for (IrisDimension exportedDimension : dimensions) {
collectStructureKeys(structureKeys, exportedDimension.getStructures());
}
regions.forEach((region) -> collectStructureKeys(structureKeys, region.getStructures()));
biomes.forEach((biome) -> collectStructureKeys(structureKeys, biome.getStructures()));
KMap<String, String> renameObjects = new KMap<>();
@@ -176,8 +195,10 @@ public class IrisPackageCompiler {
j.setPlace(newNames);
}
for (IrisStaticObject placement : dimension.getStaticObjects()) {
placement.setObject(renameObjects.get(placement.getObject()));
for (IrisDimension exportedDimension : dimensions) {
for (IrisStaticObject placement : exportedDimension.getStaticObjects()) {
placement.setObject(renameObjects.get(placement.getObject()));
}
}
KMap<String, KList<String>> lookupObjects = renameObjects.flip();
@@ -230,9 +251,12 @@ public class IrisPackageCompiler {
}
b.append(structureClosure.writeTo(folder, minify));
b.append(ImageMapPackageClosure.writeAll(dm, folder, minify));
a = new JSONObject(new Gson().toJson(dimension)).toString(minify ? 0 : 4);
IO.writeAll(new File(folder, "dimensions/" + dimension.getLoadKey() + ".json"), a);
b.append(IO.hash(a));
for (IrisDimension exportedDimension : dimensions.stream()
.sorted(Comparator.comparing(IrisDimension::getLoadKey)).toList()) {
a = new JSONObject(new Gson().toJson(exportedDimension)).toString(minify ? 0 : 4);
IO.writeAll(new File(folder, "dimensions/" + exportedDimension.getLoadKey() + ".json"), a);
b.append(IO.hash(a));
}
for (IrisGenerator i : generators) {
a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4);
@@ -240,6 +264,13 @@ public class IrisPackageCompiler {
b.append(IO.hash(a));
}
for (IrisExpression i : expressions.stream()
.sorted(Comparator.comparing(IrisExpression::getLoadKey)).toList()) {
a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4);
IO.writeAll(new File(folder, "expressions/" + i.getLoadKey() + ".json"), a);
b.append(IO.hash(a));
}
c.append(IO.hash(b.toString()));
b = new StringBuilder();
@@ -21,6 +21,7 @@ package art.arcane.iris.core.project;
import art.arcane.iris.core.pack.PackDirectoryResolver;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.io.IO;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import java.io.File;
@@ -179,19 +180,58 @@ public final class IrisProjectCopier {
}
private static void transformDimension(Path stage, String sourceKey, String targetKey) throws IOException {
Path oldDimension = stage.resolve("dimensions").resolve(sourceKey + ".json");
Path newDimension = stage.resolve("dimensions").resolve(targetKey + ".json");
Path dimensionsRoot = stage.resolve("dimensions");
Path oldDimension = dimensionsRoot.resolve(sourceKey + ".json");
Path newDimension = dimensionsRoot.resolve(targetKey + ".json");
if (!oldDimension.equals(newDimension)) {
Files.move(oldDimension, newDimension);
}
JSONObject json = new JSONObject(IO.readAll(newDimension.toFile()));
if (json.has("name")) {
json.put("name", Form.capitalizeWords(targetKey.replace('-', ' ')));
IO.writeAll(newDimension.toFile(), json.toString(4));
try (Stream<Path> files = Files.walk(dimensionsRoot)) {
for (Path dimensionFile : files.filter(path -> Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS))
.filter(path -> path.getFileName().toString().endsWith(".json"))
.sorted()
.toList()) {
JSONObject json = new JSONObject(IO.readAll(dimensionFile.toFile()));
boolean changed = false;
if (dimensionFile.equals(newDimension) && json.has("name")) {
json.put("name", Form.capitalizeWords(targetKey.replace('-', ' ')));
changed = true;
}
if (rewriteDimensionStackReferences(json, sourceKey, targetKey)) {
changed = true;
}
if (changed) {
IO.writeAll(dimensionFile.toFile(), json.toString(4));
}
}
}
}
private static boolean rewriteDimensionStackReferences(JSONObject json, String sourceKey, String targetKey) {
JSONObject stack = json.optJSONObject("dimensionStack");
JSONArray dimensions = stack == null ? null : stack.optJSONArray("dimensions");
if (dimensions == null) {
return false;
}
boolean changed = false;
JSONArray rewritten = new JSONArray();
for (int index = 0; index < dimensions.length(); index++) {
Object value = dimensions.opt(index);
if (sourceKey.equals(value)) {
rewritten.put(targetKey);
changed = true;
} else {
rewritten.put(value == null ? JSONObject.NULL : value);
}
}
if (changed) {
stack.put("dimensions", rewritten);
}
return changed;
}
private static void publish(Path stage, Path target) throws IOException {
try {
Files.move(stage, target, StandardCopyOption.ATOMIC_MOVE);
@@ -515,7 +515,7 @@ public class BoardSVC implements IrisService, BoardProvider {
}
lines.add("&7&m ");
lines.add(IrisLanguage.text(BukkitUiMessages.SCOREBOARD_REGION, MessageArgument.untrusted("region", engine.getRegion(x, z).getName())));
lines.add(IrisLanguage.text(BukkitUiMessages.SCOREBOARD_REGION, MessageArgument.untrusted("region", engine.getRegion(x, y, z).getName())));
lines.add(IrisLanguage.text(BukkitUiMessages.SCOREBOARD_BIOME, MessageArgument.untrusted("biome", engine.getBiomeOrMantle(x, y, z).getName())));
lines.add(IrisLanguage.text(BukkitUiMessages.SCOREBOARD_HEIGHT, MessageArgument.trusted("height", Math.round(engine.getHeight(x, z)))));
lines.add(IrisLanguage.text(BukkitUiMessages.SCOREBOARD_SLOPE, MessageArgument.trusted("slope", Form.f(engine.getSlope(x, z), 2))));
@@ -326,7 +326,10 @@ public class TreeSVC implements IrisService {
// Add more or find any in the region
if (isUseAll || placements.isEmpty()) {
IrisRegion region = worldAccess.getEngine().getRegion(location.getBlockX(), location.getBlockZ());
IrisRegion region = worldAccess.getEngine().getRegion(
location.getBlockX(),
location.getBlockY() - worldAccess.getTarget().getWorld().minHeight(),
location.getBlockZ());
placements.addAll(matchObjectPlacements(region.getObjects(), size, type, worldAccess.getData()));
}
@@ -0,0 +1,363 @@
package art.arcane.iris.engine;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisDimensionStack;
import art.arcane.iris.engine.object.IrisDimensionStackBlend;
import art.arcane.iris.engine.object.IrisGeneratorStyle;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Supplier;
public final class DimensionStackContext {
private static final int LAYOUT_CACHE_SIZE = 65536;
private static final long SEAM_SALT = 0x44494D535441434BL;
private static final long HASH_OFFSET = 0xCBF29CE484222325L;
private static final long HASH_PRIME = 0x100000001B3L;
private final Engine engine;
private final int outputHeight;
private final int spacer;
private final List<DimensionTerrainContext> layersTopToBottom;
private final List<DimensionTerrainContext> layersBottomToTop;
private final List<SeamOffsetSampler> seamOffsetSamplersBottomToTop;
private final Cache<Long, DimensionStackLayout> layoutCache;
private DimensionStackContext(ContextState state) {
engine = state.engine();
outputHeight = state.outputHeight();
spacer = state.spacer();
layersBottomToTop = List.copyOf(state.layersBottomToTop());
ArrayList<DimensionTerrainContext> reversed = new ArrayList<>(layersBottomToTop);
Collections.reverse(reversed);
layersTopToBottom = List.copyOf(reversed);
seamOffsetSamplersBottomToTop = List.copyOf(state.seamOffsetSamplersBottomToTop());
layoutCache = Caffeine.newBuilder().maximumSize(LAYOUT_CACHE_SIZE).build();
}
public static DimensionStackContext create(Engine engine, IrisDimensionStack stack) {
if (stack == null) {
throw new IllegalArgumentException("Dimension stack configuration is required");
}
KList<String> dimensionKeys = stack.getDimensions();
validateRuntimeContract(engine.getDimension(), dimensionKeys);
if (stack.getSpacer() < 0 || stack.getSpacer() > 256) {
throw new IllegalArgumentException("Dimension stack spacer must be between 0 and 256");
}
ArrayList<DimensionTerrainContext> layersBottomToTop = new ArrayList<>(dimensionKeys.size());
for (int keyIndex = dimensionKeys.size() - 1; keyIndex >= 0; keyIndex--) {
String dimensionKey = dimensionKeys.get(keyIndex);
IrisDimension dimension = dimensionKey.equals(engine.getDimension().getLoadKey())
? engine.getDimension()
: engine.getData().getDimensionLoader().load(dimensionKey);
if (dimension == null) {
throw new IllegalArgumentException(
"Dimension stack reference does not resolve in this pack: " + dimensionKey);
}
layersBottomToTop.add(DimensionTerrainContext.forStack(engine, dimension));
}
List<SeamOffsetSampler> seamSamplers = createSeamSamplers(
engine,
stack.getBlend(),
layersBottomToTop
);
return new DimensionStackContext(new ContextState(
engine,
engine.getHeight(),
stack.getSpacer(),
layersBottomToTop,
seamSamplers
));
}
private static void validateRuntimeContract(IrisDimension host, KList<String> dimensionKeys) {
if (dimensionKeys == null || dimensionKeys.size() < 2) {
throw new IllegalArgumentException("Dimension stack requires at least two dimensions");
}
String hostKey = host.getLoadKey();
if (!hostKey.equals(dimensionKeys.get(dimensionKeys.size() - 1))) {
throw new IllegalArgumentException("Dimension stack host must be its final bottom dimension");
}
int hostReferences = 0;
for (String dimensionKey : dimensionKeys) {
if (dimensionKey == null || dimensionKey.isBlank()) {
throw new IllegalArgumentException("Dimension stack keys must be nonblank");
}
if (hostKey.equals(dimensionKey)) {
hostReferences++;
}
}
if (hostReferences != 1) {
throw new IllegalArgumentException("Dimension stack must contain its host exactly once");
}
}
private static List<SeamOffsetSampler> createSeamSamplers(
Engine engine,
IrisDimensionStackBlend blend,
List<DimensionTerrainContext> layersBottomToTop
) {
int seamCount = layersBottomToTop.size() - 1;
ArrayList<SeamOffsetSampler> samplers = new ArrayList<>(seamCount);
if (blend == null || blend.getAmplitude() == 0) {
for (int seamIndex = 0; seamIndex < seamCount; seamIndex++) {
samplers.add((x, z) -> 0);
}
return samplers;
}
if (blend.getAmplitude() < 0 || blend.getAmplitude() > 256) {
throw new IllegalArgumentException("Dimension stack blend amplitude must be between 0 and 256");
}
IrisGeneratorStyle style = blend.getStyle();
if (style == null) {
throw new IllegalArgumentException("Dimension stack blend style is required when amplitude is positive");
}
for (int seamIndex = 0; seamIndex < seamCount; seamIndex++) {
DimensionTerrainContext lower = layersBottomToTop.get(seamIndex);
DimensionTerrainContext upper = layersBottomToTop.get(seamIndex + 1);
long salt = seamSalt(
seamIndex,
lower.getDimension().getLoadKey(),
upper.getDimension().getLoadKey()
);
RNG seamRng = new RNG(engine.getSeedManager().getTerrain() ^ salt);
CNG seamNoise = style.create(seamRng, engine.getData(), engine);
int amplitude = blend.getAmplitude();
samplers.add((x, z) -> clampSeamOffset(
seamNoise.fitDouble(-amplitude, amplitude, x, z), amplitude));
}
return samplers;
}
static int clampSeamOffset(double sampledOffset, int amplitude) {
if (!Double.isFinite(sampledOffset)) {
return 0;
}
double clamped = Math.max(-amplitude, Math.min(amplitude, sampledOffset));
return (int) Math.round(clamped);
}
static long seamSalt(int seamIndex, String lowerDimensionKey, String upperDimensionKey) {
long hash = HASH_OFFSET ^ SEAM_SALT;
hash = hashString(hash, lowerDimensionKey);
hash ^= seamIndex;
hash *= HASH_PRIME;
return hashString(hash, upperDimensionKey);
}
private static long hashString(long hash, String value) {
long result = hash;
for (int characterIndex = 0; characterIndex < value.length(); characterIndex++) {
result ^= value.charAt(characterIndex);
result *= HASH_PRIME;
}
return result;
}
public DimensionStackLayout sample(int x, int z) {
return sample(x, z, usesTemporaryNaturalFallback(x, z));
}
private DimensionStackLayout sample(int x, int z, boolean naturalFallback) {
ArrayList<DimensionStackLayout.LayerInput> layerInputs =
new ArrayList<>(layersBottomToTop.size());
for (DimensionTerrainContext terrainContext : layersBottomToTop) {
DimensionTerrainContext.ColumnSample column = terrainContext.sampleColumn(
x, z, naturalFallback);
int normalTerrainHeight = localCoordinate(
column.terrainHeight(),
terrainContext.getLocalHeight()
);
int fluidHeight = localCoordinate(
column.fluidHeight(),
terrainContext.getLocalHeight()
);
layerInputs.add(new DimensionStackLayout.LayerInput(
terrainContext,
column.biome(),
column.region(),
column.rockBlock(),
column.fluidBlock(),
column.surfaceBlock(),
normalTerrainHeight,
fluidHeight
));
}
int[] seamOffsets = new int[seamOffsetSamplersBottomToTop.size()];
for (int seamIndex = 0; seamIndex < seamOffsets.length; seamIndex++) {
seamOffsets[seamIndex] = seamOffsetSamplersBottomToTop.get(seamIndex).sample(x, z);
}
return DimensionStackLayout.create(outputHeight, spacer, layerInputs, seamOffsets);
}
private static int localCoordinate(double sample, int localHeight) {
int maximum = Math.max(0, localHeight - 1);
long rounded = Math.round(sample);
return (int) Math.max(0L, Math.min(maximum, rounded));
}
public int getStackTerrainHeight(int x, int z) {
return (int) (sampleTopHeights(x, z) >>> 32);
}
public int getStackTopHeight(int x, int z) {
return (int) sampleTopHeights(x, z);
}
public DimensionStackLayout getLayout(int x, int z) {
boolean naturalFallback = usesTemporaryNaturalFallback(x, z);
long columnKey = ((long) x << 32) ^ (z & 0xFFFFFFFFL);
return resolveLayout(
layoutCache,
columnKey,
naturalFallback,
() -> sample(x, z, naturalFallback)
);
}
public DimensionStackLayout.Layer getLayerAt(int x, int y, int z) {
return getLayout(x, z).layerAt(y);
}
static DimensionStackLayout resolveLayout(
Cache<Long, DimensionStackLayout> cache,
long columnKey,
boolean naturalFallback,
Supplier<DimensionStackLayout> sampler
) {
if (naturalFallback) {
return sampler.get();
}
return cache.get(columnKey, ignored -> sampler.get());
}
private long sampleTopHeights(int x, int z) {
boolean naturalFallback = usesTemporaryNaturalFallback(x, z);
int baseY = 0;
int renderedTerrainTopY = Integer.MIN_VALUE;
int renderedContentTopY = Integer.MIN_VALUE;
int outputMaxY = outputHeight - 1;
int previousContentTopY = Integer.MIN_VALUE;
int previousRenderMinY = Integer.MIN_VALUE;
int previousRenderMaxY = Integer.MIN_VALUE;
int previousSurfaceY = Integer.MIN_VALUE;
boolean previousVisible = false;
for (int layerIndex = 0; layerIndex < layersBottomToTop.size(); layerIndex++) {
DimensionTerrainContext terrainContext = layersBottomToTop.get(layerIndex);
if (layerIndex > 0) {
int gapMinY = (int) Math.max(0L, (long) previousContentTopY + 1L);
int gapMaxY = (int) Math.min((long) outputMaxY, (long) baseY - 1L);
int previousTerrainY = previousVisible && previousSurfaceY >= previousRenderMinY
? Math.min(outputMaxY, previousSurfaceY)
: Integer.MIN_VALUE;
renderedTerrainTopY = DimensionStackLayout.eraseHeightThroughGap(
renderedTerrainTopY,
gapMinY,
gapMaxY,
previousTerrainY
);
renderedContentTopY = DimensionStackLayout.eraseHeightThroughGap(
renderedContentTopY,
gapMinY,
gapMaxY,
previousVisible ? previousRenderMaxY : Integer.MIN_VALUE
);
}
int localTerrainHeight = localCoordinate(
terrainContext.getNormalTerrainHeight(x, z, naturalFallback),
terrainContext.getLocalHeight()
);
int localFluidHeight = localCoordinate(
terrainContext.getFluidHeight(x, z, naturalFallback),
terrainContext.getLocalHeight()
);
int surfaceY = saturatedAdd(baseY, localTerrainHeight);
int fluidY = saturatedAdd(baseY, localFluidHeight);
int layerContentTopY = Math.max(surfaceY, fluidY);
int renderMinY = Math.max(0, baseY);
int renderMaxY = Math.min(outputMaxY, layerContentTopY);
if (renderMinY <= renderMaxY) {
renderedContentTopY = Math.max(renderedContentTopY, renderMaxY);
renderedTerrainTopY = DimensionStackLayout.mergeVisibleTerrainHeight(
renderedTerrainTopY,
renderMinY,
renderMaxY,
surfaceY,
outputMaxY
);
}
previousContentTopY = layerContentTopY;
previousRenderMinY = renderMinY;
previousRenderMaxY = renderMaxY;
previousSurfaceY = surfaceY;
previousVisible = renderMinY <= renderMaxY;
if (layerIndex + 1 < layersBottomToTop.size()) {
int seamOffset = seamOffsetSamplersBottomToTop.get(layerIndex).sample(x, z);
baseY = saturatedAdd(
layerContentTopY,
saturatedAdd(spacer, saturatedAdd(1, seamOffset))
);
}
}
int clippedTerrainTopY = Math.max(0, renderedTerrainTopY);
int clippedContentTopY = Math.max(0, renderedContentTopY);
return ((long) clippedTerrainTopY << 32) | (clippedContentTopY & 0xFFFFFFFFL);
}
private static int saturatedAdd(int left, int right) {
long sum = (long) left + right;
if (sum > Integer.MAX_VALUE) {
return Integer.MAX_VALUE;
}
if (sum < Integer.MIN_VALUE) {
return Integer.MIN_VALUE;
}
return (int) sum;
}
public int getOutputHeight() {
return outputHeight;
}
public int getSpacer() {
return spacer;
}
public boolean usesTemporaryNaturalFallback(int x, int z) {
return engine.answersFromNaturalTerrain(x, z);
}
public List<DimensionTerrainContext> getLayersTopToBottom() {
return layersTopToBottom;
}
public List<DimensionTerrainContext> getLayersBottomToTop() {
return layersBottomToTop;
}
private interface SeamOffsetSampler {
int sample(double x, double z);
}
private record ContextState(
Engine engine,
int outputHeight,
int spacer,
List<DimensionTerrainContext> layersBottomToTop,
List<SeamOffsetSampler> seamOffsetSamplersBottomToTop
) {
}
}
@@ -0,0 +1,349 @@
package art.arcane.iris.engine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.spi.PlatformBlockState;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public final class DimensionStackLayout {
private final List<Layer> layersBottomToTop;
private final List<Layer> layersTopToBottom;
private final int stackTerrainTopY;
private final int stackTopY;
private final int clippedStackTerrainTopY;
private final int clippedStackTopY;
private DimensionStackLayout(LayoutState state) {
layersBottomToTop = List.copyOf(state.layersBottomToTop());
ArrayList<Layer> reversed = new ArrayList<>(layersBottomToTop);
Collections.reverse(reversed);
layersTopToBottom = List.copyOf(reversed);
stackTerrainTopY = state.stackTerrainTopY();
stackTopY = state.stackTopY();
clippedStackTerrainTopY = state.clippedStackTerrainTopY();
clippedStackTopY = state.clippedStackTopY();
}
static DimensionStackLayout create(
int outputHeight,
int spacer,
List<LayerInput> layerInputsBottomToTop,
int[] seamOffsetsBottomToTop
) {
if (outputHeight < 1) {
throw new IllegalArgumentException("Output height must be positive");
}
if (spacer < 0) {
throw new IllegalArgumentException("Dimension stack spacer cannot be negative");
}
if (layerInputsBottomToTop.isEmpty()) {
throw new IllegalArgumentException("Dimension stack requires at least one layer");
}
if (seamOffsetsBottomToTop.length != layerInputsBottomToTop.size() - 1) {
throw new IllegalArgumentException("Dimension stack seam offset count does not match its layers");
}
int outputMaxY = outputHeight - 1;
int baseY = 0;
int stackTerrainTopY = Integer.MIN_VALUE;
int stackTopY = Integer.MIN_VALUE;
int renderedTerrainTopY = Integer.MIN_VALUE;
int renderedContentTopY = Integer.MIN_VALUE;
ArrayList<Layer> layers = new ArrayList<>(layerInputsBottomToTop.size());
for (int layerIndex = 0; layerIndex < layerInputsBottomToTop.size(); layerIndex++) {
LayerInput input = layerInputsBottomToTop.get(layerIndex);
int seamOffsetBelow = layerIndex == 0 ? 0 : seamOffsetsBottomToTop[layerIndex - 1];
int spacerBelow = layerIndex == 0 ? 0 : spacer;
if (layerIndex > 0) {
Layer lower = layers.get(layerIndex - 1);
int gapMinY = (int) Math.max(0L, (long) lower.contentTopY() + 1L);
int gapMaxY = (int) Math.min((long) outputMaxY, (long) baseY - 1L);
renderedTerrainTopY = eraseHeightThroughGap(
renderedTerrainTopY,
gapMinY,
gapMaxY,
visibleTerrainY(lower)
);
renderedContentTopY = eraseHeightThroughGap(
renderedContentTopY,
gapMinY,
gapMaxY,
lower.visible() ? lower.renderMaxY() : Integer.MIN_VALUE
);
}
int surfaceY = saturatedAdd(baseY, input.normalTerrainHeight());
int fluidY = saturatedAdd(baseY, input.fluidHeight());
int contentTopY = Math.max(surfaceY, fluidY);
int renderMinY = Math.max(0, baseY);
int renderMaxY = Math.min(outputMaxY, contentTopY);
boolean visible = renderMinY <= renderMaxY;
Layer layer = new Layer(
input.terrainContext(),
input.biome(),
input.region(),
input.rockBlock(),
input.fluidBlock(),
input.surfaceBlock(),
baseY,
input.normalTerrainHeight(),
input.fluidHeight(),
surfaceY,
fluidY,
contentTopY,
spacerBelow,
seamOffsetBelow,
renderMinY,
renderMaxY,
clip(surfaceY, outputMaxY),
clip(fluidY, outputMaxY),
visible
);
layers.add(layer);
stackTerrainTopY = Math.max(stackTerrainTopY, surfaceY);
stackTopY = Math.max(stackTopY, contentTopY);
if (visible) {
renderedContentTopY = Math.max(renderedContentTopY, renderMaxY);
renderedTerrainTopY = mergeVisibleTerrainHeight(
renderedTerrainTopY,
renderMinY,
renderMaxY,
surfaceY,
outputMaxY
);
}
if (layerIndex + 1 < layerInputsBottomToTop.size()) {
baseY = saturatedAdd(
contentTopY,
saturatedAdd(spacer, saturatedAdd(1, seamOffsetsBottomToTop[layerIndex]))
);
}
}
return new DimensionStackLayout(new LayoutState(
layers,
stackTerrainTopY,
stackTopY,
Math.max(0, renderedTerrainTopY),
Math.max(0, renderedContentTopY)
));
}
private static int saturatedAdd(int left, int right) {
long sum = (long) left + right;
if (sum > Integer.MAX_VALUE) {
return Integer.MAX_VALUE;
}
if (sum < Integer.MIN_VALUE) {
return Integer.MIN_VALUE;
}
return (int) sum;
}
private static int clip(int value, int outputMaxY) {
return Math.max(0, Math.min(outputMaxY, value));
}
static int mergeVisibleTerrainHeight(
int currentTerrainTopY,
int renderMinY,
int renderMaxY,
int surfaceY,
int outputMaxY
) {
int terrainTopY = currentTerrainTopY;
if (terrainTopY >= renderMinY && terrainTopY <= renderMaxY) {
terrainTopY = Integer.MIN_VALUE;
}
if (surfaceY >= renderMinY) {
terrainTopY = Math.max(terrainTopY, Math.min(outputMaxY, surfaceY));
}
return terrainTopY;
}
static int eraseHeightThroughGap(
int currentTopY,
int gapMinY,
int gapMaxY,
int fallbackTopY
) {
if (currentTopY >= gapMinY && currentTopY <= gapMaxY) {
return fallbackTopY;
}
return currentTopY;
}
private static int visibleTerrainY(Layer layer) {
if (!layer.visible() || layer.surfaceY() < layer.renderMinY()) {
return Integer.MIN_VALUE;
}
return layer.clippedSurfaceY();
}
public List<Layer> layersBottomToTop() {
return layersBottomToTop;
}
public List<Layer> layersTopToBottom() {
return layersTopToBottom;
}
public int stackTerrainTopY() {
return stackTerrainTopY;
}
public int stackTopY() {
return stackTopY;
}
public int clippedStackTerrainTopY() {
return clippedStackTerrainTopY;
}
public int clippedStackTopY() {
return clippedStackTopY;
}
public Layer topTerrainLayer() {
Layer highest = null;
int highestSurfaceY = Integer.MIN_VALUE;
for (int layerIndex = 0; layerIndex < layersBottomToTop.size(); layerIndex++) {
Layer layer = layersBottomToTop.get(layerIndex);
if (layerIndex > 0) {
Layer lower = layersBottomToTop.get(layerIndex - 1);
if ((long) highestSurfaceY > lower.contentTopY()
&& (long) highestSurfaceY < layer.localBaseY()) {
highest = visibleTerrainY(lower) == Integer.MIN_VALUE ? null : lower;
highestSurfaceY = visibleTerrainY(lower);
}
}
if (layer.containsRenderedY(highestSurfaceY)) {
highest = null;
highestSurfaceY = Integer.MIN_VALUE;
}
if (!layer.visible() || layer.surfaceY() < layer.renderMinY()) {
continue;
}
if (layer.clippedSurfaceY() >= highestSurfaceY) {
highest = layer;
highestSurfaceY = layer.clippedSurfaceY();
}
}
return highest;
}
public Layer surfaceLayer() {
Layer terrainLayer = topTerrainLayer();
return terrainLayer == null ? topVisibleLayer() : terrainLayer;
}
private Layer topVisibleLayer() {
Layer highest = null;
int highestContentY = Integer.MIN_VALUE;
for (int layerIndex = 0; layerIndex < layersBottomToTop.size(); layerIndex++) {
Layer layer = layersBottomToTop.get(layerIndex);
if (layerIndex > 0) {
Layer lower = layersBottomToTop.get(layerIndex - 1);
if ((long) highestContentY > lower.contentTopY()
&& (long) highestContentY < layer.localBaseY()) {
highest = lower.visible() ? lower : null;
highestContentY = lower.visible()
? lower.renderMaxY()
: Integer.MIN_VALUE;
}
}
if (layer.visible() && layer.renderMaxY() >= highestContentY) {
highest = layer;
highestContentY = layer.renderMaxY();
}
}
return highest;
}
public Layer layerAt(int y) {
Layer bottom = layersBottomToTop.get(0);
Layer owner = bottom;
for (int layerIndex = 1; layerIndex < layersBottomToTop.size(); layerIndex++) {
Layer lower = layersBottomToTop.get(layerIndex - 1);
Layer layer = layersBottomToTop.get(layerIndex);
if ((long) y > lower.contentTopY() && (long) y < layer.localBaseY()) {
owner = bottom;
}
if (layer.containsRenderedY(y)) {
owner = layer;
}
}
return owner;
}
public boolean containsUpperLayerY(int y) {
for (int layerIndex = 1; layerIndex < layersBottomToTop.size(); layerIndex++) {
if (layersBottomToTop.get(layerIndex).containsRenderedY(y)) {
return true;
}
}
return false;
}
public boolean isHostFeatureProtectedY(int y) {
for (int layerIndex = 1; layerIndex < layersBottomToTop.size(); layerIndex++) {
Layer lower = layersBottomToTop.get(layerIndex - 1);
Layer upper = layersBottomToTop.get(layerIndex);
if (upper.containsRenderedY(y)
|| ((long) y > lower.contentTopY() && (long) y < upper.localBaseY())) {
return true;
}
}
return false;
}
static record LayerInput(
DimensionTerrainContext terrainContext,
IrisBiome biome,
IrisRegion region,
PlatformBlockState rockBlock,
PlatformBlockState fluidBlock,
PlatformBlockState surfaceBlock,
int normalTerrainHeight,
int fluidHeight
) {
}
public record Layer(
DimensionTerrainContext terrainContext,
IrisBiome biome,
IrisRegion region,
PlatformBlockState rockBlock,
PlatformBlockState fluidBlock,
PlatformBlockState surfaceBlock,
int localBaseY,
int normalTerrainHeight,
int fluidHeight,
int surfaceY,
int fluidY,
int contentTopY,
int spacerBelow,
int seamOffsetBelow,
int renderMinY,
int renderMaxY,
int clippedSurfaceY,
int clippedFluidY,
boolean visible
) {
public boolean containsRenderedY(int y) {
return visible && y >= renderMinY && y <= renderMaxY;
}
}
private record LayoutState(
List<Layer> layersBottomToTop,
int stackTerrainTopY,
int stackTopY,
int clippedStackTerrainTopY,
int clippedStackTopY
) {
}
}
@@ -0,0 +1,764 @@
package art.arcane.iris.engine;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.image.IrisImageMapRuntime;
import art.arcane.iris.engine.object.InferredType;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisGenerator;
import art.arcane.iris.engine.object.IrisInterpolator;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisShapedGeneratorStyle;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.DataProvider;
import art.arcane.iris.util.project.interpolation.NoiseBounds;
import art.arcane.iris.util.project.stream.ProceduralStream;
import art.arcane.iris.util.project.stream.interpolation.Interpolated;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
public final class DimensionTerrainContext implements DataProvider {
private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D);
private final Engine engine;
private final IrisDimension dimension;
private final IrisData data;
private final int localHeight;
private final ProceduralStream<Double> heightStream;
private final ProceduralStream<Double> slopeStream;
private final ProceduralStream<Double> fluidHeightStream;
private final ProceduralStream<IrisBiome> biomeStream;
private final ProceduralStream<IrisRegion> regionStream;
private final ProceduralStream<PlatformBlockState> rockStream;
private final FluidBlockSampler fluidBlockSampler;
private final IrisImageMapRuntime imageMapRuntime;
private final boolean selfReferencing;
private final SelfFallback selfFallback;
private DimensionTerrainContext(ContextState state) {
engine = state.engine();
dimension = state.dimension();
data = state.data();
localHeight = state.localHeight();
heightStream = state.heightStream();
slopeStream = state.slopeStream();
fluidHeightStream = state.fluidHeightStream();
biomeStream = state.biomeStream();
regionStream = state.regionStream();
rockStream = state.rockStream();
fluidBlockSampler = state.fluidBlockSampler();
imageMapRuntime = state.imageMapRuntime();
selfReferencing = state.selfReferencing();
selfFallback = state.selfFallback();
}
public static DimensionTerrainContext forStack(Engine engine, IrisDimension dimension) {
boolean selfReferencing = dimension.getLoadKey().equals(engine.getDimension().getLoadKey());
if (selfReferencing) {
return createSelfReferencing(engine, false);
}
int localHeight = dimension.getMaxHeight() - dimension.getMinHeight();
return createCrossReferencing(
engine,
dimension,
localHeight,
dimension.getMinHeight(),
"stack-" + dimension.getLoadKey(),
true
);
}
static DimensionTerrainContext forUpper(Engine engine, IrisDimension dimension) {
boolean selfReferencing = dimension.getLoadKey().equals(engine.getDimension().getLoadKey());
if (selfReferencing) {
return createSelfReferencing(engine, true);
}
return createCrossReferencing(
engine,
dimension,
engine.getHeight(),
engine.getMinHeight(),
"upper",
false
);
}
private static DimensionTerrainContext createSelfReferencing(Engine engine, boolean natural) {
IrisComplex complex = engine.getComplex();
double fluidHeight = engine.getDimension().getFluidHeight();
ProceduralStream<Double> fluidHeightStream = natural
? ProceduralStream.ofDouble((x, z) -> fluidHeight)
: complex.getRiverWaterSurfaceStream();
FluidBlockSampler fluidBlockSampler = natural
? (x, z) -> complex.getFluidStream().get(x, z)
: complex::resolveSurfaceFluid;
SelfFallback selfFallback = natural
? null
: new SelfFallback(
complex.getNaturalHeightStream(),
complex.getNaturalTrueBiomeStream(),
fluidHeight,
(x, z) -> complex.getFluidStream().get(x, z)
);
return new DimensionTerrainContext(new ContextState(
engine,
engine.getDimension(),
engine.getData(),
engine.getHeight(),
natural ? complex.getNaturalHeightStream() : complex.getHeightStream(),
natural ? complex.getNaturalSlopeStream() : complex.getSlopeStream(),
fluidHeightStream,
natural ? complex.getNaturalTrueBiomeStream() : complex.getTrueBiomeStream(),
complex.getRegionStream(),
complex.getRockStream(),
fluidBlockSampler,
complex.getImageMapRuntime(),
true,
selfFallback
));
}
private static DimensionTerrainContext createCrossReferencing(
Engine engine,
IrisDimension dimension,
int localHeight,
int minimumWorldHeight,
String cachePrefix,
boolean includeFluid
) {
IrisData resolvedData = dimension.getLoader();
if (resolvedData == null) {
resolvedData = engine.getData();
}
IrisData dimensionData = resolvedData;
IrisImageMapRuntime imageMapRuntime = IrisImageMapRuntime.compile(
dimensionData,
dimension,
minimumWorldHeight
);
long seedOffset = dimension.getLoadKey().hashCode();
RNG rng = new RNG(engine.getSeedManager().getComplex() ^ seedOffset);
double fluidHeight = dimension.getFluidHeight();
int cacheSize = IrisSettings.get().getPerformance().getNoiseCacheSize();
DataProvider dataProvider = () -> dimensionData;
IrisBiome configuredFocusBiome = loadFocusBiome(dimension, dimensionData);
IrisBiome focusBiome = configuredFocusBiome == null
? null
: configuredFocusBiome.withInferredType(InferredType.LAND);
IrisRegion focusRegion = focusBiome == null
? loadFocusRegion(dimension, dimensionData)
: findFocusRegion(focusBiome, dimension, dataProvider, dimensionData);
Map<IrisInterpolator, Set<IrisGenerator>> generators = new HashMap<>();
Set<IrisBiome> allBiomes = Collections.newSetFromMap(new IdentityHashMap<>());
Map<IrisBiome, IrisComplex.ChildSelectionPlan> childSelectionPlans =
Collections.synchronizedMap(new IdentityHashMap<>());
KList<IrisRegion> preparedRegions = new KList<>();
if (focusRegion != null) {
preparedRegions.add(focusRegion);
focusRegion.getNaturalBiomes(dataProvider).forEach(biome -> registerBiomeGenerators(
biome, dataProvider, allBiomes, generators));
} else {
for (IrisRegion region : dimension.getAllRegions(dataProvider)) {
preparedRegions.add(region);
region.getNaturalBiomes(dataProvider).forEach(biome -> registerBiomeGenerators(
biome, dataProvider, allBiomes, generators));
}
for (IrisBiome mappedBiome : imageMapRuntime.getMappedBiomes()) {
if (!mappedBiome.isCompatExcluded()) {
registerBiomeGenerators(mappedBiome, dataProvider, allBiomes, generators);
}
}
}
if (focusBiome != null) {
allBiomes.add(focusBiome);
}
IrisComplex.GeneratorGroup[] generatorGroups = IrisComplex.freezeGeneratorGroups(generators);
Map<IrisInterpolator, IdentityHashMap<IrisBiome, NoiseBounds>> generatorBounds = new HashMap<>();
for (IrisComplex.GeneratorGroup group : generatorGroups) {
IdentityHashMap<IrisBiome, NoiseBounds> interpolatorBounds =
new IdentityHashMap<>(Math.max(allBiomes.size(), 16));
for (IrisBiome biome : allBiomes) {
double minimum = 0D;
double maximum = 0D;
for (IrisGenerator generator : group.generators()) {
String key = generator.getLoadKey();
if (key == null || key.isBlank()) {
continue;
}
maximum += biome.getGenLinkMax(key, engine);
minimum += biome.getGenLinkMin(key, engine);
}
interpolatorBounds.put(biome, new NoiseBounds(minimum, maximum));
}
generatorBounds.put(group.interpolator(), interpolatorBounds);
}
ProceduralStream<Double> regionStyleStream = dimension.getRegionStyle()
.create(rng.nextParallelRNG(883), dimensionData).stream()
.zoom(dimension.getRegionZoom());
KList<IrisRegion> regionPool = new KList<>();
for (IrisRegion region : dimensionData.getRegionLoader().loadAll(dimension.getRegions())) {
if (!region.isCompatExcluded()) {
regionPool.add(region);
}
}
ProceduralStream<IrisRegion> proceduralRegionStream = focusRegion == null
? regionStyleStream.selectRarity(regionPool)
: ProceduralStream.of(
(x, z) -> focusRegion,
Interpolated.of(value -> 0D, value -> focusRegion)
);
ProceduralStream<IrisRegion> regionStream = focusRegion == null
? proceduralRegionStream
.convertAware2D((region, x, z) -> mappedRegion(imageMapRuntime, region, x, z))
.cache2D(cachePrefix + "ImageMappedRegionStream", engine, cacheSize)
: proceduralRegionStream;
Map<IrisRegion, Map<InferredType, ProceduralStream<IrisBiome>>> preparedBiomeStreams =
compileInferredBiomeStreams(dimension, dimensionData, rng, preparedRegions);
ProceduralStream<IrisBiome> landBiomeStream = regionStream
.convert(region -> IrisComplex.preparedInferredBiomeStream(
preparedBiomeStreams, region, InferredType.LAND))
.convertAware2D(ProceduralStream::get);
ProceduralStream<IrisBiome> seaBiomeStream = regionStream
.convert(region -> IrisComplex.preparedInferredBiomeStream(
preparedBiomeStreams, region, InferredType.SEA))
.convertAware2D(ProceduralStream::get);
ProceduralStream<IrisBiome> shoreBiomeStream = regionStream
.convert(region -> IrisComplex.preparedInferredBiomeStream(
preparedBiomeStreams, region, InferredType.SHORE))
.convertAware2D(ProceduralStream::get);
Map<InferredType, ProceduralStream<IrisBiome>> inferredStreams = new HashMap<>();
inferredStreams.put(InferredType.LAND, landBiomeStream);
inferredStreams.put(InferredType.SEA, seaBiomeStream);
inferredStreams.put(InferredType.SHORE, shoreBiomeStream);
ProceduralStream<InferredType> bridgeStream = focusBiome == null
? dimension.getContinentalStyle()
.create(rng.nextParallelRNG(234234565), dimensionData)
.bake().scale(1D / dimension.getContinentZoom()).bake().stream()
.convert(value -> value >= dimension.getLandChance()
? InferredType.SEA
: InferredType.LAND)
: ProceduralStream.of(
(x, z) -> focusBiome.getInferredType(),
Interpolated.of(value -> 0D, value -> focusBiome.getInferredType())
);
ProceduralStream<IrisBiome> proceduralBaseBiomeStream = focusBiome == null
? bridgeStream
.convertAware2D((type, x, z) -> {
ProceduralStream<IrisBiome> stream = inferredStreams.get(type);
return stream != null
? stream.get(x, z)
: inferredStreams.get(InferredType.LAND).get(x, z);
})
.convertAware2D((biome, x, z) -> implode(
biome, x, z, rng, dataProvider, childSelectionPlans, 3))
: ProceduralStream.of(
(x, z) -> focusBiome,
Interpolated.of(value -> 0D, value -> focusBiome)
);
ProceduralStream<IrisBiome> baseBiomeStream = focusBiome == null
? proceduralBaseBiomeStream
.convertAware2D((biome, x, z) -> mappedBiome(imageMapRuntime, biome, x, z))
.cache2D(cachePrefix + "ImageMappedBaseBiomeStream", engine, cacheSize)
: proceduralBaseBiomeStream;
KList<IrisShapedGeneratorStyle> overlayNoise = dimension.getOverlayNoise();
ProceduralStream<Double> overlayStream = overlayNoise.isEmpty()
? ProceduralStream.ofDouble((x, z) -> 0D)
: ProceduralStream.ofDouble((x, z) -> {
double value = 0D;
for (IrisShapedGeneratorStyle style : overlayNoise) {
value += style.get(rng, dimensionData, x, z);
}
return value;
});
long heightSeed = engine.getSeedManager().getHeight() ^ seedOffset;
ProceduralStream<Double> heightStream = ProceduralStream.of((x, z) -> {
IrisBiome biome = baseBiomeStream.get(x, z);
if (biome == null) {
return mappedTerrainHeight(imageMapRuntime, fluidHeight, x, z);
}
double interpolatedHeight = 0D;
for (IrisComplex.GeneratorGroup group : generatorGroups) {
IrisInterpolator interpolator = group.interpolator();
IrisGenerator[] groupGenerators = group.generators();
if (groupGenerators.length == 0) {
continue;
}
IdentityHashMap<IrisBiome, NoiseBounds> cachedBounds = generatorBounds.get(interpolator);
NoiseBounds sampledBounds = interpolator.interpolateBounds(x, z, (sampleX, sampleZ) -> {
try {
IrisBiome sampledBiome = baseBiomeStream.get(sampleX, sampleZ);
if (sampledBiome == null) {
return ZERO_NOISE_BOUNDS;
}
NoiseBounds bounds = cachedBounds == null ? null : cachedBounds.get(sampledBiome);
if (bounds != null) {
return bounds;
}
double minimum = 0D;
double maximum = 0D;
for (IrisGenerator generator : groupGenerators) {
String key = generator.getLoadKey();
if (key == null || key.isBlank()) {
continue;
}
maximum += sampledBiome.getGenLinkMax(key, engine);
minimum += sampledBiome.getGenLinkMin(key, engine);
}
return new NoiseBounds(minimum, maximum);
} catch (Throwable e) {
IrisLogging.reportError(e);
return ZERO_NOISE_BOUNDS;
}
});
interpolatedHeight += IrisComplex.averageGeneratorHeights(
groupGenerators,
sampledBounds.min(),
sampledBounds.max(),
x,
z,
heightSeed + 239945
);
}
double proceduralHeight = Math.max(
Math.min(interpolatedHeight + fluidHeight + overlayStream.get(x, z), localHeight),
0D
);
return mappedTerrainHeight(imageMapRuntime, proceduralHeight, x, z);
}, Interpolated.DOUBLE).cache2DDouble(
cachePrefix + "ImageMappedHeightStream", engine, cacheSize);
ProceduralStream<Double> slopeStream = heightStream.slope(3)
.cache2DDouble(cachePrefix + "SlopeStream", engine, cacheSize);
ProceduralStream<IrisBiome> finalBiomeStream = focusBiome == null
? heightStream.convertAware2D((height, x, z) -> {
IrisBiome mappedBiome = imageMapRuntime.sampleBiome(x, z);
if (mappedBiome != null) {
return mappedBiome;
}
IrisBiome baseBiome = baseBiomeStream.get(x, z);
IrisBiome resolvedBiome = IrisComplex.resolveSurfaceBiome(
height,
baseBiome,
regionStream.get(x, z),
x,
z,
fluidHeight,
landBiomeStream,
seaBiomeStream,
shoreBiomeStream
);
return resolvedBiome == baseBiome
? baseBiome
: implode(resolvedBiome, x, z, rng, dataProvider, childSelectionPlans, 3);
}).cache2D(cachePrefix + "ImageMappedFinalBiomeStream", engine, cacheSize)
: ProceduralStream.of(
(x, z) -> focusBiome,
Interpolated.of(value -> 0D, value -> focusBiome)
).cache2D(cachePrefix + "FinalBiomeStreamFocus", engine, cacheSize);
ProceduralStream<PlatformBlockState> rockStream = dimension.getRockPalette()
.getLayerGenerator(rng.nextParallelRNG(45), dimensionData).stream()
.select(dimension.getRockPalette().getBlockData(dimensionData));
FluidBlockSampler fluidBlockSampler;
if (includeFluid) {
ProceduralStream<PlatformBlockState> fluidStream = dimension.getFluidPalette()
.getLayerGenerator(rng.nextParallelRNG(78), dimensionData).stream()
.select(dimension.getFluidPalette().getBlockData(dimensionData));
fluidBlockSampler = fluidStream::get;
} else {
fluidBlockSampler = (x, z) -> null;
}
return new DimensionTerrainContext(new ContextState(
engine,
dimension,
dimensionData,
localHeight,
heightStream,
slopeStream,
ProceduralStream.ofDouble((x, z) -> fluidHeight),
finalBiomeStream,
regionStream,
rockStream,
fluidBlockSampler,
imageMapRuntime,
false,
null
));
}
private static void registerBiomeGenerators(
IrisBiome biome,
DataProvider dataProvider,
Set<IrisBiome> allBiomes,
Map<IrisInterpolator, Set<IrisGenerator>> generators
) {
allBiomes.add(biome);
biome.getGenerators().forEach(link -> {
IrisGenerator generator = link.getCachedGenerator(dataProvider);
if (generator != null) {
generators.computeIfAbsent(generator.getInterpolator(), key -> new HashSet<>()).add(generator);
}
});
}
static IrisRegion mappedRegion(
IrisImageMapRuntime imageMapRuntime,
IrisRegion proceduralRegion,
double worldX,
double worldZ
) {
IrisRegion mappedRegion = imageMapRuntime.sampleRegion(worldX, worldZ);
return mappedRegion == null ? proceduralRegion : mappedRegion;
}
static double mappedTerrainHeight(
IrisImageMapRuntime imageMapRuntime,
double proceduralHeight,
double worldX,
double worldZ
) {
return imageMapRuntime.sampleTerrainHeight(worldX, worldZ, proceduralHeight);
}
static IrisBiome mappedBiome(
IrisImageMapRuntime imageMapRuntime,
IrisBiome proceduralBiome,
double worldX,
double worldZ
) {
IrisBiome mappedBiome = imageMapRuntime.sampleBiome(worldX, worldZ);
return mappedBiome == null ? proceduralBiome : mappedBiome;
}
static PlatformBlockState mappedSurfaceBlock(
IrisImageMapRuntime imageMapRuntime,
PlatformBlockState proceduralBlock,
double worldX,
double worldZ
) {
PlatformBlockState mappedBlock = imageMapRuntime.sampleSurfaceBlock(worldX, worldZ);
return mappedBlock == null ? proceduralBlock : mappedBlock;
}
private static Map<IrisRegion, Map<InferredType, ProceduralStream<IrisBiome>>> compileInferredBiomeStreams(
IrisDimension dimension,
IrisData data,
RNG rng,
Iterable<IrisRegion> regions
) {
IdentityHashMap<IrisRegion, Map<InferredType, ProceduralStream<IrisBiome>>> compiled =
new IdentityHashMap<>();
for (IrisRegion region : regions) {
if (compiled.containsKey(region)) {
continue;
}
EnumMap<InferredType, ProceduralStream<IrisBiome>> streams = new EnumMap<>(InferredType.class);
streams.put(InferredType.LAND, createInferredBiomeStream(
dimension, data, rng, region, InferredType.LAND));
streams.put(InferredType.SEA, createInferredBiomeStream(
dimension, data, rng, region, InferredType.SEA));
streams.put(InferredType.SHORE, createInferredBiomeStream(
dimension, data, rng, region, InferredType.SHORE));
compiled.put(region, Collections.unmodifiableMap(streams));
}
return Collections.unmodifiableMap(compiled);
}
private static ProceduralStream<IrisBiome> createInferredBiomeStream(
IrisDimension dimension,
IrisData data,
RNG rng,
IrisRegion region,
InferredType type
) {
return switch (type) {
case LAND -> dimension.getLandBiomeStyle()
.create(rng.nextParallelRNG(InferredType.LAND.ordinal()), data).stream()
.zoom(dimension.getBiomeZoom())
.zoom(dimension.getLandZoom())
.zoom(region.getLandBiomeZoom())
.selectRarity(loadInferredBiomes(data, region.getLandBiomes(), InferredType.LAND));
case SEA -> dimension.getSeaBiomeStyle()
.create(rng.nextParallelRNG(InferredType.SEA.ordinal()), data).stream()
.zoom(dimension.getBiomeZoom())
.zoom(dimension.getSeaZoom())
.zoom(region.getSeaBiomeZoom())
.selectRarity(loadInferredBiomes(data, region.getSeaBiomes(), InferredType.SEA));
case SHORE -> dimension.getShoreBiomeStyle()
.create(rng.nextParallelRNG(InferredType.SHORE.ordinal()), data).stream()
.zoom(dimension.getBiomeZoom())
.zoom(region.getShoreBiomeZoom())
.selectRarity(loadInferredBiomes(data, region.getShoreBiomes(), InferredType.SHORE));
default -> throw new IllegalArgumentException("Unsupported stacked terrain biome type: " + type);
};
}
private static KList<IrisBiome> loadInferredBiomes(
IrisData data,
KList<String> keys,
InferredType type
) {
KList<IrisBiome> inferredBiomes = new KList<>();
for (IrisBiome biome : data.getBiomeLoader().loadAll(keys)) {
if (biome.isCompatExcluded()) {
continue;
}
inferredBiomes.add(biome.withInferredType(type));
}
return inferredBiomes;
}
private static IrisBiome loadFocusBiome(IrisDimension dimension, IrisData data) {
String key = dimension.getFocus();
if (key == null || key.isBlank()) {
return null;
}
IrisBiome biome = data.getBiomeLoader().load(key);
return biome == null || biome.isCompatExcluded() ? null : biome;
}
private static IrisRegion loadFocusRegion(IrisDimension dimension, IrisData data) {
String key = dimension.getFocusRegion();
if (key == null || key.isBlank()) {
return null;
}
IrisRegion region = data.getRegionLoader().load(key);
return region == null || region.isCompatExcluded() ? null : region;
}
private static IrisRegion findFocusRegion(
IrisBiome focusBiome,
IrisDimension dimension,
DataProvider dataProvider,
IrisData data
) {
for (IrisRegion region : dimension.getAllRegions(dataProvider)) {
if (region.getAllBiomeIds().contains(focusBiome.getLoadKey())) {
return region;
}
}
String key = UUID.nameUUIDFromBytes((dimension.getLoadKey() + "\u0000" + focusBiome.getLoadKey())
.getBytes(StandardCharsets.UTF_8)).toString();
IrisRegion region = new IrisRegion();
region.getLandBiomes().add(focusBiome.getLoadKey());
region.getSeaBiomes().add(focusBiome.getLoadKey());
region.getShoreBiomes().add(focusBiome.getLoadKey());
region.setLoadKey(key);
region.setLoader(data);
region.setLoadFile(new File(
data.getDataFolder(),
data.getRegionLoader().getFolderName() + "/" + key + ".json"
));
return region;
}
private static IrisBiome implode(
IrisBiome biome,
double x,
double z,
RNG rng,
DataProvider dataProvider,
Map<IrisBiome, IrisComplex.ChildSelectionPlan> childSelectionPlans,
int remainingDepth
) {
if (biome == null || remainingDepth < 0 || biome.getChildren().isEmpty()) {
return biome;
}
IrisComplex.ChildSelectionPlan selectionPlan = childSelectionPlans.get(biome);
if (selectionPlan == null) {
synchronized (childSelectionPlans) {
selectionPlan = childSelectionPlans.get(biome);
if (selectionPlan == null) {
KList<IrisBiome> options = new KList<>();
for (IrisBiome child : biome.getRealChildren(dataProvider)) {
if (child != null) {
options.add(child);
}
}
options.add(biome);
selectionPlan = IrisComplex.ChildSelectionPlan.create(options);
childSelectionPlans.put(biome, selectionPlan);
}
}
}
IrisBiome selected = selectionPlan.select(
biome.getChildrenGenerator(rng, 123, biome.getChildShrinkFactor()), x, z);
if (selected == null) {
return biome;
}
return implode(
selected.withInferredType(biome.getInferredType()),
x,
z,
rng,
dataProvider,
childSelectionPlans,
remainingDepth - 1
);
}
public double getNormalTerrainHeight(double x, double z) {
return getNormalTerrainHeight(x, z, usesNaturalFallback(x, z));
}
double getNormalTerrainHeight(double x, double z, boolean naturalFallback) {
if (naturalFallback && selfFallback != null) {
return selfFallback.heightStream().getDouble(x, z);
}
return heightStream.getDouble(x, z);
}
public double getFluidHeight(double x, double z) {
return getFluidHeight(x, z, usesNaturalFallback(x, z));
}
double getFluidHeight(double x, double z, boolean naturalFallback) {
if (naturalFallback && selfFallback != null) {
return selfFallback.fluidHeight();
}
return fluidHeightStream.getDouble(x, z);
}
public ProceduralStream<Double> getSlopeStream() {
return slopeStream;
}
public IrisBiome getBiome(double x, double z) {
return getBiome(x, z, usesNaturalFallback(x, z));
}
private IrisBiome getBiome(double x, double z, boolean naturalFallback) {
if (naturalFallback && selfFallback != null) {
return selfFallback.biomeStream().get(x, z);
}
return biomeStream.get(x, z);
}
public IrisRegion getRegion(double x, double z) {
return regionStream == null ? null : regionStream.get(x, z);
}
public PlatformBlockState getRockBlock(double x, double z) {
return rockStream.get(x, z);
}
public PlatformBlockState getFluidBlock(double x, double z) {
return getFluidBlock(x, z, usesNaturalFallback(x, z));
}
private PlatformBlockState getFluidBlock(double x, double z, boolean naturalFallback) {
if (naturalFallback && selfFallback != null) {
return selfFallback.fluidBlockSampler().sample(x, z);
}
return fluidBlockSampler.sample(x, z);
}
public PlatformBlockState getSurfaceBlock(double x, double z) {
return imageMapRuntime.sampleSurfaceBlock(x, z);
}
public IrisDimension getDimension() {
return dimension;
}
@Override
public IrisData getData() {
return data;
}
public int getLocalHeight() {
return localHeight;
}
public boolean isSelfReferencing() {
return selfReferencing;
}
ColumnSample sampleColumn(double x, double z, boolean naturalFallback) {
return new ColumnSample(
getNormalTerrainHeight(x, z, naturalFallback),
getFluidHeight(x, z, naturalFallback),
getBiome(x, z, naturalFallback),
getRegion(x, z),
getRockBlock(x, z),
getFluidBlock(x, z, naturalFallback),
getSurfaceBlock(x, z)
);
}
private boolean usesNaturalFallback(double x, double z) {
return selfFallback != null && engine.answersFromNaturalTerrain(
(int) Math.floor(x),
(int) Math.floor(z)
);
}
private interface FluidBlockSampler {
PlatformBlockState sample(double x, double z);
}
private record ContextState(
Engine engine,
IrisDimension dimension,
IrisData data,
int localHeight,
ProceduralStream<Double> heightStream,
ProceduralStream<Double> slopeStream,
ProceduralStream<Double> fluidHeightStream,
ProceduralStream<IrisBiome> biomeStream,
ProceduralStream<IrisRegion> regionStream,
ProceduralStream<PlatformBlockState> rockStream,
FluidBlockSampler fluidBlockSampler,
IrisImageMapRuntime imageMapRuntime,
boolean selfReferencing,
SelfFallback selfFallback
) {
}
private record SelfFallback(
ProceduralStream<Double> heightStream,
ProceduralStream<IrisBiome> biomeStream,
double fluidHeight,
FluidBlockSampler fluidBlockSampler
) {
}
record ColumnSample(
double terrainHeight,
double fluidHeight,
IrisBiome biome,
IrisRegion region,
PlatformBlockState rockBlock,
PlatformBlockState fluidBlock,
PlatformBlockState surfaceBlock
) {
}
}
@@ -59,15 +59,22 @@ final class EngineHotloader {
IrisComplex nextComplex = null;
try {
engine.sealForTransition("complex hotload", false);
engine.prepareRuntimeHotload();
RuntimeAssembly assembly = new RuntimeAssembly(RuntimeAssembly.nextRuntimeId(), previous.target());
engine.runtimeAssembly.set(assembly);
EngineRuntime next;
try (IrisContext.Scope ignored = IrisContext.open(engine, engine.getGenerationSessions().currentSessionId(), null)) {
assembly.complex = new IrisComplex(engine);
nextComplex = assembly.complex;
assembly.dimensionStackContext = engine.runtimeBuilder.buildDimensionStackContext();
assembly.upperContext = engine.runtimeBuilder.buildUpperContext();
BiomeMaxes biomeMaxes = engine.runtimeBuilder.computeBiomeMaxes();
next = previous.withComplex(assembly.cacheId, assembly.complex, assembly.upperContext, biomeMaxes);
next = previous.withComplex(
assembly.cacheId,
assembly.complex,
assembly.upperContext,
assembly.dimensionStackContext,
biomeMaxes);
} finally {
engine.runtimeAssembly.remove();
}
@@ -110,6 +117,7 @@ final class EngineHotloader {
boolean published = false;
try {
engine.sealForTransition("hotload", false);
engine.prepareRuntimeHotload();
replacementData = IrisData.openRuntime(previousData.getDataFolder());
IrisDimension replacement = replacementData.getDimensionLoader().load(previousDimension.getLoadKey());
if (replacement == null) {
@@ -34,6 +34,7 @@ record EngineRuntime(
EngineTarget target,
IrisComplex complex,
UpperDimensionContext upperContext,
DimensionStackContext dimensionStackContext,
EngineEffects effects,
EngineMode mode,
EngineWorldManager worldManager,
@@ -44,6 +45,7 @@ record EngineRuntime(
int nextCacheId,
IrisComplex nextComplex,
UpperDimensionContext nextUpperContext,
DimensionStackContext nextDimensionStackContext,
BiomeMaxes nextBiomeMaxes
) {
return new EngineRuntime(
@@ -51,6 +53,7 @@ record EngineRuntime(
target,
nextComplex,
nextUpperContext,
nextDimensionStackContext,
effects,
mode,
worldManager,
@@ -22,12 +22,14 @@ import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.engine.EngineRuntime.BiomeMaxes;
import art.arcane.iris.engine.IrisEngine.LifecycleState;
import art.arcane.iris.engine.actuator.IrisDimensionStackActuator;
import art.arcane.iris.engine.framework.EngineEffects;
import art.arcane.iris.engine.framework.EngineEffectsProvider;
import art.arcane.iris.engine.framework.EngineMode;
import art.arcane.iris.engine.framework.EngineTarget;
import art.arcane.iris.engine.framework.EngineWorldManager;
import art.arcane.iris.engine.framework.EngineWorldManagerProvider;
import art.arcane.iris.engine.modifier.IrisCustomModifier;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomePaletteLayer;
import art.arcane.iris.engine.object.IrisDecorator;
@@ -74,6 +76,9 @@ final class EngineRuntimeBuilder {
assembly.complex = new IrisComplex(engine);
IrisLogging.debug("[IrisEngine timing] complex=" + (M.ms() - started) + "ms");
started = M.ms();
assembly.dimensionStackContext = buildDimensionStackContext();
IrisLogging.debug("[IrisEngine timing] buildDimensionStackContext=" + (M.ms() - started) + "ms");
started = M.ms();
assembly.upperContext = buildUpperContext();
IrisLogging.debug("[IrisEngine timing] buildUpperContext=" + (M.ms() - started) + "ms");
started = M.ms();
@@ -91,6 +96,14 @@ final class EngineRuntimeBuilder {
IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData());
assembly.mode.registerStage((x, z, blocks, biomes, multicore, context) ->
staticObjects.apply(engine, x, z, blocks));
if (assembly.dimensionStackContext != null) {
IrisDimensionStackActuator dimensionStack = new IrisDimensionStackActuator(engine);
assembly.mode.registerStage((x, z, blocks, biomes, multicore, context) ->
dimensionStack.actuate(x, z, blocks, multicore, context));
IrisCustomModifier stackCustom = new IrisCustomModifier(engine);
assembly.mode.registerStage((x, z, blocks, biomes, multicore, context) ->
stackCustom.modify(x, z, blocks, multicore, context));
}
IrisLogging.debug("[IrisEngine timing] setupMode=" + (M.ms() - started) + "ms");
started = M.ms();
assembly.worldManager = IrisServices.get(EngineWorldManagerProvider.class).create(engine);
@@ -205,6 +218,21 @@ final class EngineRuntimeBuilder {
return null;
}
DimensionStackContext buildDimensionStackContext() {
IrisDimension dimension = engine.getDimension();
if (!dimension.hasDimensionStack()) {
return null;
}
if (dimension.hasUpperDimension()) {
throw new IllegalStateException("Dimension '" + dimension.getLoadKey()
+ "' cannot enable both dimensionStack and upperDimension.");
}
DimensionStackContext context = DimensionStackContext.create(engine, dimension.getDimensionStack());
IrisLogging.info("Dimension stack enabled: "
+ String.join(" -> ", dimension.getDimensionStack().getDimensions()));
return context;
}
private EngineMode createMode() {
Throwable configuredFailure = null;
try {
@@ -300,6 +328,7 @@ final class EngineRuntimeBuilder {
}
IrisComplex complex;
UpperDimensionContext upperContext;
DimensionStackContext dimensionStackContext;
EngineEffects effects;
EngineMode mode;
EngineWorldManager worldManager;
@@ -319,6 +348,7 @@ final class EngineRuntimeBuilder {
target,
complex,
upperContext,
dimensionStackContext,
effects,
mode,
worldManager,
@@ -501,10 +501,14 @@ public class IrisEngine implements Engine {
}
public void hotloadSilently() {
nativeStructureVolumeMemo.clear();
hotloader.hotloadSilently();
}
void prepareRuntimeHotload() {
nativeStructureVolumeMemo.clear();
platformHooks.prepareRuntimeHotload(this);
}
@Override
public KList<NativeStructureVolume> getNativeStructureVolumes(int minX, int minZ, int maxX, int maxZ) {
if (!nativeStructureVolumeQueriesEnabled.get()) {
@@ -632,6 +636,16 @@ public class IrisEngine implements Engine {
return current == null ? null : current.upperContext();
}
@Override
public DimensionStackContext getDimensionStackContext() {
RuntimeAssembly assembly = runtimeAssembly.get();
if (assembly != null) {
return assembly.dimensionStackContext;
}
EngineRuntime current = runtime;
return current == null ? null : current.dimensionStackContext();
}
@Override
public CompletableFuture<Long> getHash32() {
RuntimeAssembly assembly = runtimeAssembly.get();
@@ -1,311 +1,46 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.image.IrisImageMapRuntime;
import art.arcane.iris.engine.object.InferredType;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisGenerator;
import art.arcane.iris.engine.object.IrisInterpolator;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisShapedGeneratorStyle;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.util.common.data.DataProvider;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.iris.util.project.interpolation.NoiseBounds;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.project.stream.ProceduralStream;
import art.arcane.iris.util.project.stream.interpolation.Interpolated;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Set;
import art.arcane.iris.util.common.data.DataProvider;
public class UpperDimensionContext implements DataProvider {
private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D);
private final IrisDimension dimension;
private final IrisData data;
private final DimensionTerrainContext terrainContext;
private final int chunkHeight;
private final ProceduralStream<Double> heightStream;
private final ProceduralStream<IrisBiome> biomeStream;
private final ProceduralStream<IrisRegion> regionStream;
private final ProceduralStream<PlatformBlockState> rockStream;
private final IrisImageMapRuntime imageMapRuntime;
private final boolean selfReferencing;
private UpperDimensionContext(ContextState state) {
this.dimension = state.dimension();
this.data = state.data();
this.chunkHeight = state.chunkHeight();
this.heightStream = state.heightStream();
this.biomeStream = state.biomeStream();
this.regionStream = state.regionStream();
this.rockStream = state.rockStream();
this.imageMapRuntime = state.imageMapRuntime();
this.selfReferencing = state.selfReferencing();
private UpperDimensionContext(DimensionTerrainContext terrainContext, int chunkHeight) {
this.terrainContext = terrainContext;
this.chunkHeight = chunkHeight;
}
public static UpperDimensionContext create(Engine engine, IrisDimension upperDim) {
boolean selfRef = upperDim.getLoadKey().equals(engine.getDimension().getLoadKey());
int chunkHeight = engine.getHeight();
if (selfRef) {
return createSelfReferencing(engine, chunkHeight);
}
return createCrossReferencing(engine, upperDim, chunkHeight);
}
private static UpperDimensionContext createSelfReferencing(Engine engine, int chunkHeight) {
IrisComplex complex = engine.getComplex();
return new UpperDimensionContext(new ContextState(
engine.getDimension(),
engine.getData(),
chunkHeight,
complex.getNaturalHeightStream(),
complex.getNaturalTrueBiomeStream(),
complex.getRegionStream(),
complex.getRockStream(),
complex.getImageMapRuntime(),
true
));
}
private static UpperDimensionContext createCrossReferencing(Engine engine, IrisDimension upperDim, int chunkHeight) {
IrisData resolvedData = upperDim.getLoader();
if (resolvedData == null) {
resolvedData = engine.getData();
}
IrisData upperData = resolvedData;
IrisImageMapRuntime imageMapRuntime = IrisImageMapRuntime.compile(
upperData,
upperDim,
engine.getMinHeight()
public static UpperDimensionContext create(Engine engine, IrisDimension upperDimension) {
return new UpperDimensionContext(
DimensionTerrainContext.forUpper(engine, upperDimension),
engine.getHeight()
);
long seedOffset = upperDim.getLoadKey().hashCode();
RNG rng = new RNG(engine.getSeedManager().getComplex() ^ seedOffset);
double fluidHeight = upperDim.getFluidHeight();
int cacheSize = IrisSettings.get().getPerformance().getNoiseCacheSize();
DataProvider dataProvider = () -> upperData;
Map<IrisInterpolator, Set<IrisGenerator>> generators = new HashMap<>();
Set<IrisBiome> allBiomes = Collections.newSetFromMap(new IdentityHashMap<>());
Map<IrisBiome, IrisComplex.ChildSelectionPlan> childSelectionPlans =
Collections.synchronizedMap(new IdentityHashMap<>());
upperDim.getRegions().forEach(regionKey -> {
IrisRegion region = upperData.getRegionLoader().load(regionKey);
if (region != null) {
region.getNaturalBiomes(dataProvider).forEach(biome -> registerBiomeGenerators(
biome, dataProvider, allBiomes, generators));
}
});
for (IrisRegion mappedRegion : imageMapRuntime.getMappedRegions()) {
mappedRegion.getNaturalBiomes(dataProvider).forEach(biome -> registerBiomeGenerators(
biome, dataProvider, allBiomes, generators));
}
for (IrisBiome mappedBiome : imageMapRuntime.getMappedBiomes()) {
registerBiomeGenerators(mappedBiome, dataProvider, allBiomes, generators);
}
IrisComplex.GeneratorGroup[] generatorGroups = IrisComplex.freezeGeneratorGroups(generators);
Map<IrisInterpolator, IdentityHashMap<IrisBiome, NoiseBounds>> generatorBounds = new HashMap<>();
for (IrisComplex.GeneratorGroup group : generatorGroups) {
IdentityHashMap<IrisBiome, NoiseBounds> interpolatorBounds = new IdentityHashMap<>(Math.max(allBiomes.size(), 16));
for (IrisBiome biome : allBiomes) {
double min = 0D;
double max = 0D;
for (IrisGenerator gen : group.generators()) {
String key = gen.getLoadKey();
if (key == null || key.isBlank()) {
continue;
}
max += biome.getGenLinkMax(key, engine);
min += biome.getGenLinkMin(key, engine);
}
interpolatorBounds.put(biome, new NoiseBounds(min, max));
}
generatorBounds.put(group.interpolator(), interpolatorBounds);
}
ProceduralStream<Double> regionStyleStream = upperDim.getRegionStyle()
.create(rng.nextParallelRNG(883), upperData).stream()
.zoom(upperDim.getRegionZoom());
ProceduralStream<IrisRegion> proceduralRegionStream = regionStyleStream
.selectRarity(upperData.getRegionLoader().loadAll(upperDim.getRegions()));
ProceduralStream<IrisRegion> regionStream = proceduralRegionStream
.convertAware2D((region, x, z) -> mappedRegion(imageMapRuntime, region, x, z))
.cache2D("upperImageMappedRegionStream", engine, cacheSize);
ProceduralStream<IrisBiome> landBiomeStream = regionStream
.convert(r -> upperDim.getLandBiomeStyle()
.create(rng.nextParallelRNG(InferredType.LAND.ordinal()), upperData).stream()
.zoom(upperDim.getBiomeZoom())
.zoom(upperDim.getLandZoom())
.zoom(r.getLandBiomeZoom())
.selectRarity(loadInferredBiomes(upperData, r.getLandBiomes(), InferredType.LAND)))
.convertAware2D(ProceduralStream::get);
ProceduralStream<IrisBiome> seaBiomeStream = regionStream
.convert(r -> upperDim.getSeaBiomeStyle()
.create(rng.nextParallelRNG(InferredType.SEA.ordinal()), upperData).stream()
.zoom(upperDim.getBiomeZoom())
.zoom(upperDim.getSeaZoom())
.zoom(r.getSeaBiomeZoom())
.selectRarity(loadInferredBiomes(upperData, r.getSeaBiomes(), InferredType.SEA)))
.convertAware2D(ProceduralStream::get);
ProceduralStream<IrisBiome> shoreBiomeStream = regionStream
.convert(r -> upperDim.getShoreBiomeStyle()
.create(rng.nextParallelRNG(InferredType.SHORE.ordinal()), upperData).stream()
.zoom(upperDim.getBiomeZoom())
.zoom(r.getShoreBiomeZoom())
.selectRarity(loadInferredBiomes(upperData, r.getShoreBiomes(), InferredType.SHORE)))
.convertAware2D(ProceduralStream::get);
Map<InferredType, ProceduralStream<IrisBiome>> inferredStreams = new HashMap<>();
inferredStreams.put(InferredType.LAND, landBiomeStream);
inferredStreams.put(InferredType.SEA, seaBiomeStream);
inferredStreams.put(InferredType.SHORE, shoreBiomeStream);
ProceduralStream<InferredType> bridgeStream = upperDim.getContinentalStyle()
.create(rng.nextParallelRNG(234234565), upperData)
.bake().scale(1D / upperDim.getContinentZoom()).bake().stream()
.convert(v -> v >= upperDim.getLandChance() ? InferredType.SEA : InferredType.LAND);
ProceduralStream<IrisBiome> proceduralBaseBiomeStream = bridgeStream
.convertAware2D((t, x, z) -> {
ProceduralStream<IrisBiome> stream = inferredStreams.get(t);
return stream != null ? stream.get(x, z) : inferredStreams.get(InferredType.LAND).get(x, z);
})
.convertAware2D((biome, x, z) -> implode(
biome, x, z, rng, dataProvider, childSelectionPlans, 3));
ProceduralStream<IrisBiome> baseBiomeStream = proceduralBaseBiomeStream
.convertAware2D((biome, x, z) -> mappedBiome(imageMapRuntime, biome, x, z))
.cache2D("upperImageMappedBaseBiomeStream", engine, cacheSize);
KList<IrisShapedGeneratorStyle> overlayNoise = upperDim.getOverlayNoise();
ProceduralStream<Double> overlayStream = overlayNoise.isEmpty()
? ProceduralStream.ofDouble((x, z) -> 0.0D)
: ProceduralStream.ofDouble((x, z) -> {
double value = 0D;
for (IrisShapedGeneratorStyle style : overlayNoise) {
value += style.get(rng, upperData, x, z);
}
return value;
});
long heightSeed = engine.getSeedManager().getHeight() ^ seedOffset;
ProceduralStream<Double> heightStream = ProceduralStream.of((x, z) -> {
IrisBiome b = baseBiomeStream.get(x, z);
if (b == null) {
return mappedTerrainHeight(imageMapRuntime, fluidHeight, x, z);
}
double interpolatedHeight = 0;
for (IrisComplex.GeneratorGroup group : generatorGroups) {
IrisInterpolator interpolator = group.interpolator();
IrisGenerator[] groupGenerators = group.generators();
if (groupGenerators.length == 0) {
continue;
}
IdentityHashMap<IrisBiome, NoiseBounds> cachedBounds = generatorBounds.get(interpolator);
NoiseBounds sampledBounds = interpolator.interpolateBounds(x, z, (xx, zz) -> {
try {
IrisBiome bx = baseBiomeStream.get(xx, zz);
if (bx == null) {
return ZERO_NOISE_BOUNDS;
}
NoiseBounds bounds = cachedBounds != null ? cachedBounds.get(bx) : null;
if (bounds != null) {
return bounds;
}
double bMin = 0D;
double bMax = 0D;
for (IrisGenerator gen : groupGenerators) {
String key = gen.getLoadKey();
if (key == null || key.isBlank()) {
continue;
}
bMax += bx.getGenLinkMax(key, engine);
bMin += bx.getGenLinkMin(key, engine);
}
return new NoiseBounds(bMin, bMax);
} catch (Throwable e) {
IrisLogging.reportError(e);
return ZERO_NOISE_BOUNDS;
}
});
double hi = sampledBounds.max();
double lo = sampledBounds.min();
interpolatedHeight += IrisComplex.averageGeneratorHeights(
groupGenerators,
lo,
hi,
x,
z,
heightSeed + 239945
);
}
double proceduralHeight = Math.max(
Math.min(interpolatedHeight + fluidHeight + overlayStream.get(x, z), chunkHeight),
0D
);
return mappedTerrainHeight(imageMapRuntime, proceduralHeight, x, z);
}, Interpolated.DOUBLE).cache2DDouble("upperImageMappedHeightStream", engine, cacheSize);
ProceduralStream<IrisBiome> finalBiomeStream = heightStream.convertAware2D((height, x, z) -> {
IrisBiome mappedBiome = imageMapRuntime.sampleBiome(x, z);
if (mappedBiome != null) {
return mappedBiome;
}
IrisBiome baseBiome = baseBiomeStream.get(x, z);
IrisBiome resolved = IrisComplex.resolveSurfaceBiome(
height,
baseBiome,
regionStream.get(x, z),
x,
z,
fluidHeight,
landBiomeStream,
seaBiomeStream,
shoreBiomeStream);
return resolved == baseBiome
? baseBiome
: implode(resolved, x, z, rng, dataProvider, childSelectionPlans, 3);
}).cache2D("upperImageMappedFinalBiomeStream", engine, cacheSize);
ProceduralStream<PlatformBlockState> rockStream = upperDim.getRockPalette()
.getLayerGenerator(rng.nextParallelRNG(45), upperData).stream()
.select(upperDim.getRockPalette().getBlockData(upperData));
return new UpperDimensionContext(new ContextState(
upperDim,
upperData,
chunkHeight,
heightStream,
finalBiomeStream,
regionStream,
rockStream,
imageMapRuntime,
false
));
}
private static void registerBiomeGenerators(
IrisBiome biome,
DataProvider dataProvider,
Set<IrisBiome> allBiomes,
Map<IrisInterpolator, Set<IrisGenerator>> generators
) {
allBiomes.add(biome);
biome.getGenerators().forEach(link -> {
IrisGenerator generator = link.getCachedGenerator(dataProvider);
if (generator != null) {
generators.computeIfAbsent(generator.getInterpolator(), key -> new HashSet<>()).add(generator);
}
});
}
static IrisRegion mappedRegion(
@@ -314,8 +49,8 @@ public class UpperDimensionContext implements DataProvider {
double worldX,
double worldZ
) {
IrisRegion mappedRegion = imageMapRuntime.sampleRegion(worldX, worldZ);
return mappedRegion == null ? proceduralRegion : mappedRegion;
return DimensionTerrainContext.mappedRegion(
imageMapRuntime, proceduralRegion, worldX, worldZ);
}
static double mappedTerrainHeight(
@@ -324,7 +59,8 @@ public class UpperDimensionContext implements DataProvider {
double worldX,
double worldZ
) {
return imageMapRuntime.sampleTerrainHeight(worldX, worldZ, proceduralHeight);
return DimensionTerrainContext.mappedTerrainHeight(
imageMapRuntime, proceduralHeight, worldX, worldZ);
}
static IrisBiome mappedBiome(
@@ -333,8 +69,8 @@ public class UpperDimensionContext implements DataProvider {
double worldX,
double worldZ
) {
IrisBiome mappedBiome = imageMapRuntime.sampleBiome(worldX, worldZ);
return mappedBiome == null ? proceduralBiome : mappedBiome;
return DimensionTerrainContext.mappedBiome(
imageMapRuntime, proceduralBiome, worldX, worldZ);
}
static PlatformBlockState mappedSurfaceBlock(
@@ -343,108 +79,40 @@ public class UpperDimensionContext implements DataProvider {
double worldX,
double worldZ
) {
PlatformBlockState mappedBlock = imageMapRuntime.sampleSurfaceBlock(worldX, worldZ);
return mappedBlock == null ? proceduralBlock : mappedBlock;
}
private static KList<IrisBiome> loadInferredBiomes(IrisData data, KList<String> keys, InferredType type) {
KList<IrisBiome> inferred = new KList<>();
for (IrisBiome biome : data.getBiomeLoader().loadAll(keys)) {
inferred.add(biome.withInferredType(type));
}
return inferred;
}
private static IrisBiome implode(
IrisBiome biome,
double x,
double z,
RNG rng,
DataProvider dataProvider,
Map<IrisBiome, IrisComplex.ChildSelectionPlan> childSelectionPlans,
int remainingDepth
) {
if (biome == null || remainingDepth < 0 || biome.getChildren().isEmpty()) {
return biome;
}
IrisComplex.ChildSelectionPlan selectionPlan = childSelectionPlans.get(biome);
if (selectionPlan == null) {
synchronized (childSelectionPlans) {
selectionPlan = childSelectionPlans.get(biome);
if (selectionPlan == null) {
KList<IrisBiome> options = new KList<>();
for (IrisBiome child : biome.getRealChildren(dataProvider)) {
if (child != null) {
options.add(child);
}
}
options.add(biome);
selectionPlan = IrisComplex.ChildSelectionPlan.create(options);
childSelectionPlans.put(biome, selectionPlan);
}
}
}
IrisBiome selected = selectionPlan.select(
biome.getChildrenGenerator(rng, 123, biome.getChildShrinkFactor()), x, z);
if (selected == null) {
return biome;
}
return implode(
selected.withInferredType(biome.getInferredType()),
x,
z,
rng,
dataProvider,
childSelectionPlans,
remainingDepth - 1);
return DimensionTerrainContext.mappedSurfaceBlock(
imageMapRuntime, proceduralBlock, worldX, worldZ);
}
public int getUpperSurfaceY(int x, int z) {
double rawHeight = heightStream.get((double) x, (double) z);
return chunkHeight - 1 - (int) Math.round(rawHeight);
return chunkHeight - 1 - (int) Math.round(terrainContext.getNormalTerrainHeight(x, z));
}
public IrisBiome getUpperBiome(int x, int z) {
return biomeStream.get((double) x, (double) z);
return terrainContext.getBiome(x, z);
}
public IrisRegion getUpperRegion(int x, int z) {
return regionStream == null ? null : regionStream.get((double) x, (double) z);
return terrainContext.getRegion(x, z);
}
public PlatformBlockState getRockBlock(int x, int z) {
return rockStream.get((double) x, (double) z);
return terrainContext.getRockBlock(x, z);
}
public PlatformBlockState getSurfaceBlock(int x, int z) {
return imageMapRuntime.sampleSurfaceBlock(x, z);
return terrainContext.getSurfaceBlock(x, z);
}
public IrisDimension getDimension() {
return dimension;
return terrainContext.getDimension();
}
@Override
public IrisData getData() {
return data;
return terrainContext.getData();
}
public boolean isSelfReferencing() {
return selfReferencing;
}
private record ContextState(
IrisDimension dimension,
IrisData data,
int chunkHeight,
ProceduralStream<Double> heightStream,
ProceduralStream<IrisBiome> biomeStream,
ProceduralStream<IrisRegion> regionStream,
ProceduralStream<PlatformBlockState> rockStream,
IrisImageMapRuntime imageMapRuntime,
boolean selfReferencing
) {
return terrainContext.isSelfReferencing();
}
}
@@ -18,10 +18,13 @@
package art.arcane.iris.engine.actuator;
import art.arcane.iris.engine.DimensionStackContext;
import art.arcane.iris.engine.DimensionStackLayout;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineAssignedActuator;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.util.project.context.ChunkedDataCache;
import art.arcane.volmlib.util.collection.KMap;
@@ -38,6 +41,8 @@ import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBiome;
import java.util.List;
public class IrisBiomeActuator extends EngineAssignedActuator<PlatformBiome> {
private final RNG rng;
private final ChronoLatch cl = new ChronoLatch(5000);
@@ -60,33 +65,167 @@ public class IrisBiomeActuator extends EngineAssignedActuator<PlatformBiome> {
Engine engine = getEngine();
Mantle<Matter> mantle = engine.getMantle().getMantle();
ChunkedDataCache<IrisBiome> biomeCache = context.getBiome();
DimensionStackContext dimensionStackContext = engine.getDimensionStackContext();
for (int xf = 0; xf < width; xf++) {
IrisBiome ib;
for (int zf = 0; zf < depth; zf++) {
ib = biomeCache.get(xf, zf);
String key;
int worldX = x + xf;
int worldZ = z + zf;
IrisBiome biome = biomeCache.get(xf, zf);
ResolvedBiome resolved = resolve(biomeKey(
biome,
getDimension(),
engine,
worldX,
0,
worldZ
));
PlatformBiome platformBiome = resolved.biome();
if (ib.isCustom()) {
IrisBiomeCustom custom = ib.getCustomBiome(rng, engine, x + xf, 0, z + zf);
key = getDimension().getLoadKey() + ":" + custom.getId();
} else {
key = ib.getSkyBiomeKey(rng, engine, x + xf, 0, z + zf);
if (platformBiome != null) {
h.set(xf, 0, zf, xf, height - 1, zf, platformBiome);
}
ResolvedBiome resolved = resolve(key);
PlatformBiome biome = resolved.biome();
if (biome != null) {
h.set(xf, 0, zf, xf, height - 1, zf, biome);
mantle.set(worldX, 0, worldZ, resolved.matter());
if (dimensionStackContext != null) {
PlatformBiome bottomBiome = platformBiome == null
? h.getRaw(xf, 0, zf)
: platformBiome;
applyDimensionStackBiomes(
xf,
zf,
worldX,
worldZ,
h,
mantle,
engine,
new ResolvedBiome(bottomBiome, resolved.matter()),
context.getDimensionStackLayout(xf, zf)
);
}
mantle.set(x + xf, 0, z + zf, resolved.matter());
}
}
engine.getMetrics().getBiome().put(p.getMilliseconds());
}
private void applyDimensionStackBiomes(
int localX,
int localZ,
int worldX,
int worldZ,
Hunk<PlatformBiome> output,
Mantle<Matter> mantle,
Engine engine,
ResolvedBiome bottom,
DimensionStackLayout layout
) {
List<DimensionStackLayout.Layer> layers = layout.layersBottomToTop();
for (int layerIndex = 1; layerIndex < layers.size(); layerIndex++) {
DimensionStackLayout.Layer lower = layers.get(layerIndex - 1);
DimensionStackLayout.Layer layer = layers.get(layerIndex);
int gapMinY = (int) Math.max(0L, (long) lower.contentTopY() + 1L);
int gapMaxY = (int) Math.min(
(long) output.getHeight() - 1L,
(long) layer.localBaseY() - 1L
);
applyBiomeRange(
localX,
localZ,
worldX,
worldZ,
gapMinY,
gapMaxY,
output,
mantle,
bottom
);
if (!layer.visible()) {
continue;
}
ResolvedBiome resolved = bottom;
if (layer.biome() != null) {
IrisDimension dimension = layer.terrainContext().getDimension();
resolved = resolve(biomeKey(
layer.biome(),
dimension,
engine,
worldX,
layer.clippedSurfaceY(),
worldZ
));
}
applyBiomeRange(
localX,
localZ,
worldX,
worldZ,
layer.renderMinY(),
layer.renderMaxY(),
output,
mantle,
resolved
);
}
}
private void applyBiomeRange(
int localX,
int localZ,
int worldX,
int worldZ,
int minimumY,
int maximumY,
Hunk<PlatformBiome> output,
Mantle<Matter> mantle,
ResolvedBiome resolved
) {
if (minimumY > maximumY) {
return;
}
clearBiomeMatterRange(mantle, worldX, worldZ, minimumY, maximumY);
if (resolved.biome() != null) {
output.set(
localX,
minimumY,
localZ,
localX,
maximumY,
localZ,
resolved.biome()
);
}
if (resolved.matter() != null) {
mantle.set(worldX, minimumY, worldZ, resolved.matter());
}
}
static void clearBiomeMatterRange(
Mantle<Matter> mantle,
int worldX,
int worldZ,
int minimumY,
int maximumY
) {
for (int y = minimumY; y <= maximumY; y++) {
mantle.remove(worldX, y, worldZ, MatterBiomeInject.class);
}
}
private String biomeKey(
IrisBiome biome,
IrisDimension dimension,
Engine engine,
int x,
int y,
int z
) {
if (biome.isCustom()) {
IrisBiomeCustom custom = biome.getCustomBiome(rng, engine, x, y, z);
return dimension.getCustomBiomeKey(custom.getId());
}
return biome.getSkyBiomeKey(rng, engine, x, y, z);
}
/**
* The registry lookup and the biome id only depend on the scatter resolved key, so cache the pair
* instead of paying two string keyed lookups per column. Unresolved biomes are never cached, so a
@@ -0,0 +1,258 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine.actuator;
import art.arcane.iris.core.link.Identifier;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.DimensionStackLayout;
import art.arcane.iris.engine.DimensionTerrainContext;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineAssignedActuator;
import art.arcane.iris.engine.framework.TreeBlockMaterial;
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveCell;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.B;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.iris.util.project.matter.TileWrapper;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.documentation.BlockCoordinates;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterCavern;
import art.arcane.volmlib.util.matter.MatterMarker;
import art.arcane.volmlib.util.matter.MatterSlice;
import art.arcane.volmlib.util.matter.MatterStructurePOI;
import art.arcane.volmlib.util.matter.MatterUpdate;
import java.util.ArrayList;
import java.util.List;
public final class IrisDimensionStackActuator extends EngineAssignedActuator<PlatformBlockState> {
private static final PlatformBlockState AIR = B.getState("AIR");
private static final PlatformBlockState BEDROCK = B.getState("BEDROCK");
private static final List<Class<?>> REPLACED_METADATA = List.of(
TileWrapper.class,
Identifier.class,
String.class,
TreeBlockMaterial.class,
MatterCavern.class,
HydrologyCaveCell.class,
MatterMarker.class,
MatterStructurePOI.class,
MatterUpdate.class
);
private final RNG rng;
public IrisDimensionStackActuator(Engine engine) {
super(engine, "Dimension Stack");
rng = new RNG(engine.getSeedManager().getTerrain());
}
@BlockCoordinates
@Override
public void onActuate(
int x,
int z,
Hunk<PlatformBlockState> blocks,
boolean multicore,
ChunkContext context
) {
if (getEngine().getDimensionStackContext() == null) {
return;
}
MantleChunk<Matter> mantleChunk = getEngine().getMantle().getMantle()
.getChunk(x >> 4, z >> 4).use();
try {
MetadataCleaner metadata = new MetadataCleaner(mantleChunk, blocks.getHeight());
for (int localX = 0; localX < blocks.getWidth(); localX++) {
int worldX = x + localX;
for (int localZ = 0; localZ < blocks.getDepth(); localZ++) {
renderLayers(
localX,
localZ,
worldX,
z + localZ,
blocks,
context.getDimensionStackLayout(localX, localZ),
metadata
);
}
}
} finally {
mantleChunk.release();
}
}
private void renderLayers(
int localX,
int localZ,
int worldX,
int worldZ,
Hunk<PlatformBlockState> blocks,
DimensionStackLayout layout,
MetadataCleaner metadata
) {
if (layout == null) {
return;
}
List<DimensionStackLayout.Layer> layers = layout.layersBottomToTop();
for (int layerIndex = 1; layerIndex < layers.size(); layerIndex++) {
DimensionStackLayout.Layer layer = layers.get(layerIndex);
clearGap(localX, localZ, blocks, layers.get(layerIndex - 1), layer, metadata);
if (layer.visible()) {
renderLayer(localX, localZ, worldX, worldZ, blocks, layer, metadata);
}
}
}
private void clearGap(
int localX,
int localZ,
Hunk<PlatformBlockState> output,
DimensionStackLayout.Layer lower,
DimensionStackLayout.Layer upper,
MetadataCleaner metadata
) {
int minimumY = (int) Math.max(0L, (long) lower.contentTopY() + 1L);
int maximumY = (int) Math.min(
(long) output.getHeight() - 1L,
(long) upper.localBaseY() - 1L
);
for (int y = minimumY; y <= maximumY; y++) {
metadata.clear(localX, y, localZ);
output.setRaw(localX, y, localZ, AIR);
}
}
private void renderLayer(
int localX,
int localZ,
int worldX,
int worldZ,
Hunk<PlatformBlockState> output,
DimensionStackLayout.Layer layer,
MetadataCleaner metadata
) {
DimensionTerrainContext terrainContext = layer.terrainContext();
IrisDimension dimension = terrainContext.getDimension();
IrisData data = terrainContext.getData();
IrisBiome biome = layer.biome();
int surfaceDepth = Math.max(0, layer.normalTerrainHeight());
int fluidDepth = Math.max(0, layer.fluidHeight() - layer.normalTerrainHeight());
KList<PlatformBlockState> surfaceBlocks = biome == null
? null
: biome.generateLayersWithSlope(
dimension,
worldX,
worldZ,
rng,
surfaceDepth,
layer.normalTerrainHeight(),
data,
terrainContext.getSlopeStream()
);
KList<PlatformBlockState> seaBlocks = biome == null || fluidDepth == 0
? null
: biome.generateSeaLayers(worldX, worldZ, rng, fluidDepth, data);
for (int y = layer.renderMaxY(); y >= layer.renderMinY(); y--) {
int sourceY = y - layer.localBaseY();
if (sourceY == 0 && dimension.isBedrock()) {
writeBlock(output, metadata, localX, y, localZ, BEDROCK);
continue;
}
if (sourceY > layer.normalTerrainHeight() && sourceY <= layer.fluidHeight()) {
int fluidLayerDepth = layer.fluidY() - y;
writeBlock(output, metadata, localX, y, localZ, HydrologyFluidLayerSelector.select(
seaBlocks,
fluidLayerDepth,
layer.fluidBlock(),
false
));
continue;
}
if (sourceY > layer.normalTerrainHeight()) {
continue;
}
int depth = layer.surfaceY() - y;
if (depth == 0 && layer.surfaceBlock() != null) {
writeBlock(output, metadata, localX, y, localZ, layer.surfaceBlock());
} else if (surfaceBlocks != null && surfaceBlocks.hasIndex(depth)) {
writeBlock(output, metadata, localX, y, localZ, surfaceBlocks.get(depth));
} else {
writeBlock(output, metadata, localX, y, localZ, layer.rockBlock());
}
}
}
private void writeBlock(
Hunk<PlatformBlockState> output,
MetadataCleaner metadata,
int x,
int y,
int z,
PlatformBlockState block
) {
metadata.clear(x, y, z);
output.setRaw(x, y, z, block);
}
static final class MetadataCleaner {
private final MantleChunk<Matter> chunk;
private final List<MatterSlice<?>>[] slicesBySection;
@SuppressWarnings("unchecked")
MetadataCleaner(MantleChunk<Matter> chunk, int height) {
this.chunk = chunk;
slicesBySection = (List<MatterSlice<?>>[]) new List<?>[(height + 15) >> 4];
}
void clear(int x, int y, int z) {
int sectionIndex = y >> 4;
List<MatterSlice<?>> slices = slicesBySection[sectionIndex];
if (slices == null) {
slices = resolveSlices(sectionIndex);
slicesBySection[sectionIndex] = slices;
}
for (MatterSlice<?> slice : slices) {
slice.set(x, y & 15, z, null);
}
}
private List<MatterSlice<?>> resolveSlices(int sectionIndex) {
Matter section = chunk.get(sectionIndex);
if (section == null) {
return List.of();
}
ArrayList<MatterSlice<?>> slices = new ArrayList<>(REPLACED_METADATA.size());
for (Class<?> type : REPLACED_METADATA) {
MatterSlice<?> slice = section.getSlice(type);
if (slice != null) {
slices.add(slice);
}
}
return List.copyOf(slices);
}
}
}
@@ -28,6 +28,9 @@ import art.arcane.iris.core.nms.container.BlockPos;
import art.arcane.iris.core.nms.container.Pair;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.UpperDimensionContext;
import art.arcane.iris.engine.DimensionStackContext;
import art.arcane.iris.engine.DimensionStackLayout;
import art.arcane.iris.engine.DimensionTerrainContext;
import art.arcane.iris.engine.data.chunk.TerrainChunk;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.object.IrisBiome;
@@ -90,6 +93,10 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
return null;
}
default @Nullable DimensionStackContext getDimensionStackContext() {
return null;
}
EngineMode getMode();
EnginePlatformHooks getPlatformHooks();
@@ -255,6 +262,25 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
@BlockCoordinates
default IrisRegion getRegion(int x, int z) {
DimensionStackContext dimensionStackContext = getDimensionStackContext();
if (dimensionStackContext != null) {
DimensionStackLayout.Layer layer = dimensionStackContext.getLayout(x, z).surfaceLayer();
if (layer != null && layer.region() != null) {
return layer.region();
}
}
return getComplex().getRegionStream().get(x, z);
}
@BlockCoordinates
default IrisRegion getRegion(int x, int y, int z) {
DimensionStackContext dimensionStackContext = getDimensionStackContext();
if (dimensionStackContext != null) {
DimensionStackLayout.Layer layer = dimensionStackContext.getLayout(x, z).layerAt(y);
if (layer != null && layer.region() != null) {
return layer.region();
}
}
return getComplex().getRegionStream().get(x, z);
}
@@ -301,7 +327,45 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
@BlockCoordinates
default IrisBiome getCaveBiome(int x, int y, int z, IrisDimensionCarvingResolver.State state) {
IrisBiome surfaceBiome = getSurfaceBiome(x, z);
IrisBiome configuredBiome = resolveConfiguredCaveBiome(x, y, z, state);
if (configuredBiome != null) {
return configuredBiome;
}
boolean naturalFallback = answersFromNaturalTerrain(x, z);
DimensionStackContext dimensionStackContext = getDimensionStackContext();
IrisBiome surfaceBiome = naturalFallback || dimensionStackContext == null
? naturalFallback
? getComplex().naturalSurfaceBiome(x, z)
: getSurfaceBiome(x, z)
: getComplex().getTrueBiomeStream().get(x, z);
int surfaceY = naturalFallback
? getComplex().naturalTrueHeight(x, z)
: getComplex().getHeightStream().get(x, z).intValue();
return resolveDepthCaveBiome(x, y, z, surfaceBiome, surfaceY);
}
@BlockCoordinates
default IrisBiome getCaveBiome(
int x,
int y,
int z,
IrisDimensionCarvingResolver.State state,
IrisBiome surfaceBiome,
int surfaceY
) {
IrisBiome configuredBiome = resolveConfiguredCaveBiome(x, y, z, state);
if (configuredBiome != null) {
return configuredBiome;
}
return resolveDepthCaveBiome(x, y, z, surfaceBiome, surfaceY);
}
private IrisBiome resolveConfiguredCaveBiome(
int x,
int y,
int z,
IrisDimensionCarvingResolver.State state
) {
int worldY = y + getWorld().minHeight();
IrisDimensionCarvingEntry rootCarvingEntry = IrisDimensionCarvingResolver.resolveRootEntry(this, worldY, state);
if (rootCarvingEntry != null) {
@@ -311,13 +375,21 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
return resolvedCarvingBiome;
}
}
return null;
}
private IrisBiome resolveDepthCaveBiome(
int x,
int y,
int z,
IrisBiome surfaceBiome,
int surfaceY
) {
IrisBiome caveBiome = getCaveBiome(x, z);
if (caveBiome == null) {
return surfaceBiome;
}
int surfaceY = getComplex().getHeightStream().get(x, z).intValue();
int depthBelowSurface = surfaceY - y;
if (depthBelowSurface <= 0) {
return surfaceBiome;
@@ -333,6 +405,18 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
@BlockCoordinates
default IrisBiome getSurfaceBiome(int x, int z) {
DimensionStackContext dimensionStackContext = getDimensionStackContext();
if (dimensionStackContext != null) {
DimensionStackLayout.Layer layer = dimensionStackContext.getLayout(x, z).surfaceLayer();
if (layer != null && layer.biome() != null) {
return layer.biome();
}
}
return getHostSurfaceBiome(x, z);
}
@BlockCoordinates
default IrisBiome getHostSurfaceBiome(int x, int z) {
if (answersFromNaturalTerrain(x, z)) {
return getComplex().naturalSurfaceBiome(x, z);
}
@@ -364,6 +448,12 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
@BlockCoordinates
default int getHeight(int x, int z, boolean ignoreFluid) {
DimensionStackContext dimensionStackContext = getDimensionStackContext();
if (dimensionStackContext != null) {
return ignoreFluid
? dimensionStackContext.getStackTerrainHeight(x, z)
: dimensionStackContext.getStackTopHeight(x, z);
}
if (answersFromNaturalTerrain(x, z)) {
int natural = getComplex().naturalTrueHeight(x, z);
return ignoreFluid ? natural : Math.max(natural, getMantle().getFluidHeight());
@@ -371,6 +461,24 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
return getMantle().getHighest(x, z, getData(), ignoreFluid);
}
@BlockCoordinates
static int hostHeight(Engine engine, int x, int z, boolean ignoreFluid) {
EngineMantle mantle = engine.getMantle();
if (mantle == null) {
return engine.getHeight(x, z, ignoreFluid);
}
if (engine.answersFromNaturalTerrain(x, z)) {
int naturalHeight = engine.getComplex().naturalTrueHeight(x, z);
return ignoreFluid
? naturalHeight
: Math.max(naturalHeight, mantle.getFluidHeight());
}
int terrainHeight = mantle.trueHeight(x, z);
return ignoreFluid
? terrainHeight
: Math.max(terrainHeight, mantle.getFluidHeight(x, z));
}
@BlockCoordinates
@Override
default void catchBlockUpdates(int x, int y, int z, PlatformBlockState data) {
@@ -410,6 +518,15 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
IrisDimension dim = getDimension();
dim.getReachableBiomes(this).forEach((i) -> v.put(i.getLoadKey(), i));
DimensionStackContext dimensionStackContext = getDimensionStackContext();
if (dimensionStackContext != null) {
for (DimensionTerrainContext terrainContext : dimensionStackContext.getLayersBottomToTop()) {
if (!terrainContext.isSelfReferencing()) {
terrainContext.getDimension().getReachableBiomes(terrainContext)
.forEach((i) -> v.put(i.getLoadKey(), i));
}
}
}
return v.v();
}
@@ -454,6 +571,22 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
boolean isStudio();
default IrisBiome getBiome(int x, int y, int z) {
DimensionStackContext dimensionStackContext = getDimensionStackContext();
if (dimensionStackContext != null) {
DimensionStackLayout layout = dimensionStackContext.getLayout(x, z);
IrisBiome stackedBiome = dimensionStackBiome(layout, y);
if (stackedBiome != null) {
return stackedBiome;
}
DimensionStackLayout.Layer bottomLayer = layout.layersBottomToTop().get(0);
if (y > bottomLayer.renderMaxY()) {
return bottomLayer.biome();
}
if (y <= bottomLayer.clippedSurfaceY() - 2) {
return getCaveBiome(x, y, z);
}
return bottomLayer.biome();
}
if (y <= getHeight(x, z) - 2) {
return getCaveBiome(x, y, z);
}
@@ -462,6 +595,22 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
}
default IrisBiome getBiomeOrMantle(int x, int y, int z) {
DimensionStackContext dimensionStackContext = getDimensionStackContext();
if (dimensionStackContext != null) {
DimensionStackLayout layout = dimensionStackContext.getLayout(x, z);
IrisBiome stackedBiome = dimensionStackBiome(layout, y);
if (stackedBiome != null) {
return stackedBiome;
}
DimensionStackLayout.Layer bottomLayer = layout.layersBottomToTop().get(0);
if (y > bottomLayer.renderMaxY()) {
return bottomLayer.biome();
}
if (y <= bottomLayer.clippedSurfaceY() - 2) {
return getCaveOrMantleBiome(x, y, z);
}
return bottomLayer.biome();
}
if (y <= getHeight(x, z) - 2) {
return getCaveOrMantleBiome(x, y, z);
}
@@ -469,6 +618,12 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
return getSurfaceBiome(x, z);
}
private static IrisBiome dimensionStackBiome(DimensionStackLayout layout, int y) {
DimensionStackLayout.Layer bottom = layout.layersBottomToTop().get(0);
DimensionStackLayout.Layer owner = layout.layerAt(y);
return owner == bottom ? null : owner.biome();
}
default String getObjectPlacementKey(int x, int y, int z) {
PlacedObject o = getObjectPlacement(x, y, z);
@@ -524,7 +679,7 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
}
IrisRegion region = getRegion(x, z);
IrisRegion region = getComplex().getRegionStream().get(x, z);
for (IrisObjectPlacement i : region.getObjects()) {
if (i.compatPlace(getData()).contains(object)) {
@@ -532,7 +687,7 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
}
}
IrisBiome biome = getSurfaceBiome(x, z);
IrisBiome biome = getComplex().getTrueBiomeStream().get(x, z);
for (IrisObjectPlacement i : biome.getObjects()) {
if (i.compatPlace(getData()).contains(object)) {
@@ -151,7 +151,16 @@ public interface EngineMode extends Staged {
default void generate(int x, int z, Hunk<PlatformBlockState> blocks, Hunk<PlatformBiome> biomes, boolean multicore, long generationSessionId) {
boolean cacheContext = !getEngine().getPlatformHooks().shouldDisableChunkContextCache(getEngine());
ChunkContext.PrefillPlan prefillPlan = cacheContext ? ChunkContext.PrefillPlan.NO_CAVE : ChunkContext.PrefillPlan.NONE;
ChunkContext ctx = new ChunkContext(x, z, getComplex(), generationSessionId, cacheContext, prefillPlan, getEngine().getMetrics());
ChunkContext ctx = new ChunkContext(
x,
z,
getComplex(),
generationSessionId,
cacheContext,
prefillPlan,
getEngine().getMetrics(),
getEngine().getDimensionStackContext()
);
EngineStage[] stages = getStages().toArray(new EngineStage[0]);
try (IrisContext.Scope chunkScope = IrisContext.open(getEngine(), generationSessionId, ctx)) {
@@ -46,6 +46,9 @@ public interface EnginePlatformHooks {
default void validateDimensionHotload(Engine engine, IrisDimension replacement) {
}
default void prepareRuntimeHotload(Engine engine) {
}
default void applyWorldBoundary(Engine engine) {
}
@@ -19,6 +19,8 @@
package art.arcane.iris.engine.framework;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.engine.DimensionStackContext;
import art.arcane.iris.engine.DimensionTerrainContext;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.object.IrisBiome;
@@ -282,6 +284,13 @@ public final class HintedLocator<T> implements Locator<T> {
if (engine.getFocus() != null || engine.getFocusRegion() != null) {
return SearchPlan.unpruned();
}
if (engine.getDimensionStackContext() != null) {
for (IrisBiome biome : engine.getAllBiomes()) {
if (biome != null && biomeKey.equals(biome.getLoadKey())) {
return SearchPlan.unpruned();
}
}
}
IrisComplex complex = engine.getComplex();
BiomeSource source = new BiomeSource(engine);
@@ -380,6 +389,17 @@ public final class HintedLocator<T> implements Locator<T> {
return SearchPlan.unpruned();
}
DimensionStackContext stackContext = engine.getDimensionStackContext();
if (stackContext != null) {
for (DimensionTerrainContext terrainContext : stackContext.getLayersTopToBottom()) {
for (IrisRegion region : terrainContext.getDimension().getAllRegions(terrainContext)) {
if (region != null && regionKey.equals(region.getLoadKey())) {
return SearchPlan.unpruned();
}
}
}
}
if (!engine.getDimension().getRegions().contains(regionKey)) {
return SearchPlan.impossible();
}
@@ -655,7 +655,7 @@ public final class IrisStructureLocator {
return null;
}
if (placement.resolvedAnchor() == IrisStructureAnchorMode.SURFACE) {
int surfaceY = engine.getHeight(originX, originZ, true) + engine.getMinHeight();
int surfaceY = Engine.hostHeight(engine, originX, originZ, true) + engine.getMinHeight();
return surfaceY < placement.getMinHeight() || surfaceY > placement.getMaxHeight() ? null : surfaceY;
}
int bandMin = Math.max(worldMin, Math.min(placement.getMinHeight(), placement.getMaxHeight()));
@@ -766,7 +766,7 @@ public final class IrisStructureLocator {
if (surfaceHeights.size() >= MAX_BURIAL_COLUMNS) {
return Integer.MIN_VALUE;
}
surfaceY = engine.getHeight(x, z, true) + engine.getMinHeight();
surfaceY = Engine.hostHeight(engine, x, z, true) + engine.getMinHeight();
surfaceHeights.put(columnKey, surfaceY);
}
int allowedTopY = surfaceY - UNDERGROUND_SURFACE_CLEARANCE;
@@ -142,7 +142,7 @@ public final class NativeStructurePlacementPlanner {
return null;
}
if (!placement.isUnderground()) {
int surfaceY = engine.getHeight(blockX, blockZ, true) + engine.getMinHeight();
int surfaceY = Engine.hostHeight(engine, blockX, blockZ, true) + engine.getMinHeight();
if (surfaceY < placement.getMinHeight() || surfaceY > placement.getMaxHeight()) {
return null;
}
@@ -160,7 +160,7 @@ public final class NativeStructurePlacementPlanner {
int localFluidHeight = engine.getComplex() == null
? engine.getDimension().getFluidHeight()
: (int) Math.round(engine.getComplex().getRiverWaterSurfaceStream().get(blockX, blockZ));
return engine.getHeight(blockX, blockZ, true) < localFluidHeight;
return Engine.hostHeight(engine, blockX, blockZ, true) < localFluidHeight;
}
private static int comparePlacementPriority(IrisStructurePlacement left, IrisStructurePlacement right) {
@@ -21,6 +21,8 @@ package art.arcane.iris.engine.mantle;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.DimensionStackContext;
import art.arcane.iris.engine.DimensionStackLayout;
import art.arcane.iris.engine.UpperDimensionContext;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineTarget;
@@ -36,6 +38,7 @@ import art.arcane.iris.util.common.data.B;
import art.arcane.volmlib.util.documentation.BlockCoordinates;
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
@@ -196,17 +199,37 @@ public interface EngineMantle extends MatterGenerator {
}
@ChunkCoordinates
default void insertMatter(int x, int z, Hunk<PlatformBlockState> blocks, boolean multicore) {
default void insertMatter(
int x,
int z,
Hunk<PlatformBlockState> blocks,
boolean multicore,
ChunkContext context
) {
if (!getEngine().getDimension().isUseMantle()) {
return;
}
UpperDimensionContext upperCtx = getEngine().getUpperContext();
boolean protectUpper = upperCtx != null;
DimensionStackContext stackContext = getEngine().getDimensionStackContext();
MantleChunk<Matter> chunk = getMantle().getChunk(x, z).use();
try {
if (protectUpper) {
if (stackContext != null) {
DimensionStackLayout[] layouts = new DimensionStackLayout[256];
for (int i = 0; i < layouts.length; i++) {
int localX = i >> 4;
int localZ = i & 15;
layouts[i] = context.getDimensionStackLayout(localX, localZ);
}
chunk.iterate(PlatformBlockState.class, (localX, y, localZ, value) -> {
DimensionStackLayout layout = layouts[(localX << 4) | (localZ & 15)];
if (!layout.isHostFeatureProtectedY(y)) {
blocks.set(localX, y, localZ, value);
}
});
} else if (protectUpper) {
int chunkBlockX = x << 4;
int chunkBlockZ = z << 4;
int gap = getEngine().getDimension().getUpperDimensionGap();
@@ -227,7 +227,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
if (cached != CACHE_MISS) {
return cached;
}
int surfaceY = engine.getHeight(x, z, true);
int surfaceY = Engine.hostHeight(engine, x, z, true);
surfaceHeights.put(key, surfaceY);
return surfaceY;
}
@@ -286,7 +286,7 @@ public class IrisCaveCarver3D {
if (precomputedSurfaceHeights != null && precomputedSurfaceHeights.length > index) {
columnSurfaceY = precomputedSurfaceHeights[index];
} else {
columnSurfaceY = engine.getHeight(x, z);
columnSurfaceY = Engine.hostHeight(engine, x, z, true);
}
int unclampedClearanceTopY = columnSurfaceY - surfaceClearance;
int clearanceTopY = Math.min(maxY, Math.max(minY, unclampedClearanceTopY));
@@ -189,7 +189,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
int zzz = 8 + (z << 4);
IrisRegion region = complex.getRegionStream().get(xxx, zzz);
IrisBiome surfaceBiome = complex.getTrueBiomeStream().get(xxx, zzz);
int surfaceY = getEngineMantle().getEngine().getHeight(xxx, zzz, true);
int surfaceY = complex.getRoundedHeighteightStream().get(xxx, zzz);
IrisBiome caveBiome = resolveCaveObjectBiome(xxx, zzz, surfaceY, surfaceBiome);
if (IrisSettings.get().getGeneral().isDebug() && (x & 31) == 0 && (z & 31) == 0) {
int carvedBlocks = 0;
@@ -948,7 +948,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
}
Engine engine = getEngineMantle().getEngine();
IrisBiome at = engine.getCaveBiome(x, y, z);
IrisBiome surface = engine.getSurfaceBiome(x, z);
IrisBiome surface = engine.getComplex().getTrueBiomeStream().get(x, z);
return caveAnchorBiomeConflicts(at, surface, expectedCaveBiomeKey);
}
@@ -1631,8 +1631,10 @@ public class MantleObjectComponent extends IrisMantleComponent {
public Set<String> guess(int x, int z) {
// todo The guess doesnt bring into account that the placer may return -1
RNG rng = applyNoise(x, z, Cache.key(x, z) + seed());
IrisBiome biome = getEngineMantle().getEngine().getSurfaceBiome((x << 4) + 8, (z << 4) + 8);
IrisRegion region = getEngineMantle().getEngine().getRegion((x << 4) + 8, (z << 4) + 8);
int blockX = (x << 4) + 8;
int blockZ = (z << 4) + 8;
IrisBiome biome = getComplex().getTrueBiomeStream().get(blockX, blockZ);
IrisRegion region = getComplex().getRegionStream().get(blockX, blockZ);
Set<String> v = new KSet<>();
for (IrisObjectPlacement i : biome.getSurfaceObjects()) {
if (rng.chance(i.getChance() + rng.d(-0.005, 0.005)) && !i.isCompatExcluded(getData())) {
@@ -1879,7 +1881,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
return surfaceY;
}
surfaceY = getEngineMantle().getEngine().getHeight(x, z);
surfaceY = getEngineMantle().trueHeight(x, z);
surfaceHeights.put(columnKey, surfaceY);
return surfaceY;
}
@@ -27,8 +27,8 @@ import art.arcane.iris.engine.framework.IrisEngineMode;
public class ModeEnclosure extends IrisEngineMode implements EngineMode {
public ModeEnclosure(Engine engine) {
super(engine);
var terrain = new IrisTerrainNormalActuator(getEngine());
var biome = new IrisBiomeActuator(getEngine());
IrisTerrainNormalActuator terrain = new IrisTerrainNormalActuator(getEngine());
IrisBiomeActuator biome = new IrisBiomeActuator(getEngine());
registerStage(burst(
(x, z, k, p, m, c) -> terrain.actuate(x, z, k, m, c),
@@ -27,8 +27,8 @@ import art.arcane.iris.engine.framework.IrisEngineMode;
public class ModeIslands extends IrisEngineMode implements EngineMode {
public ModeIslands(Engine engine) {
super(engine);
var terrain = new IrisTerrainNormalActuator(getEngine());
var biome = new IrisBiomeActuator(getEngine());
IrisTerrainNormalActuator terrain = new IrisTerrainNormalActuator(getEngine());
IrisBiomeActuator biome = new IrisBiomeActuator(getEngine());
registerStage(burst(
(x, z, k, p, m, c) -> terrain.actuate(x, z, k, m, c),
@@ -87,7 +87,7 @@ public class ModeOverworld extends IrisEngineMode implements EngineMode {
if (shouldBypassMantleStages()) {
return;
}
getMantle().insertMatter(x >> 4, z >> 4, K, m);
getMantle().insertMatter(x >> 4, z >> 4, K, m, c);
};
EngineStage sFloatingTerrainSolid = (x, z, k, p, m, c) -> floatingChildBiomes.modify(x, z, k, m, c);
EngineStage sFloatingDecorate = (x, z, k, p, m, c) -> floatingChildBiomes.decorateColumns(x, z, k, m, c);
@@ -118,7 +118,9 @@ public class ModeOverworld extends IrisEngineMode implements EngineMode {
registerStage(sDecorant);
registerStage(sFloatingDecorate);
registerStage(sPerfection);
registerStage(sCustom);
if (getEngine().getDimensionStackContext() == null) {
registerStage(sCustom);
}
}
private boolean shouldBypassMantleStages() {
@@ -27,8 +27,8 @@ import art.arcane.iris.engine.framework.IrisEngineMode;
public class ModeSuperFlat extends IrisEngineMode implements EngineMode {
public ModeSuperFlat(Engine engine) {
super(engine);
var terrain = new IrisTerrainNormalActuator(getEngine());
var biome = new IrisBiomeActuator(getEngine());
IrisTerrainNormalActuator terrain = new IrisTerrainNormalActuator(getEngine());
IrisBiomeActuator biome = new IrisBiomeActuator(getEngine());
registerStage(burst(
(x, z, k, p, m, c) -> terrain.actuate(x, z, k, m, c),
@@ -19,6 +19,8 @@
package art.arcane.iris.engine.modifier;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.DimensionStackContext;
import art.arcane.iris.engine.DimensionStackLayout;
import art.arcane.iris.engine.UpperDimensionContext;
import art.arcane.iris.engine.actuator.IrisDecorantActuator;
import art.arcane.iris.engine.framework.Engine;
@@ -91,6 +93,8 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
UpperDimensionContext upperCtx = getEngine().getUpperContext();
boolean protectUpper = upperCtx != null && !getEngine().getDimension().isUpperDimensionCarving();
int[] upperSurfaceHeights = protectUpper ? scratch.getOrCreateUpperSurfaceHeights() : null;
DimensionStackContext stackContext = getEngine().getDimensionStackContext();
DimensionStackLayout[] stackLayouts = stackContext == null ? null : new DimensionStackLayout[256];
int chunkBlockX = PowerOfTwoCoordinates.chunkToBlock(x);
int chunkBlockZ = PowerOfTwoCoordinates.chunkToBlock(z);
for (int columnIndex = 0; columnIndex < 256; columnIndex++) {
@@ -104,6 +108,9 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
int gap = getEngine().getDimension().getUpperDimensionGap();
upperSurfaceHeights[columnIndex] = Math.max(rawUpper, surfaceHeights[columnIndex] + gap);
}
if (stackLayouts != null) {
stackLayouts[columnIndex] = context.getDimensionStackLayout(localX, localZ);
}
}
MantleChunk<Matter> mantleChunk = mantle.getChunk(x, z).use();
@@ -117,6 +124,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
scratch,
columnMasks,
upperSurfaceHeights,
stackLayouts,
worldHeightSpan,
caveLavaHeight,
chunkBlockX,
@@ -348,6 +356,10 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
if (context.upperSurfaceHeights() != null && y >= context.upperSurfaceHeights()[columnIndex]) {
return;
}
if (context.stackLayouts() != null
&& context.stackLayouts()[columnIndex].isHostFeatureProtectedY(y)) {
return;
}
PlatformBlockState current = context.output().getRaw(localX, y, localZ);
if (hydrology != null && hydrology.action() == HydrologyCaveAction.SEAL_GUARD) {
@@ -405,6 +417,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
IrisCarveScratch scratch,
CarveColumnMask[] columnMasks,
int[] upperSurfaceHeights,
DimensionStackLayout[] stackLayouts,
int worldHeightSpan,
int caveLavaHeight,
int chunkBlockX,
@@ -20,6 +20,7 @@ package art.arcane.iris.engine.modifier;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineAssignedModifier;
import art.arcane.iris.engine.DimensionStackLayout;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDepositGenerator;
import art.arcane.iris.engine.object.IrisDepositHeightDistribution;
@@ -186,6 +187,11 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
continue;
}
DimensionStackLayout stackLayout = context.getDimensionStackLayout(nx, nz);
if (stackLayout != null && stackLayout.isHostFeatureProtectedY(ny)) {
continue;
}
PlatformBlockState current = data.get(nx, ny, nz);
if (!canReplaceDepositTarget(current)) {
continue;
@@ -20,6 +20,7 @@ package art.arcane.iris.engine.modifier;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.DimensionStackLayout;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.decorator.FloatingDecorator;
import art.arcane.iris.engine.decorator.IrisSeaSurfaceDecorator;
@@ -285,7 +286,15 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Platf
}
}
writeIslandSkyBiomes(parent, wx, wz, sample, chunkHeight, data);
writeIslandSkyBiomes(
parent,
wx,
wz,
sample,
chunkHeight,
data,
context.getDimensionStackLayout(xf, zf)
);
}
}
@@ -367,7 +376,15 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Platf
}
}
private void writeIslandSkyBiomes(IrisBiome parent, int wx, int wz, FloatingIslandSample sample, int chunkHeight, IrisData data) {
private void writeIslandSkyBiomes(
IrisBiome parent,
int wx,
int wz,
FloatingIslandSample sample,
int chunkHeight,
IrisData data,
DimensionStackLayout stackLayout
) {
try {
IdentityHashMap<IrisFloatingChildBiomes, MatterBiomeInject> matterByEntry = new IdentityHashMap<>();
for (int k = 0; k <= sample.topIdx; k++) {
@@ -378,6 +395,9 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Platf
if (y < 0 || y >= chunkHeight) {
continue;
}
if (!shouldWriteHostBiomeMarker(stackLayout, y)) {
continue;
}
IrisFloatingChildBiomes entry = sample.entryAt(k);
MatterBiomeInject matter = matterByEntry.get(entry);
if (matter == null) {
@@ -395,10 +415,15 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Platf
}
}
static boolean shouldWriteHostBiomeMarker(DimensionStackLayout stackLayout, int y) {
return stackLayout == null || !stackLayout.isHostFeatureProtectedY(y);
}
private MatterBiomeInject createSkyBiomeMatter(IrisBiome target, int wx, int wz) {
if (target.isCustom()) {
IrisBiomeCustom custom = target.getCustomBiome(rng, getEngine(), wx, 0, wz);
return BiomeInjectMatter.get(IrisPlatforms.get().biomeWriter().biomeIdFor(getDimension().getLoadKey() + ":" + custom.getId()));
return BiomeInjectMatter.get(IrisPlatforms.get().biomeWriter().biomeIdFor(
getDimension().getCustomBiomeKey(custom.getId())));
}
return BiomeInjectMatter.get(IrisPlatforms.get().biomeWriter().biomeIdFor(target.getSkyBiomeKey(rng, getEngine(), wx, 0, wz)));
@@ -40,6 +40,7 @@ import art.arcane.iris.util.common.data.DataProvider;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.iris.util.project.stream.ProceduralStream;
import lombok.AccessLevel;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -495,6 +496,10 @@ public class IrisBiome extends IrisRegistrant implements IRare {
return IrisBiomeLayerGenerator.generateLayers(this, dim, wx, wz, random, maxDepth, height, rdata, complex);
}
public KList<PlatformBlockState> generateLayersWithSlope(IrisDimension dim, double wx, double wz, RNG random, int maxDepth, int height, IrisData rdata, ProceduralStream<Double> slopeStream) {
return IrisBiomeLayerGenerator.generateLayers(this, dim, wx, wz, random, maxDepth, height, rdata, slopeStream);
}
public KList<PlatformBlockState> generateCeilingLayers(IrisDimension dim, double wx, double wz, RNG random, int maxDepth, int height, IrisData rdata, IrisComplex complex) {
return IrisBiomeLayerGenerator.generateCeilingLayers(this, dim, wx, wz, random, maxDepth, height, rdata, complex);
}
@@ -26,6 +26,7 @@ import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.B;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.iris.util.project.stream.ProceduralStream;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
@@ -42,8 +43,16 @@ final class IrisBiomeLayerGenerator {
}
static KList<PlatformBlockState> generateLayers(IrisBiome biome, IrisDimension dim, double wx, double wz, RNG random, int maxDepth, int height, IrisData rdata, IrisComplex complex) {
return generateLayers(biome, dim, wx, wz, random, maxDepth, height, rdata, complex, null);
}
static KList<PlatformBlockState> generateLayers(IrisBiome biome, IrisDimension dim, double wx, double wz, RNG random, int maxDepth, int height, IrisData rdata, ProceduralStream<Double> slopeStream) {
return generateLayers(biome, dim, wx, wz, random, maxDepth, height, rdata, null, slopeStream);
}
private static KList<PlatformBlockState> generateLayers(IrisBiome biome, IrisDimension dim, double wx, double wz, RNG random, int maxDepth, int height, IrisData rdata, IrisComplex complex, ProceduralStream<Double> slopeStream) {
if (biome.isLockLayers()) {
return generateLockedLayers(biome, wx, wz, random, maxDepth, height, rdata, complex);
return generateLockedLayers(biome, wx, wz, random, maxDepth, height, rdata, complex, slopeStream);
}
KList<PlatformBlockState> data = new KList<>();
@@ -70,7 +79,7 @@ final class IrisBiomeLayerGenerator {
IrisSlopeClip sc = layer.getSlopeCondition();
if (!sc.isDefault()) {
if (!sc.isValid(complex.getSlopeStream().get(wx, wz))) {
if (!sc.isValid(resolveSlopeStream(complex, slopeStream).getDouble(wx, wz))) {
d = 0;
}
}
@@ -168,6 +177,10 @@ final class IrisBiomeLayerGenerator {
}
static KList<PlatformBlockState> generateLockedLayers(IrisBiome biome, double wx, double wz, RNG random, int maxDepthf, int height, IrisData rdata, IrisComplex complex) {
return generateLockedLayers(biome, wx, wz, random, maxDepthf, height, rdata, complex, null);
}
private static KList<PlatformBlockState> generateLockedLayers(IrisBiome biome, double wx, double wz, RNG random, int maxDepthf, int height, IrisData rdata, IrisComplex complex, ProceduralStream<Double> slopeStream) {
KList<PlatformBlockState> data = new KList<>();
KList<PlatformBlockState> real = new KList<>();
int maxDepth = Math.min(maxDepthf, biome.getLockLayersMax());
@@ -190,7 +203,7 @@ final class IrisBiomeLayerGenerator {
IrisSlopeClip sc = layer.getSlopeCondition();
if (!sc.isDefault()) {
if (!sc.isValid(complex.getSlopeStream().get(wx, wz))) {
if (!sc.isValid(resolveSlopeStream(complex, slopeStream).getDouble(wx, wz))) {
d = 0;
}
}
@@ -222,6 +235,13 @@ final class IrisBiomeLayerGenerator {
return real;
}
private static ProceduralStream<Double> resolveSlopeStream(
IrisComplex complex,
ProceduralStream<Double> slopeStream
) {
return slopeStream == null ? complex.getSlopeStream() : slopeStream;
}
static KList<PlatformBlockState> generateSeaLayers(IrisBiome biome, double wx, double wz, RNG random, int maxDepth, IrisData rdata) {
KList<PlatformBlockState> data = new KList<>();
@@ -222,6 +222,8 @@ public class IrisDimension extends IrisRegistrant {
private boolean upperDimensionObjects = false;
@Desc("When true, upper-dimension objects force-place regardless of placement restrictions (slope, underwater, clamp, collisions, carving). Normal dimension objects always place first; upper objects place second and may clip or occlude lower-dimension placements when this is enabled.")
private boolean upperObjectsForcePlace = false;
@Desc("Stack dimension terrain upright in declared top-to-bottom order. The final dimension key must reference this dimension.")
private IrisDimensionStack dimensionStack = null;
@RegistryListResource(IrisBiome.class)
@Desc("Keep this either undefined or empty. Setting any biome name into this will force iris to only generate the specified biome. Great for testing.")
private String focus = "";
@@ -528,6 +530,12 @@ public class IrisDimension extends IrisRegistrant {
}
}
}
if (hasFocusRegion()) {
IrisRegion focus = data.getRegionLoader().load(getFocusRegion());
if (focus != null && !focus.isCompatExcluded()) {
regions.put(getFocusRegion(), focus);
}
}
return regions.v();
}
@@ -564,6 +572,7 @@ public class IrisDimension extends IrisRegistrant {
}
Deque<String> pending = new ArrayDeque<>();
addReachableBiomeKey(pending, getFocus());
IrisHydrology configuredHydrology = getHydrology();
IrisRiverHydrology configuredRivers = configuredHydrology == null ? null : configuredHydrology.getRivers();
boolean hydrologyPoliciesActive = configuredRivers != null && configuredRivers.isEnabled();
@@ -721,8 +730,14 @@ public class IrisDimension extends IrisRegistrant {
}
public void installBiomes(IDataFixer fixer, DataProvider data, KList<File> datapackRoots, KSet<String> biomes) throws IOException {
String namespace = getLoadKey().toLowerCase(Locale.ROOT);
installBiomes(fixer, data, datapackRoots, namespace, "", biomes);
String keyPrefix = getCustomBiomeKeyPrefix();
int separator = keyPrefix.indexOf(':');
String namespace = keyPrefix.substring(0, separator);
String pathPrefix = keyPrefix.substring(separator + 1);
if (pathPrefix.endsWith("/")) {
pathPrefix = pathPrefix.substring(0, pathPrefix.length() - 1);
}
installBiomes(fixer, data, datapackRoots, namespace, pathPrefix, biomes);
}
public void installBiomes(IDataFixer fixer, DataProvider data, KList<File> datapackRoots,
@@ -747,18 +762,19 @@ public class IrisDimension extends IrisRegistrant {
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
String customBiomeId = customBiome.getId();
String customBiomePath = sanitizeRegistryPath(customBiomeId, "biome");
String json = customBiome.generateJson(fixer, contentGate);
synchronized (biomes) {
if (!biomes.add(customBiomeId)) {
if (!biomes.add(customBiomePath)) {
IrisLogging.debug("Duplicate Data Pack Biome: " + getLoadKey() + "/" + customBiomeId);
continue;
}
}
String biomePath = pathPrefix.isBlank()
? customBiomeId
: pathPrefix + "/" + customBiomeId;
? customBiomePath
: pathPrefix + "/" + customBiomePath;
for (File datapackRoot : datapackRoots) {
File output = new File(datapackRoot, "data/" + namespace + "/worldgen/biome/" + biomePath + ".json");
@@ -900,6 +916,28 @@ public class IrisDimension extends IrisRegistrant {
return sanitizeDimensionTypeKeyValue(getLoadKey());
}
public String getCustomBiomeKey(String customBiomeId) {
return customBiomeKey(getLoadKey(), customBiomeId);
}
public String getCustomBiomeKeyPrefix() {
return customBiomeKeyPrefix(getLoadKey());
}
public static String customBiomeKey(String dimensionLoadKey, String customBiomeId) {
return customBiomeKeyPrefix(dimensionLoadKey) + sanitizeRegistryPath(customBiomeId, "biome");
}
private static String customBiomeKeyPrefix(String dimensionLoadKey) {
String dimensionPath = sanitizeRegistryPath(dimensionLoadKey, "dimension");
int separator = dimensionPath.indexOf('/');
if (separator < 0) {
return sanitizeRegistryNamespace(dimensionPath) + ":";
}
String namespace = sanitizeRegistryNamespace(dimensionPath.substring(0, separator));
return namespace + ":" + dimensionPath.substring(separator + 1) + "/";
}
public static String sanitizeDimensionTypeKeyValue(String value) {
if (value == null || value.isBlank()) {
return "dimension";
@@ -918,6 +956,26 @@ public class IrisDimension extends IrisRegistrant {
return sanitized.isBlank() ? "dimension" : sanitized;
}
private static String sanitizeRegistryNamespace(String value) {
String sanitized = value.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9_.-]", "_");
return sanitized.isBlank() ? "dimension" : sanitized;
}
private static String sanitizeRegistryPath(String value, String fallback) {
if (value == null || value.isBlank()) {
return fallback;
}
String sanitized = value.trim().toLowerCase(Locale.ROOT).replace("\\", "/");
sanitized = sanitized.replaceAll("[^a-z0-9/._-]", "_");
sanitized = sanitized.replaceAll("/+", "/");
sanitized = sanitized.replaceAll("^/+", "");
sanitized = sanitized.replaceAll("/+$", "");
while (sanitized.contains("..")) {
sanitized = sanitized.replace("..", "_");
}
return sanitized.isBlank() ? fallback : sanitized;
}
public IrisDimensionType getDimensionType() {
IrisDimensionTypeOptions options = getDimensionOptions();
if (fullbright) {
@@ -930,6 +988,10 @@ public class IrisDimension extends IrisRegistrant {
return upperDimension != null && !upperDimension.isEmpty() && !upperDimension.equalsIgnoreCase("none");
}
public boolean hasDimensionStack() {
return dimensionStack != null;
}
public void installDimensionType(IDataFixer fixer, KList<File> datapackRoots) throws IOException {
IrisDimensionType type = getDimensionType();
String json = type.toJson(fixer);
@@ -0,0 +1,50 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.object.annotations.ArrayType;
import art.arcane.iris.engine.object.annotations.Desc;
import art.arcane.iris.engine.object.annotations.MaxNumber;
import art.arcane.iris.engine.object.annotations.MinNumber;
import art.arcane.iris.engine.object.annotations.RegistryListResource;
import art.arcane.iris.engine.object.annotations.Required;
import art.arcane.volmlib.util.collection.KList;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Accessors(chain = true)
@NoArgsConstructor
@Desc("Stacks dimension terrain upright in declared top-to-bottom order")
@Data
public class IrisDimensionStack {
@Required
@ArrayType(min = 2, type = String.class)
@RegistryListResource(IrisDimension.class)
@Desc("Dimension load keys ordered from the highest layer to the lowest layer. The final key must be this dimension.")
private KList<String> dimensions = new KList<>();
@MinNumber(0)
@MaxNumber(256)
@Desc("Nominal air gap in blocks between adjacent dimension layers")
private int spacer = 32;
@Desc("Optional noise that blends adjacent layer boundaries. Omit this object for a constant configured gap.")
private IrisDimensionStackBlend blend = null;
}
@@ -0,0 +1,40 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.object.annotations.Desc;
import art.arcane.iris.engine.object.annotations.MaxNumber;
import art.arcane.iris.engine.object.annotations.MinNumber;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Accessors(chain = true)
@NoArgsConstructor
@Desc("Controls noise blending at each dimension stack boundary")
@Data
public class IrisDimensionStackBlend {
@MinNumber(0)
@MaxNumber(256)
@Desc("Maximum vertical boundary displacement in blocks")
private int amplitude = 8;
@Desc("Noise style sampled independently for each adjacent layer boundary")
private IrisGeneratorStyle style = NoiseStyle.SIMPLEX.style();
}
@@ -553,11 +553,15 @@ public final class EngineBukkitOps {
}
public static IrisRegion getRegion(Engine engine, Location l) {
return engine.getRegion(l.getBlockX(), l.getBlockZ());
return engine.getRegion(
l.getBlockX(), l.getBlockY() - engine.getWorld().minHeight(), l.getBlockZ());
}
public static IrisBiome getBiomeOrMantle(Engine engine, Location l) {
return engine.getBiomeOrMantle(l.getBlockX(), l.getBlockY(), l.getBlockZ());
return engine.getBiomeOrMantle(
l.getBlockX(),
l.getBlockY() - engine.getWorld().minHeight(),
l.getBlockZ());
}
public static IrisBiome getSurfaceBiome(Engine engine, Chunk c) {
@@ -1,5 +1,7 @@
package art.arcane.iris.util.project.context;
import art.arcane.iris.engine.DimensionStackContext;
import art.arcane.iris.engine.DimensionStackLayout;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.EngineMetrics;
@@ -27,6 +29,8 @@ public class ChunkContext {
private final ChunkedDataCache<PlatformBlockState> fluid;
private final ChunkedDataCache<IrisRegion> region;
private final FloatingIslandBoundarySampler floatingIslandBoundarySampler;
private final DimensionStackContext dimensionStackContext;
private final DimensionStackLayout[] dimensionStackLayouts;
public ChunkContext(int x, int z, IrisComplex complex) {
this(x, z, complex, 0L, true, PrefillPlan.NO_CAVE, null);
@@ -45,6 +49,19 @@ public class ChunkContext {
}
public ChunkContext(int x, int z, IrisComplex complex, long generationSessionId, boolean cache, PrefillPlan prefillPlan, EngineMetrics metrics) {
this(x, z, complex, generationSessionId, cache, prefillPlan, metrics, null);
}
public ChunkContext(
int x,
int z,
IrisComplex complex,
long generationSessionId,
boolean cache,
PrefillPlan prefillPlan,
EngineMetrics metrics,
DimensionStackContext dimensionStackContext
) {
this.x = x;
this.z = z;
this.complex = complex;
@@ -57,6 +74,8 @@ public class ChunkContext {
this.fluid = new ChunkedDataCache<>(complex.getFluidStream(), x, z, cache);
this.region = new ChunkedDataCache<>(complex.getRegionStream(), x, z, cache);
this.floatingIslandBoundarySampler = new FloatingIslandBoundarySampler((wx, wz) -> complex.getTrueBiomeStream().get(wx, wz));
this.dimensionStackContext = dimensionStackContext;
this.dimensionStackLayouts = dimensionStackContext == null ? null : new DimensionStackLayout[256];
if (cache) {
PrefillPlan resolvedPlan = prefillPlan == null ? PrefillPlan.NO_CAVE : prefillPlan;
@@ -165,6 +184,19 @@ public class ChunkContext {
return region;
}
public DimensionStackLayout getDimensionStackLayout(int localX, int localZ) {
if (dimensionStackContext == null) {
return null;
}
int index = (localZ << 4) | localX;
DimensionStackLayout layout = dimensionStackLayouts[index];
if (layout == null) {
layout = dimensionStackContext.sample(x + localX, z + localZ);
dimensionStackLayouts[index] = layout;
}
return layout;
}
public enum PrefillPlan {
ALL(true, true, true, true, true, true),
NO_CAVE(true, true, false, true, true, true),
@@ -1,8 +1,10 @@
package art.arcane.iris.core;
import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration.IrisGeneratorBinding;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.nms.datapack.DataVersion;
import art.arcane.iris.core.nms.datapack.v1217.DataFixerV1217;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.json.JSONObject;
import org.junit.Rule;
@@ -88,6 +90,26 @@ public class IrisDatapackCompilerTest {
assertTrue(Files.isRegularFile(datapackRoot.resolve("pack.mcmeta")));
}
@Test
public void compilesRecursiveDimensionBiomeToCanonicalRegistryPath() throws Exception {
Path packRoot = temporaryFolder.newFolder("recursive-dimension-pack").toPath();
Path datapackRoot = temporaryFolder.newFolder("recursive-dimension-datapack").toPath();
createPack(packRoot, "layers/sky", "Aurora");
IrisDatapackCompiler.compile(
List.of(packRoot.toFile()),
new KList<File>().qadd(datapackRoot.toFile()),
List.of(),
new DataFixerV1217(),
false
);
assertTrue(Files.isRegularFile(
datapackRoot.resolve("data/layers/worldgen/biome/sky/aurora.json")));
assertTrue(Files.isRegularFile(
datapackRoot.resolve("data/iris/dimension_type/layers_sky.json")));
}
@Test
public void collectsInstalledPacksThroughSymbolicLinkWorkspace() throws Exception {
Path dataDirectory = temporaryFolder.newFolder("linked-data").toPath();
@@ -252,6 +274,57 @@ public class IrisDatapackCompilerTest {
assertFalse(ServerConfigurator.loadedRegistrySatisfies(loaded, changedBiomeTags));
}
@Test
public void stackRuntimeRequirementsIncludeSourceDimensionRegistries() throws Exception {
Path pack = temporaryFolder.newFolder("registry-stack-pack").toPath();
createPack(pack, "host", "stack_custom", "NORMAL");
Files.writeString(
pack.resolve("dimensions/host.json"),
"""
{
"name": "Host",
"environment": "NORMAL",
"logicalHeight": 256,
"dimensionHeight": {"min": -64, "max": 320},
"dimensionStack": {
"dimensions": ["layers/source", "host"],
"spacer": 8
}
}
""",
StandardCharsets.UTF_8
);
Files.createDirectories(pack.resolve("dimensions/layers"));
Files.writeString(
pack.resolve("dimensions/layers/source.json"),
"""
{
"name": "Source",
"environment": "NORMAL",
"logicalHeight": 256,
"dimensionHeight": {"min": -64, "max": 320}
}
""",
StandardCharsets.UTF_8
);
IrisData data = IrisData.openDatapackCompiler(pack.toFile());
try {
IrisDimension host = data.getDimensionLoader().load("host");
Map<String, String> requirements = IrisDatapackCompiler.computeRegistryRequirements(
host,
new DataFixerV1217()
);
assertTrue(requirements.containsKey("dimension_type/iris:host"));
assertTrue(requirements.containsKey("dimension_type/iris:layers_source"));
assertTrue(requirements.containsKey("worldgen/biome/host:stack_custom"));
assertTrue(requirements.containsKey("worldgen/biome/layers:source/stack_custom"));
} finally {
data.close();
}
}
@Test
public void rejectsBindingToMissingDimension() throws Exception {
Path packRoot = temporaryFolder.newFolder("missing-binding-pack").toPath();
@@ -325,6 +398,7 @@ public class IrisDatapackCompilerTest {
) throws Exception {
Files.createDirectories(root.resolve("dimensions"));
Files.createDirectories(root.resolve("biomes"));
Files.createDirectories(root.resolve("dimensions").resolve(dimensionKey).getParent());
Files.writeString(
root.resolve("dimensions").resolve(dimensionKey + ".json"),
"""
@@ -80,6 +80,7 @@ public class ServerConfiguratorRegistryVerificationTest {
IrisDimension dimension = mock(IrisDimension.class);
when(dimension.getAllBiomes(any())).thenReturn(new KList<>(biome));
when(dimension.getLoadKey()).thenReturn("overworld");
when(dimension.getCustomBiomeKey("missing")).thenReturn("overworld:missing");
when(dimension.getLoadFile()).thenReturn(
new File("packs/overworld/dimensions/overworld.json"));
when(dimension.getDimensionTypeKey()).thenReturn("iris:overworld");
@@ -1,5 +1,7 @@
package art.arcane.iris.core.pack;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisDimensionStack;
import art.arcane.iris.engine.object.IrisObjectMarker;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisStaticObject;
@@ -9,6 +11,7 @@ import org.junit.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@@ -63,6 +66,38 @@ public class PackExportClosureTest {
PackExportClosure.collectStaticObjectKeys(placements).stream().sorted().toList());
}
@Test
public void collectsRootAndStackedDimensionKeys() {
IrisDimension dimension = new IrisDimension();
dimension.setLoadKey("main");
dimension.setDimensionStack(new IrisDimensionStack()
.setDimensions(new KList<>("sky", "middle", "main", "sky")));
assertEquals(List.of("main", "middle", "sky"),
PackExportClosure.collectDimensionKeys(dimension).stream().sorted().toList());
}
@Test
public void collectsTransitiveStackDimensionsWithoutLooping() {
IrisDimension main = dimension("main", "sky", "main");
IrisDimension sky = dimension("sky", "cloud", "sky");
IrisDimension cloud = dimension("cloud", "main", "cloud");
Map<String, IrisDimension> dimensions = Map.of(
"main", main,
"sky", sky,
"cloud", cloud);
assertEquals(List.of("cloud", "main", "sky"),
PackExportClosure.collectDimensionKeys(main, dimensions::get).stream().sorted().toList());
}
private static IrisDimension dimension(String key, String... stackKeys) {
IrisDimension dimension = new IrisDimension();
dimension.setLoadKey(key);
dimension.setDimensionStack(new IrisDimensionStack().setDimensions(new KList<>(stackKeys)));
return dimension;
}
/**
* Source guard: both packagers must export the ambient-spawning graph. Spawner and marker
* folders were silently omitted from exports, leaving dangling entitySpawners references.
@@ -75,10 +110,16 @@ public class PackExportClosureTest {
assertTrue("Bukkit packager must write markers/", bukkit.contains("\"markers/\""));
assertTrue("Bukkit packager must export region objects alongside biome objects",
bukkit.contains("regions.forEach((r) -> allPlacements.addAll(r.getObjects()))"));
assertTrue("Bukkit packager must export static objects", bukkit.contains("dimension.getStaticObjects()"));
assertTrue("Bukkit packager must export static objects",
bukkit.contains("exportedDimension.getStaticObjects()"));
assertTrue("Bukkit obfuscation must rewrite static object references",
bukkit.contains("placement.setObject(renameObjects.get(placement.getObject()))"));
assertTrue("Bukkit packager must export entity loot tables", bukkit.contains("getLoot().getTables()"));
assertTrue("Bukkit packager must include dimension-stack resources",
bukkit.contains("PackExportClosure.collectDimensionKeys(dimension)"));
assertTrue("Bukkit packager must export expression-backed stack blend styles",
bukkit.contains("dm.getExpressionLoader().getPossibleKeys()")
&& bukkit.contains("\"expressions/\""));
int bukkitValidation = bukkit.indexOf("PackValidator.validateForPackaging(project.getPath())");
assertTrue("Bukkit packager must validate before opening or mutating package state",
bukkitValidation >= 0
@@ -92,7 +133,14 @@ public class PackExportClosureTest {
assertTrue("modded packager must include initial spawns", modded.contains("getInitialSpawns"));
assertTrue("modded packager must export region objects", modded.contains("region.getObjects()"));
assertTrue("modded packager must export static objects",
modded.contains("PackExportClosure.collectStaticObjectKeys(dimension.getStaticObjects())"));
modded.contains("PackExportClosure.collectStaticObjectKeys(exportedDimension.getStaticObjects())"));
assertTrue("modded packager must include dimension-stack resources",
modded.contains("PackExportClosure.collectDimensionKeys(dimension)"));
assertTrue("modded packager must export expression-backed stack blend styles",
modded.contains("dm.getExpressionLoader().getPossibleKeys()")
&& modded.contains("\"expressions\""));
assertTrue("modded packager must retain referenced style snippets",
modded.contains("copyStyleSnippets(packFolder, folder)"));
int moddedValidation = modded.indexOf("PackValidator.validateForPackaging(packFolder)");
assertTrue("modded packager must validate before mutating package state",
moddedValidation >= 0 && moddedValidation < modded.indexOf("IO.delete(folder)"));
@@ -317,6 +317,48 @@ public class PackImageMapValidatorTest {
"Dimension 'upper' image-map 'terrain' source footprint does not cover"));
}
@Test
public void validatesStackLayerCoverageAgainstHostBoundary() throws Exception {
File pack = createPack("stack-host-boundary", """
{
"regions": ["region"],
"worldBoundary": {"center": {"x": 0, "z": 0}, "size": 16},
"dimensionStack": {
"dimensions": ["upper", "upper", "main"],
"spacer": 24
}
}
""");
write(pack, "dimensions/upper.json", """
{
"regions": ["region"],
"worldBoundary": {"center": {"x": 0, "z": 0}, "size": 2},
"imageMaps": [
{"key": "terrain", "map": "upper-terrain", "application": "TERRAIN_HEIGHT"}
]
}
""");
writeGray(pack, "upper-terrain", 4, 4);
write(pack, "image-maps/upper-terrain.json", """
{
"source": "upper-terrain",
"type": "GRAYSCALE_HEIGHT",
"origin": {"x": -2, "z": -2},
"outOfBounds": "ERROR"
}
""");
PackImageMapValidator.Validation validation = validate(pack, false);
assertTrue(validation.errors().toString(), contains(validation.errors(),
"Dimension 'main' dimension stack layer 'upper' image-map 'terrain' source footprint does not cover"));
assertEquals(1L, validation.errors().stream()
.filter(error -> error.contains("Dimension 'main' dimension stack layer 'upper'"))
.count());
assertFalse(validation.errors().toString(), contains(validation.errors(),
"Dimension 'upper' image-map 'terrain' source footprint does not cover"));
}
@Test
public void acceptsUpperDimensionWithoutStandaloneBoundaryWhenParentIsCovered() throws Exception {
File pack = createPack("upper-parent-only-boundary", """
@@ -0,0 +1,260 @@
package art.arcane.iris.core.pack;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class PackValidatorDimensionStackTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void acceptsCanonicalTopToBottomStack() throws Exception {
PackValidationResult result = validate("""
{
"regions": ["region"],
"dimensionStack": {
"dimensions": ["sky", "main"],
"spacer": 24,
"blend": {
"amplitude": 8,
"style": {"style": "SIMPLEX", "zoom": 128}
}
}
}
""");
assertTrue(result.getBlockingErrors().toString(), result.isLoadable());
}
@Test
public void rejectsMissingBlendStyleSnippet() throws Exception {
PackValidationResult result = validate("""
{
"regions": ["region"],
"dimensionStack": {
"dimensions": ["sky", "main"],
"blend": {"style": "snippet/style/missing"}
}
}
""");
assertFalse(result.isLoadable());
assertTrue(contains(result, "references missing style snippet 'snippet/style/missing'"));
}
@Test
public void rejectsWrongBlendSnippetType() throws Exception {
PackValidationResult result = validate("""
{
"regions": ["region"],
"dimensionStack": {
"dimensions": ["sky", "main"],
"blend": {"style": "snippet/generator/stack-blend"}
}
}
""");
assertFalse(result.isLoadable());
assertTrue(contains(result, "blend.style must be an object or style snippet reference"));
}
@Test
public void rejectsMissingLayerAndWrongLowestDimension() throws Exception {
PackValidationResult result = validate("""
{
"regions": ["region"],
"dimensionStack": {
"dimensions": ["missing", "sky"],
"spacer": 24
}
}
""");
assertFalse(result.isLoadable());
assertTrue(contains(result, "dimensions[0] references missing dimension 'missing'"));
assertTrue(contains(result, "dimensions must end with its host dimension key 'main'"));
}
@Test
public void rejectsUpperDimensionAndUnsafeNumericValues() throws Exception {
PackValidationResult result = validate("""
{
"regions": ["region"],
"upperDimension": "sky",
"dimensionStack": {
"dimensions": ["sky", "main"],
"spacer": 24.5,
"blend": {
"amplitude": 257,
"style": {}
}
}
}
""");
assertFalse(result.isLoadable());
assertTrue(contains(result, "dimensionStack cannot be used with upperDimension"));
assertTrue(contains(result, "spacer must be an integer between 0 and 256"));
assertTrue(contains(result, "amplitude must be an integer between 0 and 256"));
}
@Test
public void rejectsShortStackAndMalformedBlendStyle() throws Exception {
PackValidationResult result = validate("""
{
"regions": ["region"],
"dimensionStack": {
"dimensions": ["main"],
"blend": {"style": 42}
}
}
""");
assertFalse(result.isLoadable());
assertTrue(contains(result, "dimensions must contain at least two dimension keys"));
assertTrue(contains(result, "blend.style must be an object or style snippet reference"));
}
@Test
public void acceptsBlendStyleSnippetReference() throws Exception {
PackValidationResult result = validate("""
{
"regions": ["region"],
"dimensionStack": {
"dimensions": ["sky", "main"],
"blend": {"style": "snippet/style/stack-blend"}
}
}
""", "{\"style\":\"SIMPLEX\",\"zoom\":128}");
assertTrue(result.getBlockingErrors().toString(), result.isLoadable());
}
@Test
public void rejectsMalformedBlendStyleSnippet() throws Exception {
PackValidationResult result = validate("""
{
"regions": ["region"],
"dimensionStack": {
"dimensions": ["sky", "main"],
"blend": {"style": "snippet/style/stack-blend"}
}
}
""", "{");
assertFalse(result.isLoadable());
assertTrue(contains(result, "references invalid style snippet 'snippet/style/stack-blend'"));
}
@Test
public void acceptsNestedDimensionKeys() throws Exception {
File pack = temporaryFolder.newFolder("nested-pack");
write(pack, "dimensions/root/main.json", """
{
"regions": ["region"],
"dimensionStack": {
"dimensions": ["sky/clouds", "root/main"]
}
}
""");
write(pack, "dimensions/sky/clouds.json", "{\"regions\":[\"region\"]}");
write(pack, "regions/region.json", "{\"landBiomes\":[\"biome\"]}");
write(pack, "biomes/biome.json", "{\"name\":\"Biome\"}");
PackValidationResult result = PackValidator.validateForPackaging(pack);
assertTrue(result.getBlockingErrors().toString(), result.isLoadable());
}
@Test
public void acceptsEmptyBlendWithDefaults() throws Exception {
PackValidationResult result = validate("""
{
"regions": ["region"],
"dimensionStack": {
"dimensions": ["sky", "main"],
"blend": {}
}
}
""");
assertTrue(result.getBlockingErrors().toString(), result.isLoadable());
}
@Test
public void acceptsRepeatedNonHostDimensions() throws Exception {
PackValidationResult result = validate("""
{
"regions": ["region"],
"dimensionStack": {
"dimensions": ["sky", "sky", "main"]
}
}
""");
assertTrue(result.getBlockingErrors().toString(), result.isLoadable());
}
@Test
public void rejectsDuplicateHostDimension() throws Exception {
PackValidationResult result = validate("""
{
"regions": ["region"],
"dimensionStack": {
"dimensions": ["main", "sky", "main"]
}
}
""");
assertFalse(result.isLoadable());
assertTrue(contains(result, "host dimension key 'main' exactly once"));
}
@Test
public void rejectsExplicitNullConfiguration() throws Exception {
PackValidationResult result = validate("""
{
"regions": ["region"],
"dimensionStack": null
}
""");
assertFalse(result.isLoadable());
assertTrue(contains(result, "dimensionStack must be an object"));
}
private PackValidationResult validate(String mainDimension) throws Exception {
return validate(mainDimension, null);
}
private PackValidationResult validate(String mainDimension, String styleSnippet) throws Exception {
File pack = temporaryFolder.newFolder("pack-" + System.nanoTime());
write(pack, "dimensions/main.json", mainDimension);
write(pack, "dimensions/sky.json", "{\"regions\":[\"region\"]}");
write(pack, "regions/region.json", "{\"landBiomes\":[\"biome\"]}");
write(pack, "biomes/biome.json", "{\"name\":\"Biome\"}");
if (styleSnippet != null) {
write(pack, "snippet/style/stack-blend.json", styleSnippet);
}
return PackValidator.validateForPackaging(pack);
}
private static boolean contains(PackValidationResult result, String fragment) {
return result.getBlockingErrors().stream().anyMatch(error -> error.contains(fragment));
}
private static void write(File root, String relative, String content) throws Exception {
Path target = new File(root, relative).toPath();
Files.createDirectories(target.getParent());
Files.writeString(target, content, StandardCharsets.UTF_8);
}
}
@@ -0,0 +1,108 @@
package art.arcane.iris.core.project;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.IrisRegistrant;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisDimensionStack;
import art.arcane.iris.engine.object.IrisDimensionStackBlend;
import art.arcane.iris.engine.object.annotations.ArrayType;
import art.arcane.iris.engine.object.annotations.RegistryListResource;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import com.google.gson.Gson;
import org.junit.Test;
import java.lang.reflect.Field;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisDimensionStackSchemaTest {
@Test
public void dimensionExposesNullableStackConfiguration() throws Exception {
IrisDimension dimension = new IrisDimension();
Field stackField = IrisDimension.class.getDeclaredField("dimensionStack");
Field dimensionsField = IrisDimensionStack.class.getDeclaredField("dimensions");
assertNull(dimension.getDimensionStack());
assertFalse(dimension.hasDimensionStack());
dimension.setDimensionStack(new IrisDimensionStack());
assertTrue(dimension.hasDimensionStack());
assertEquals(IrisDimensionStack.class, stackField.getType());
assertEquals(2, dimensionsField.getAnnotation(ArrayType.class).min());
assertEquals(IrisDimension.class,
dimensionsField.getAnnotation(RegistryListResource.class).value());
}
@Test
public void schemaCarriesStackOrderAndNumericBounds() {
JSONObject schema = new SchemaBuilder(IrisDimensionStack.class, schemaData()).construct();
JSONObject properties = schema.getJSONObject("properties");
JSONObject dimensions = properties.getJSONObject("dimensions");
JSONObject spacer = properties.getJSONObject("spacer");
JSONObject blend = referencedSchema(schema, properties.getJSONObject("blend"));
JSONObject blendProperties = blend.getJSONObject("properties");
JSONObject amplitude = blendProperties.getJSONObject("amplitude");
assertEquals("array", dimensions.getString("type"));
assertEquals(2, dimensions.getInt("minItems"));
assertEquals(0, spacer.getInt("minimum"));
assertEquals(256, spacer.getInt("maximum"));
assertEquals(0, amplitude.getInt("minimum"));
assertEquals(256, amplitude.getInt("maximum"));
assertTrue(contains(schema.getJSONArray("required"), "dimensions"));
assertFalse(blend.has("required"));
}
@Test
public void gsonUsesTheCanonicalNestedShape() {
IrisDimensionStack stack = new IrisDimensionStack()
.setDimensions(new KList<>("sky", "main"))
.setSpacer(24)
.setBlend(new IrisDimensionStackBlend().setAmplitude(8));
JSONObject json = new JSONObject(new Gson().toJson(stack));
assertEquals(2, json.getJSONArray("dimensions").length());
assertEquals("main", json.getJSONArray("dimensions").getString(1));
assertEquals(24, json.getInt("spacer"));
assertEquals(8, json.getJSONObject("blend").getInt("amplitude"));
assertTrue(json.getJSONObject("blend").getJSONObject("style").has("style"));
}
private static JSONObject referencedSchema(JSONObject schema, JSONObject property) {
String key = property.getString("$ref").substring("#/definitions/".length());
return schema.getJSONObject("definitions").getJSONObject(key);
}
private static boolean contains(JSONArray values, String expected) {
for (int index = 0; index < values.length(); index++) {
if (expected.equals(values.optString(index, null))) {
return true;
}
}
return false;
}
@SuppressWarnings("unchecked")
private static IrisData schemaData() {
IrisData data = mock(IrisData.class);
ResourceLoader<IrisDimension> dimensionLoader = mock(ResourceLoader.class);
KMap<Class<? extends IrisRegistrant>, ResourceLoader<? extends IrisRegistrant>> loaders = new KMap<>();
loaders.put(IrisDimension.class, dimensionLoader);
when(data.getLoaders()).thenReturn(loaders);
when(dimensionLoader.getFolderName()).thenReturn("dimensions");
when(dimensionLoader.getResourceTypeName()).thenReturn("Dimension");
when(dimensionLoader.getPossibleKeys()).thenReturn(new String[]{"sky", "main"});
when(data.getPossibleSnippets(anyString())).thenReturn(new KList<>());
return data;
}
}
@@ -86,6 +86,46 @@ public class IrisProjectCopierTest {
assertTrue(Files.isRegularFile(target.resolve("dimensions/the_nether.json")));
}
@Test
public void rewritesDimensionStackReferencesAcrossCopiedDimensionsWhenRenaming() throws Exception {
Path workspace = temporaryFolder.newFolder("dimension-stack-reference").toPath();
Path source = workspace.resolve("template-pack");
Files.createDirectories(source.resolve("dimensions"));
Files.writeString(source.resolve("dimensions/overworld.json"), """
{
"name": "Source",
"dimensionStack": {
"dimensions": ["layers/sky", "overworld"],
"spacer": 24
}
}
""");
Files.createDirectories(source.resolve("dimensions/layers"));
Files.writeString(source.resolve("dimensions/layers/sky.json"), """
{
"name": "Sky",
"dimensionStack": {
"dimensions": ["overworld", "layers/sky"]
}
}
""");
IrisProjectCopier.copyProject(source.toFile(), workspace.toFile(), "overworld", "new-project");
JSONObject dimension = new JSONObject(Files.readString(
workspace.resolve("new-project/dimensions/new-project.json")));
assertEquals("layers/sky", dimension.getJSONObject("dimensionStack")
.getJSONArray("dimensions").getString(0));
assertEquals("new-project", dimension.getJSONObject("dimensionStack")
.getJSONArray("dimensions").getString(1));
JSONObject supportingDimension = new JSONObject(Files.readString(
workspace.resolve("new-project/dimensions/layers/sky.json")));
assertEquals("new-project", supportingDimension.getJSONObject("dimensionStack")
.getJSONArray("dimensions").getString(0));
assertEquals("layers/sky", supportingDimension.getJSONObject("dimensionStack")
.getJSONArray("dimensions").getString(1));
}
@Test
public void existingTargetRemainsUnchanged() throws Exception {
Path workspace = temporaryFolder.newFolder("packs").toPath();
@@ -0,0 +1,337 @@
package art.arcane.iris.engine;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
public class DimensionStackLayoutTest {
@Test
public void spacerCountsExactAirBlocksBetweenInclusiveLayers() {
DimensionStackLayout layout = DimensionStackLayout.create(
64,
8,
List.of(input(15, 15), input(15, 15), input(15, 15)),
new int[]{0, 0}
);
List<DimensionStackLayout.Layer> layers = layout.layersBottomToTop();
assertEquals(0, layers.get(0).localBaseY());
assertEquals(15, layers.get(0).surfaceY());
assertEquals(24, layers.get(1).localBaseY());
assertEquals(39, layers.get(1).surfaceY());
assertEquals(48, layers.get(2).localBaseY());
assertEquals(63, layers.get(2).surfaceY());
assertEquals(8, layers.get(1).localBaseY() - layers.get(0).contentTopY() - 1);
assertEquals(8, layers.get(2).localBaseY() - layers.get(1).contentTopY() - 1);
assertEquals(63, layout.stackTerrainTopY());
assertEquals(63, layout.stackTopY());
}
@Test
public void independentlySignedSeamOffsetsShiftOnlyTheirUpperLayer() {
DimensionStackLayout layout = DimensionStackLayout.create(
64,
4,
List.of(input(10, 7), input(10, 7), input(10, 7)),
new int[]{3, -2}
);
List<DimensionStackLayout.Layer> layers = layout.layersBottomToTop();
assertEquals(0, layers.get(0).localBaseY());
assertEquals(18, layers.get(1).localBaseY());
assertEquals(31, layers.get(2).localBaseY());
assertEquals(3, layers.get(1).seamOffsetBelow());
assertEquals(-2, layers.get(2).seamOffsetBelow());
assertEquals(41, layout.stackTerrainTopY());
assertEquals(41, layout.stackTopY());
}
@Test
public void renderingBoundsClipWithoutChangingRawLayout() {
DimensionStackLayout layout = DimensionStackLayout.create(
32,
8,
List.of(input(15, 15), input(15, 15), input(15, 15)),
new int[]{0, 0}
);
List<DimensionStackLayout.Layer> layers = layout.layersBottomToTop();
DimensionStackLayout.Layer middle = layers.get(1);
DimensionStackLayout.Layer top = layers.get(2);
assertEquals(39, middle.contentTopY());
assertEquals(24, middle.renderMinY());
assertEquals(31, middle.renderMaxY());
assertEquals(31, middle.clippedSurfaceY());
assertTrue(middle.visible());
assertTrue(middle.containsRenderedY(31));
assertFalse(middle.containsRenderedY(32));
assertTrue(layout.containsUpperLayerY(31));
assertFalse(layout.containsUpperLayerY(23));
assertEquals(48, top.localBaseY());
assertFalse(top.visible());
assertFalse(top.containsRenderedY(31));
assertEquals(63, layout.stackTerrainTopY());
assertEquals(63, layout.stackTopY());
assertEquals(31, layout.clippedStackTerrainTopY());
assertEquals(31, layout.clippedStackTopY());
}
@Test
public void invisibleLayersDoNotRaiseRenderedHeightToTheCeiling() {
DimensionStackLayout layout = DimensionStackLayout.create(
64,
64,
List.of(input(15, 15), input(15, 15)),
new int[]{0}
);
assertFalse(layout.layersBottomToTop().get(1).visible());
assertEquals(15, layout.clippedStackTerrainTopY());
assertEquals(15, layout.clippedStackTopY());
}
@Test
public void layerLookupReturnsRenderedOwnerAndBottomOwnerForAir() {
DimensionStackLayout layout = DimensionStackLayout.create(
64,
8,
List.of(input(15, 15), input(15, 15)),
new int[]{0}
);
assertSame(layout.layersBottomToTop().get(0), layout.layerAt(8));
assertSame(layout.layersBottomToTop().get(0), layout.layerAt(20));
assertSame(layout.layersBottomToTop().get(1), layout.layerAt(24));
}
@Test
public void laterGapsEraseEarlierUpperLayerOwnership() {
DimensionStackLayout layout = DimensionStackLayout.create(
128,
0,
List.of(input(100, 100), input(60, 60), input(10, 10), input(10, 10)),
new int[]{-81, -41, 39}
);
List<DimensionStackLayout.Layer> layers = layout.layersBottomToTop();
assertSame(layers.get(1), layout.layerAt(30));
assertSame(layers.get(2), layout.layerAt(45));
assertSame(layers.get(0), layout.layerAt(60));
assertSame(layers.get(3), layout.layerAt(95));
}
@Test
public void hostFeaturesCannotFillStackGapsOrOverwriteUpperLayers() {
DimensionStackLayout layout = DimensionStackLayout.create(
64,
8,
List.of(input(15, 15), input(15, 15)),
new int[]{0}
);
assertFalse(layout.isHostFeatureProtectedY(15));
assertTrue(layout.isHostFeatureProtectedY(20));
assertTrue(layout.isHostFeatureProtectedY(24));
}
@Test
public void fluidExtendsContentWithoutChangingTerrainTop() {
DimensionStackLayout layout = DimensionStackLayout.create(
64,
2,
List.of(input(5, 9), input(4, 7)),
new int[]{0}
);
List<DimensionStackLayout.Layer> layers = layout.layersBottomToTop();
assertEquals(9, layers.get(0).contentTopY());
assertEquals(12, layers.get(1).localBaseY());
assertEquals(16, layers.get(1).surfaceY());
assertEquals(19, layers.get(1).contentTopY());
assertEquals(16, layout.stackTerrainTopY());
assertEquals(19, layout.stackTopY());
}
@Test
public void physicallyHighestTerrainLayerWinsWhenBlendEmbedsAnUpperLayer() {
DimensionStackLayout layout = DimensionStackLayout.create(
64,
0,
List.of(input(20, 20), input(5, 5)),
new int[]{-15}
);
assertSame(layout.layersBottomToTop().get(0), layout.topTerrainLayer());
}
@Test
public void declaredUpperLayerWinsWhenRenderedSurfacesTie() {
DimensionStackLayout layout = DimensionStackLayout.create(
64,
0,
List.of(input(10, 10), input(5, 5)),
new int[]{-6}
);
assertSame(layout.layersBottomToTop().get(1), layout.topTerrainLayer());
}
@Test
public void upperFluidOccludesAPhysicallyHigherLowerSurface() {
DimensionStackLayout layout = DimensionStackLayout.create(
64,
0,
List.of(input(20, 20), input(5, 15)),
new int[]{-11}
);
assertEquals(15, layout.clippedStackTerrainTopY());
assertEquals(25, layout.clippedStackTopY());
assertSame(layout.layersBottomToTop().get(1), layout.topTerrainLayer());
}
@Test
public void laterGapErasesProtrudingTerrainWhenItsUpperLayerIsClipped() {
DimensionStackLayout layout = DimensionStackLayout.create(
128,
0,
List.of(input(100, 100), input(10, 10), input(10, 10)),
new int[]{-51, 89}
);
assertEquals(50, layout.layersBottomToTop().get(1).localBaseY());
assertEquals(150, layout.layersBottomToTop().get(2).localBaseY());
assertFalse(layout.layersBottomToTop().get(2).visible());
assertEquals(60, layout.clippedStackTerrainTopY());
assertEquals(60, layout.clippedStackTopY());
assertSame(layout.layersBottomToTop().get(1), layout.topTerrainLayer());
}
@Test
public void visibleFluidOwnsSurfaceQueriesWhenAllTerrainIsOccludedBelowTheFloor() {
DimensionStackLayout layout = DimensionStackLayout.create(
64,
0,
List.of(input(5, 5), input(2, 30)),
new int[]{-20}
);
assertEquals(-14, layout.layersBottomToTop().get(1).localBaseY());
assertEquals(0, layout.clippedStackTerrainTopY());
assertEquals(16, layout.clippedStackTopY());
assertNull(layout.topTerrainLayer());
assertSame(layout.layersBottomToTop().get(1), layout.surfaceLayer());
}
@Test
public void layoutListsAreImmutableAndExposeBothOrders() {
ArrayList<DimensionStackLayout.LayerInput> inputs = new ArrayList<>();
DimensionStackLayout.LayerInput bottom = input(3, 3);
DimensionStackLayout.LayerInput top = input(4, 4);
inputs.add(bottom);
inputs.add(top);
DimensionStackLayout layout = DimensionStackLayout.create(32, 1, inputs, new int[]{0});
inputs.clear();
assertEquals(2, layout.layersBottomToTop().size());
assertEquals(
layout.layersBottomToTop().get(0),
layout.layersTopToBottom().get(1)
);
assertThrows(
UnsupportedOperationException.class,
() -> layout.layersBottomToTop().clear()
);
}
@Test
public void seamSaltsAreStableAndBoundarySpecific() {
long first = DimensionStackContext.seamSalt(0, "root", "middle");
long repeat = DimensionStackContext.seamSalt(0, "root", "middle");
long differentIndex = DimensionStackContext.seamSalt(1, "root", "middle");
long differentUpper = DimensionStackContext.seamSalt(0, "root", "top");
assertEquals(first, repeat);
assertNotEquals(first, differentIndex);
assertNotEquals(first, differentUpper);
}
@Test
public void seamOffsetsClampUnboundedAndNonFiniteNoise() {
assertEquals(4, DimensionStackContext.clampSeamOffset(12D, 4));
assertEquals(-4, DimensionStackContext.clampSeamOffset(-12D, 4));
assertEquals(2, DimensionStackContext.clampSeamOffset(1.6D, 4));
assertEquals(0, DimensionStackContext.clampSeamOffset(Double.NaN, 4));
assertEquals(0, DimensionStackContext.clampSeamOffset(Double.POSITIVE_INFINITY, 4));
}
@Test
public void queryLayoutsCacheStableColumnsAndBypassTemporaryNaturalFallbacks() {
Cache<Long, DimensionStackLayout> cache = Caffeine.newBuilder().maximumSize(4).build();
DimensionStackLayout stable = DimensionStackLayout.create(
32, 0, List.of(input(10, 10), input(10, 10)), new int[]{0});
DimensionStackLayout unusedStable = DimensionStackLayout.create(
32, 0, List.of(input(11, 11), input(10, 10)), new int[]{0});
AtomicInteger stableSamples = new AtomicInteger();
DimensionStackLayout first = DimensionStackContext.resolveLayout(
cache, 7L, false, () -> {
stableSamples.incrementAndGet();
return stable;
});
DimensionStackLayout second = DimensionStackContext.resolveLayout(
cache, 7L, false, () -> {
stableSamples.incrementAndGet();
return unusedStable;
});
assertSame(stable, first);
assertSame(stable, second);
assertEquals(1, stableSamples.get());
DimensionStackLayout naturalFirst = DimensionStackLayout.create(
32, 0, List.of(input(12, 12), input(10, 10)), new int[]{0});
DimensionStackLayout naturalSecond = DimensionStackLayout.create(
32, 0, List.of(input(13, 13), input(10, 10)), new int[]{0});
AtomicInteger naturalSamples = new AtomicInteger();
assertSame(naturalFirst, DimensionStackContext.resolveLayout(
cache, 7L, true, () -> {
naturalSamples.incrementAndGet();
return naturalFirst;
}));
assertSame(naturalSecond, DimensionStackContext.resolveLayout(
cache, 7L, true, () -> {
naturalSamples.incrementAndGet();
return naturalSecond;
}));
assertEquals(2, naturalSamples.get());
assertSame(stable, cache.getIfPresent(7L));
}
private static DimensionStackLayout.LayerInput input(int terrainHeight, int fluidHeight) {
return new DimensionStackLayout.LayerInput(
mock(DimensionTerrainContext.class),
null,
null,
null,
null,
null,
terrainHeight,
fluidHeight
);
}
}
@@ -0,0 +1,73 @@
package art.arcane.iris.engine;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.image.IrisImageMapRuntime;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.project.stream.ProceduralStream;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class DimensionTerrainContextFallbackTest {
@Test
@SuppressWarnings("unchecked")
public void selfSamplerUsesNaturalValuesOnlyForNonblockingColumns() {
Engine engine = mock(Engine.class);
IrisDimension dimension = mock(IrisDimension.class);
IrisComplex complex = mock(IrisComplex.class);
ProceduralStream<Double> naturalHeight = mock(ProceduralStream.class);
ProceduralStream<Double> resolvedHeight = mock(ProceduralStream.class);
ProceduralStream<Double> resolvedFluidHeight = mock(ProceduralStream.class);
ProceduralStream<IrisBiome> naturalBiome = mock(ProceduralStream.class);
ProceduralStream<IrisBiome> resolvedBiome = mock(ProceduralStream.class);
ProceduralStream<IrisRegion> region = mock(ProceduralStream.class);
ProceduralStream<PlatformBlockState> rock = mock(ProceduralStream.class);
ProceduralStream<PlatformBlockState> configuredFluid = mock(ProceduralStream.class);
IrisImageMapRuntime imageMapRuntime = mock(IrisImageMapRuntime.class);
IrisBiome naturalBiomeValue = mock(IrisBiome.class);
IrisBiome resolvedBiomeValue = mock(IrisBiome.class);
PlatformBlockState naturalFluid = mock(PlatformBlockState.class);
PlatformBlockState resolvedFluid = mock(PlatformBlockState.class);
when(engine.getDimension()).thenReturn(dimension);
when(engine.getComplex()).thenReturn(complex);
when(engine.getHeight()).thenReturn(128);
when(dimension.getLoadKey()).thenReturn("root");
when(dimension.getFluidHeight()).thenReturn(12);
when(complex.getNaturalHeightStream()).thenReturn(naturalHeight);
when(complex.getHeightStream()).thenReturn(resolvedHeight);
when(complex.getRiverWaterSurfaceStream()).thenReturn(resolvedFluidHeight);
when(complex.getNaturalTrueBiomeStream()).thenReturn(naturalBiome);
when(complex.getTrueBiomeStream()).thenReturn(resolvedBiome);
when(complex.getRegionStream()).thenReturn(region);
when(complex.getRockStream()).thenReturn(rock);
when(complex.getFluidStream()).thenReturn(configuredFluid);
when(complex.getImageMapRuntime()).thenReturn(imageMapRuntime);
when(naturalHeight.getDouble(12D, -8D)).thenReturn(15D);
when(resolvedHeight.getDouble(12D, -8D)).thenReturn(18D);
when(resolvedFluidHeight.getDouble(12D, -8D)).thenReturn(14D);
when(naturalBiome.get(12D, -8D)).thenReturn(naturalBiomeValue);
when(resolvedBiome.get(12D, -8D)).thenReturn(resolvedBiomeValue);
when(configuredFluid.get(12D, -8D)).thenReturn(naturalFluid);
when(complex.resolveSurfaceFluid(12D, -8D)).thenReturn(resolvedFluid);
DimensionTerrainContext context = DimensionTerrainContext.forStack(engine, dimension);
when(engine.answersFromNaturalTerrain(12, -8)).thenReturn(true);
assertEquals(15D, context.getNormalTerrainHeight(12D, -8D), 0D);
assertEquals(12D, context.getFluidHeight(12D, -8D), 0D);
assertSame(naturalBiomeValue, context.getBiome(12D, -8D));
assertSame(naturalFluid, context.getFluidBlock(12D, -8D));
when(engine.answersFromNaturalTerrain(12, -8)).thenReturn(false);
assertEquals(18D, context.getNormalTerrainHeight(12D, -8D), 0D);
assertEquals(14D, context.getFluidHeight(12D, -8D), 0D);
assertSame(resolvedBiomeValue, context.getBiome(12D, -8D));
assertSame(resolvedFluid, context.getFluidBlock(12D, -8D));
}
}
@@ -37,6 +37,20 @@ public class EngineRuntimePublicationContractTest {
assertBefore(publish, "sealAndAwait(", "closeRuntime(next, e)");
}
@Test
public void dimensionStackRunsAfterHostStaticObjects() throws IOException {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/engine/EngineRuntimeBuilder.java")).replace("\r\n", "\n");
int buildStart = source.indexOf("EngineRuntime buildRuntime(EngineTarget runtimeTarget)");
int buildEnd = source.indexOf("DimensionStackContext buildDimensionStackContext", buildStart);
String build = source.substring(buildStart, buildEnd);
assertBefore(build, "staticObjects.apply(engine, x, z, blocks)",
"dimensionStack.actuate(x, z, blocks, multicore, context)");
assertBefore(build, "dimensionStack.actuate(x, z, blocks, multicore, context)",
"stackCustom.modify(x, z, blocks, multicore, context)");
}
private static void assertBefore(String source, String first, String second) {
int firstIndex = source.indexOf(first);
int secondIndex = source.indexOf(second);
@@ -17,6 +17,7 @@ import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterBiomeInject;
import org.junit.After;
import org.junit.Test;
@@ -30,6 +31,8 @@ import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
public class IrisBiomeActuatorCoordinateTest {
@@ -69,6 +72,19 @@ public class IrisBiomeActuatorCoordinateTest {
assertEquals(List.of("100,-200", "100,-199", "101,-200", "101,-199"), samples);
}
@Test
@SuppressWarnings("unchecked")
public void stackedBiomeReplacementRemovesEveryExistingMarkerWithItsTypedSlice() {
Mantle<Matter> mantle = mock(Mantle.class);
IrisBiomeActuator.clearBiomeMatterRange(mantle, 100, -200, 4, 6);
verify(mantle).remove(100, 4, -200, MatterBiomeInject.class);
verify(mantle).remove(100, 5, -200, MatterBiomeInject.class);
verify(mantle).remove(100, 6, -200, MatterBiomeInject.class);
verifyNoMoreInteractions(mantle);
}
@SuppressWarnings("unchecked")
private Engine engine() {
Mantle<Matter> mantle = mock(Mantle.class);
@@ -0,0 +1,30 @@
package art.arcane.iris.engine.actuator;
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveCell;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterSlice;
import org.junit.Test;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class IrisDimensionStackActuatorMetadataTest {
@Test
@SuppressWarnings("unchecked")
public void overwrittenStackCellsClearHostHydrologyCaveOwnership() {
MantleChunk<Matter> chunk = mock(MantleChunk.class);
Matter section = mock(Matter.class);
MatterSlice<HydrologyCaveCell> hydrology = mock(MatterSlice.class);
when(chunk.get(1)).thenReturn(section);
doReturn(hydrology).when(section).getSlice(HydrologyCaveCell.class);
IrisDimensionStackActuator.MetadataCleaner cleaner =
new IrisDimensionStackActuator.MetadataCleaner(chunk, 64);
cleaner.clear(3, 20, 7);
verify(hydrology).set(3, 4, 7, null);
}
}
@@ -0,0 +1,123 @@
package art.arcane.iris.engine.framework;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.DimensionStackContext;
import art.arcane.iris.engine.DimensionStackLayout;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBiomeWriter;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.PlatformRegistries;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class EngineHostHeightTest {
@Before
public void bindPlatform() {
IrisPlatforms.unbind();
PlatformBiome biome = mock(PlatformBiome.class);
PlatformBlockState block = mock(PlatformBlockState.class);
PlatformRegistries registries = mock(PlatformRegistries.class);
when(registries.biome(anyString())).thenReturn(biome);
when(registries.block(anyString())).thenReturn(block);
PlatformBiomeWriter biomeWriter = mock(PlatformBiomeWriter.class);
when(biomeWriter.biomeIdFor(anyString())).thenReturn(1);
IrisPlatform platform = mock(IrisPlatform.class);
when(platform.registries()).thenReturn(registries);
when(platform.biomeWriter()).thenReturn(biomeWriter);
IrisPlatforms.bind(platform);
}
@After
public void unbindPlatform() {
IrisPlatforms.unbind();
}
@Test
public void coldMainThreadQueryUsesNaturalHostTerrain() {
Engine engine = mock(Engine.class);
EngineMantle mantle = mock(EngineMantle.class);
IrisComplex complex = mock(IrisComplex.class);
when(engine.getMantle()).thenReturn(mantle);
when(engine.getComplex()).thenReturn(complex);
when(engine.answersFromNaturalTerrain(12, -8)).thenReturn(true);
when(complex.naturalTrueHeight(12, -8)).thenReturn(42);
when(mantle.getFluidHeight()).thenReturn(50);
assertEquals(42, Engine.hostHeight(engine, 12, -8, true));
assertEquals(50, Engine.hostHeight(engine, 12, -8, false));
verify(mantle, never()).trueHeight(12, -8);
verify(mantle, never()).getFluidHeight(12, -8);
}
@Test
public void plannedQueryUsesHostColumnTerrainAndFluid() {
Engine engine = mock(Engine.class);
EngineMantle mantle = mock(EngineMantle.class);
when(engine.getMantle()).thenReturn(mantle);
when(mantle.trueHeight(3, 7)).thenReturn(45);
when(mantle.getFluidHeight(3, 7)).thenReturn(48);
assertEquals(45, Engine.hostHeight(engine, 3, 7, true));
assertEquals(48, Engine.hostHeight(engine, 3, 7, false));
}
@Test
public void yAwareRegionUsesTheOwningStackLayer() {
Engine engine = mock(Engine.class, CALLS_REAL_METHODS);
DimensionStackContext stack = mock(DimensionStackContext.class);
DimensionStackLayout.Layer layer = mock(DimensionStackLayout.Layer.class);
DimensionStackLayout layout = mock(DimensionStackLayout.class);
IrisRegion expected = mock(IrisRegion.class);
when(engine.getDimensionStackContext()).thenReturn(stack);
when(stack.getLayout(12, -8)).thenReturn(layout);
when(layout.layerAt(96)).thenReturn(layer);
when(layer.region()).thenReturn(expected);
assertEquals(expected, engine.getRegion(12, 96, -8));
}
@Test
public void stackQueryConsumersUseCacheAwareLayouts() {
Engine engine = mock(Engine.class, CALLS_REAL_METHODS);
DimensionStackContext stack = mock(DimensionStackContext.class);
DimensionStackLayout layout = mock(DimensionStackLayout.class);
DimensionStackLayout.Layer bottom = mock(DimensionStackLayout.Layer.class);
DimensionStackLayout.Layer upper = mock(DimensionStackLayout.Layer.class);
IrisRegion region = mock(IrisRegion.class);
IrisBiome biome = mock(IrisBiome.class);
when(engine.getDimensionStackContext()).thenReturn(stack);
when(stack.getLayout(12, -8)).thenReturn(layout);
when(layout.layersBottomToTop()).thenReturn(List.of(bottom, upper));
when(layout.surfaceLayer()).thenReturn(upper);
when(layout.layerAt(96)).thenReturn(upper);
when(upper.region()).thenReturn(region);
when(upper.biome()).thenReturn(biome);
assertSame(region, engine.getRegion(12, -8));
assertSame(biome, engine.getSurfaceBiome(12, -8));
assertSame(biome, engine.getBiome(12, 96, -8));
assertSame(biome, engine.getBiomeOrMantle(12, 96, -8));
verify(stack, times(4)).getLayout(12, -8);
verify(stack, never()).sample(anyInt(), anyInt());
}
}
@@ -2,6 +2,8 @@ package art.arcane.iris.engine.framework;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.engine.DimensionStackContext;
import art.arcane.iris.engine.DimensionTerrainContext;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDimension;
@@ -22,6 +24,7 @@ import org.junit.Test;
import java.io.File;
import java.nio.file.Files;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Future;
@@ -301,6 +304,20 @@ public class HintedLocatorTest {
assertEquals(1, plan.getStrideChunks());
}
@Test
public void biomePlanIsUnprunedForStackLayerBiome() {
DimensionStackContext stackContext = mock(DimensionStackContext.class);
IrisBiome stacked = biome("stacked");
when(engine.getDimensionStackContext()).thenReturn(stackContext);
when(engine.getAllBiomes()).thenReturn(new KList<>(stacked));
HintedLocator.SearchPlan plan = HintedLocator.biomePlan(engine, "stacked");
assertTrue(plan.isPossible());
assertNull(plan.getCoarse());
assertEquals(1, plan.getStrideChunks());
}
@Test
public void regionPlanMatchesOnlyTargetRegionCells() {
IrisRegion regionA = region("reg_a");
@@ -326,6 +343,24 @@ public class HintedLocatorTest {
assertFalse(plan.isPossible());
}
@Test
public void regionPlanIsUnprunedForStackLayerRegion() {
DimensionStackContext stackContext = mock(DimensionStackContext.class);
DimensionTerrainContext terrainContext = mock(DimensionTerrainContext.class);
IrisDimension stackedDimension = mock(IrisDimension.class);
IrisRegion stackedRegion = region("stacked_region");
when(engine.getDimensionStackContext()).thenReturn(stackContext);
when(stackContext.getLayersTopToBottom()).thenReturn(List.of(terrainContext));
when(terrainContext.getDimension()).thenReturn(stackedDimension);
when(stackedDimension.getAllRegions(terrainContext)).thenReturn(new KList<>(stackedRegion));
HintedLocator.SearchPlan plan = HintedLocator.regionPlan(engine, "stacked_region");
assertTrue(plan.isPossible());
assertNull(plan.getCoarse());
assertEquals(1, plan.getStrideChunks());
}
@Test
public void objectPlanPrunesToHostBiomes() {
IrisRegion regionA = region("reg_a");
@@ -0,0 +1,22 @@
package art.arcane.iris.engine.modifier;
import art.arcane.iris.engine.DimensionStackLayout;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisFloatingChildBiomeModifierStackTest {
@Test
public void floatingBiomeMarkersSkipStackProtectedCells() {
DimensionStackLayout layout = mock(DimensionStackLayout.class);
when(layout.isHostFeatureProtectedY(60)).thenReturn(true);
when(layout.isHostFeatureProtectedY(30)).thenReturn(false);
assertFalse(IrisFloatingChildBiomeModifier.shouldWriteHostBiomeMarker(layout, 60));
assertTrue(IrisFloatingChildBiomeModifier.shouldWriteHostBiomeMarker(layout, 30));
assertTrue(IrisFloatingChildBiomeModifier.shouldWriteHostBiomeMarker(null, 60));
}
}
@@ -0,0 +1,68 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.iris.util.project.stream.ProceduralStream;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public final class IrisBiomeLayerSlopeSourceTest {
@Test
public void explicitSlopeStreamControlsSurfacePaletteSelection() {
assertExplicitSlopeStreamControlsSurfacePaletteSelection(false);
}
@Test
public void explicitSlopeStreamControlsLockedSurfacePaletteSelection() {
assertExplicitSlopeStreamControlsSurfacePaletteSelection(true);
}
@SuppressWarnings("unchecked")
private static void assertExplicitSlopeStreamControlsSurfacePaletteSelection(boolean lockLayers) {
IrisData data = mock(IrisData.class);
IrisComplex hostComplex = mock(IrisComplex.class);
ProceduralStream<Double> hostSlope = mock(ProceduralStream.class);
ProceduralStream<Double> sourceSlope = mock(ProceduralStream.class);
IrisBiomePaletteLayer paletteLayer = mock(IrisBiomePaletteLayer.class);
CNG heightGenerator = mock(CNG.class);
PlatformBlockState surfaceBlock = mock(PlatformBlockState.class);
RNG rng = new RNG(19L);
IrisBiome biome = new IrisBiome();
biome.setLockLayers(lockLayers);
KList<IrisBiomePaletteLayer> paletteLayers = new KList<>();
paletteLayers.add(paletteLayer);
biome.setLayers(paletteLayers);
when(hostComplex.getSlopeStream()).thenReturn(hostSlope);
when(hostSlope.getDouble(12D, -8D)).thenReturn(0D);
when(sourceSlope.getDouble(12D, -8D)).thenReturn(5D);
when(paletteLayer.getZoom()).thenReturn(1D);
when(paletteLayer.getMinHeight()).thenReturn(1);
when(paletteLayer.getMaxHeight()).thenReturn(1);
when(paletteLayer.getSlopeCondition()).thenReturn(new IrisSlopeClip(5D, 5D));
when(paletteLayer.getHeightGenerator(any(RNG.class), same(data))).thenReturn(heightGenerator);
when(heightGenerator.fit(1, 1, 12D, -8D)).thenReturn(1);
when(paletteLayer.get(rng, 0, 12D, 0D, -8D, data)).thenReturn(surfaceBlock);
KList<PlatformBlockState> hostLayers = biome.generateLayers(
new IrisDimension(), 12D, -8D, rng, 1, 32, data, hostComplex);
KList<PlatformBlockState> sourceLayers = biome.generateLayersWithSlope(
new IrisDimension(), 12D, -8D, rng, 1, 32, data, sourceSlope);
assertTrue(hostLayers.isEmpty());
assertEquals(1, sourceLayers.size());
assertSame(surfaceBlock, sourceLayers.get(0));
}
}
@@ -16,6 +16,43 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class IrisDimensionReachableBiomesTest {
@Test
@SuppressWarnings("unchecked")
public void includesOutOfPoolFocusBiomeAndRegion() {
IrisDimension dimension = new IrisDimension()
.setRegions(new KList<>("declared"))
.setFocus("focused-biome")
.setFocusRegion("focused-region");
IrisRegion declared = new IrisRegion().setLandBiomes(new KList<>("declared-biome"));
IrisRegion focused = new IrisRegion().setLandBiomes(new KList<>("focused-region-biome"));
declared.setLoadKey("declared");
focused.setLoadKey("focused-region");
IrisBiome declaredBiome = biome("declared-biome");
IrisBiome focusedBiome = biome("focused-biome");
IrisBiome focusedRegionBiome = biome("focused-region-biome");
IrisData data = mock(IrisData.class);
ResourceLoader<IrisRegion> regionLoader = mock(ResourceLoader.class);
ResourceLoader<IrisBiome> biomeLoader = mock(ResourceLoader.class);
when(data.getRegionLoader()).thenReturn(regionLoader);
when(data.getBiomeLoader()).thenReturn(biomeLoader);
when(regionLoader.load("declared")).thenReturn(declared);
when(regionLoader.load("focused-region")).thenReturn(focused);
when(biomeLoader.load("declared-biome")).thenReturn(declaredBiome);
when(biomeLoader.load("focused-biome")).thenReturn(focusedBiome);
when(biomeLoader.load("focused-region-biome")).thenReturn(focusedRegionBiome);
Set<String> regionKeys = dimension.getAllRegions(() -> data).stream()
.map(IrisRegion::getLoadKey)
.collect(Collectors.toSet());
Set<String> biomeKeys = dimension.getReachableBiomes(() -> data).stream()
.map(IrisBiome::getLoadKey)
.collect(Collectors.toSet());
assertEquals(Set.of("declared", "focused-region"), regionKeys);
assertEquals(Set.of("declared-biome", "focused-biome", "focused-region-biome"), biomeKeys);
}
@Test
@SuppressWarnings("unchecked")
public void includesExactRecursiveGenerationClosure() {
@@ -20,4 +20,20 @@ public class IrisDimensionTypeKeyTest {
assertEquals("worlds_my_pack", dimension.getDimensionTypeKey());
}
@Test
public void customBiomeKeyPreservesFlatDimensionNamespace() {
IrisDimension dimension = new IrisDimension();
dimension.setLoadKey("Overworld");
assertEquals("overworld:aurora", dimension.getCustomBiomeKey("Aurora"));
}
@Test
public void customBiomeKeyMapsRecursiveDimensionPath() {
IrisDimension dimension = new IrisDimension();
dimension.setLoadKey("Layers/Sky");
assertEquals("layers:sky/aurora", dimension.getCustomBiomeKey("Aurora"));
}
}
@@ -0,0 +1,30 @@
package art.arcane.iris.engine.platform;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisWorld;
import org.bukkit.Location;
import org.junit.Test;
import static org.junit.Assert.assertSame;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class EngineBukkitOpsBiomeTest {
@Test
public void mantleBiomeLookupConvertsWorldYToInternalY() {
Engine engine = mock(Engine.class);
IrisWorld world = IrisWorld.builder().minHeight(-64).maxHeight(320).build();
IrisBiome biome = mock(IrisBiome.class);
Location location = mock(Location.class);
when(engine.getWorld()).thenReturn(world);
when(location.getBlockX()).thenReturn(12);
when(location.getBlockY()).thenReturn(-25);
when(location.getBlockZ()).thenReturn(-7);
when(engine.getBiomeOrMantle(12, 39, -7)).thenReturn(biome);
assertSame(biome, EngineBukkitOps.getBiomeOrMantle(engine, location));
verify(engine).getBiomeOrMantle(12, 39, -7);
}
}

Some files were not shown because too many files have changed in this diff Show More