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 committed 2026-09-04 14:23:15 -04:00
1 parent 366033a535
commit 6549b40797
119 files changed
+6874 -743

No files matched your search

@@ -4,6 +4,9 @@ import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import com.mojang.datafixers.util.Pair; import com.mojang.datafixers.util.Pair;
import com.mojang.serialization.MapCodec; import com.mojang.serialization.MapCodec;
import art.arcane.iris.spi.IrisLogging; 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.data.cache.AtomicCache;
import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.GenerationSessionException; 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.collection.KMap;
import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.math.RNG;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.Holder; import net.minecraft.core.Holder;
import net.minecraft.core.QuartPos; import net.minecraft.core.QuartPos;
import net.minecraft.core.Registry; import net.minecraft.core.Registry;
@@ -24,6 +28,8 @@ import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier; import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceKey;
import net.minecraft.util.RandomSource; 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.Biome;
import net.minecraft.world.level.biome.BiomeSource; import net.minecraft.world.level.biome.BiomeSource;
import net.minecraft.world.level.biome.Climate; import net.minecraft.world.level.biome.Climate;
@@ -80,27 +86,31 @@ public class CustomBiomeSource extends BiomeSource {
this.cacheRuntimeId = engine.getCacheID(); 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<>(); 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()); Holder<Biome> vanillaHolder = resolveBiomeHolder(registry, i.getStructureDerivativeKey());
if (vanillaHolder == null) { if (vanillaHolder == null) {
throw new IllegalStateException("Iris structure biome derivative '" throw new IllegalStateException("Iris structure biome derivative '"
+ i.getStructureDerivativeKey() + "' is not registered for biome '" + i.getLoadKey() + "'"); + i.getStructureDerivativeKey() + "' is not registered for biome '" + i.getLoadKey() + "'");
} }
biomes.add(vanillaHolder); biomes.add(vanillaHolder);
}
if (i.isCustom()) { if (includeDimensionStack) {
for (IrisBiomeCustom j : i.getCustomDerivitives()) { biomes.addAll(getVisibleBiomes(
Holder<Biome> customHolder = resolveCustomBiomeHolder(customRegistry, engine, j.getId()); customRegistry,
if (customHolder == null) { registry,
throw new IllegalStateException("Iris custom structure biome '" engine,
+ engine.getDimension().getLoadKey() + ":" + j.getId() + "' is not registered"); resolveFallbackBiome(registry, customRegistry)
} ));
biomes.add(customHolder);
}
}
} }
if (biomes.isEmpty()) { if (biomes.isEmpty()) {
@@ -110,6 +120,57 @@ public class CustomBiomeSource extends BiomeSource {
return new ArrayList<>(biomes); 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) { private static Object getFor(Class<?> type, Object source) {
Object o = fieldFor(type, source); Object o = fieldFor(type, source);
@@ -175,10 +236,19 @@ public class CustomBiomeSource extends BiomeSource {
@Override @Override
protected Stream<Holder<Biome>> collectPossibleBiomes() { 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() { Set<Holder<Biome>> possibleStructureBiomes() {
return possibleBiomes(false);
}
private Set<Holder<Biome>> possibleBiomes(boolean includeDimensionStack) {
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_possible_biomes"); GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_possible_biomes");
if (lease == null) { if (lease == null) {
throw new IllegalStateException("Iris possible biome lookup was rejected during an engine transition"); 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); .lookup(Registries.BIOME).orElse(null);
Registry<Biome> worldRegistry = ((CraftWorld) world).getHandle().registryAccess() Registry<Biome> worldRegistry = ((CraftWorld) world).getHandle().registryAccess()
.lookup(Registries.BIOME).orElse(null); .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; return m;
} }
for (IrisBiome i : engine.getAllBiomes()) { for (OwnedBiome ownedBiome : getOwnedBiomes(engine)) {
IrisBiome i = ownedBiome.biome();
if (i.isCustom()) { if (i.isCustom()) {
for (IrisBiomeCustom j : i.getCustomDerivitives()) { 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 (holder == null) {
if (fallback != 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; continue;
} }
m.put(j.getId(), holder); m.put(key, holder);
} }
} }
} }
@@ -232,7 +307,8 @@ public class CustomBiomeSource extends BiomeSource {
return Collections.unmodifiableMap(spawnBiomes); return Collections.unmodifiableMap(spawnBiomes);
} }
for (IrisBiome irisBiome : engine.getAllBiomes()) { for (OwnedBiome ownedBiome : getOwnedBiomes(engine)) {
IrisBiome irisBiome = ownedBiome.biome();
if (!irisBiome.isCustom()) { if (!irisBiome.isCustom()) {
continue; continue;
} }
@@ -241,7 +317,8 @@ public class CustomBiomeSource extends BiomeSource {
continue; continue;
} }
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) { 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) { if (customHolder != null) {
spawnBiomes.putIfAbsent(customHolder.value(), vanillaHolder); 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() { private RegistryAccess registry() {
RegistryAccess access = registryAccess.aquire(() -> (RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer())); 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"); throw new IllegalStateException("Iris structure biome lookup has no active engine runtime");
} }
ensureCachesCurrent(); ensureCachesCurrent();
if (isGuaranteedSurfaceBiome(y)) { return getStructureNoiseBiomeWithActiveGenerationLease(x, y, z);
return getSurfaceStructureBiomeHolder(x, 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); Holder<Biome> cachedHolder = structureBiomeCache.get(cacheKey);
if (cachedHolder != null) { if (cachedHolder != null) {
return cachedHolder; return cachedHolder;
} }
}
Holder<Biome> resolvedHolder = resolveStructureBiomeHolder(x, y, z); Holder<Biome> resolvedHolder = resolveStructureBiomeHolder(x, y, z);
Holder<Biome> existingHolder = structureBiomeCache.putIfAbsent(cacheKey, resolvedHolder); if (!cacheable) {
if (existingHolder != null) {
return existingHolder;
}
if (structureBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
structureBiomeCache.clear();
}
return resolvedHolder; 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 @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) { static int horizontalBiomeSearchQuartStep(int blockY, int searchRadius) {
return blockY == STRONGHOLD_RING_SEARCH_Y && searchRadius == STRONGHOLD_RING_SEARCH_RADIUS return blockY == STRONGHOLD_RING_SEARCH_Y && searchRadius == STRONGHOLD_RING_SEARCH_RADIUS
? STRONGHOLD_RING_SEARCH_QUART_STEP ? STRONGHOLD_RING_SEARCH_QUART_STEP
@@ -468,7 +639,7 @@ public class CustomBiomeSource extends BiomeSource {
private Holder<Biome> resolveSurfaceStructureBiomeHolder(int x, int z) { private Holder<Biome> resolveSurfaceStructureBiomeHolder(int x, int z) {
int blockX = x << 2; int blockX = x << 2;
int blockZ = z << 2; int blockZ = z << 2;
IrisBiome irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ); IrisBiome irisBiome = resolveSurfaceStructureBiome(engine, blockX, blockZ);
if (irisBiome == null) { if (irisBiome == null) {
throw new IllegalStateException("Iris returned no surface structure biome at block " throw new IllegalStateException("Iris returned no surface structure biome at block "
+ blockX + "," + blockZ); + blockX + "," + blockZ);
@@ -532,14 +703,32 @@ public class CustomBiomeSource extends BiomeSource {
int z, int z,
Climate.Sampler sampler, Climate.Sampler sampler,
IrisDimensionCarvingResolver.State resolverState 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); long cacheKey = packNoiseKey(x, y, z);
Holder<Biome> cachedHolder = noiseBiomeCache.get(cacheKey); if (cacheable) {
if (cachedHolder != null) { Holder<Biome> cachedHolder = noiseBiomeCache.get(cacheKey);
return cachedHolder; if (cachedHolder != null) {
return cachedHolder;
}
} }
Holder<Biome> resolvedHolder = resolveVisibleBiomeHolder(x, y, z, resolverState); Holder<Biome> resolvedHolder = resolveVisibleBiomeHolder(x, y, z, resolverState);
if (!cacheable) {
return resolvedHolder;
}
Holder<Biome> existingHolder = noiseBiomeCache.putIfAbsent(cacheKey, resolvedHolder); Holder<Biome> existingHolder = noiseBiomeCache.putIfAbsent(cacheKey, resolvedHolder);
if (existingHolder != null) { if (existingHolder != null) {
return existingHolder; return existingHolder;
@@ -552,6 +741,18 @@ public class CustomBiomeSource extends BiomeSource {
return resolvedHolder; 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) { private GenerationSessionLease tryAcquireGenerationLease(String operation) {
if (engine.isClosed()) { if (engine.isClosed()) {
return null; return null;
@@ -597,7 +798,7 @@ public class CustomBiomeSource extends BiomeSource {
} }
private Holder<Biome> resolveStructureBiomeHolder(int x, int y, int z) { 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) { if (resolution == null) {
throw new IllegalStateException("Iris returned no structure biome at quart " throw new IllegalStateException("Iris returned no structure biome at quart "
+ x + "," + y + "," + z); + x + "," + y + "," + z);
@@ -619,11 +820,26 @@ public class CustomBiomeSource extends BiomeSource {
int z, int z,
IrisDimensionCarvingResolver.State resolverState 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) { if (resolution == null) {
return getFallbackBiome(); return getFallbackBiome();
} }
return resolveVisibleBiomeHolder(resolution);
}
private Holder<Biome> resolveVisibleBiomeHolder(BiomeResolution resolution) {
if (resolution.irisBiome.isCustom()) { if (resolution.irisBiome.isCustom()) {
return resolveCustomHolder(resolution); return resolveCustomHolder(resolution);
} }
@@ -642,7 +858,8 @@ public class CustomBiomeSource extends BiomeSource {
private Holder<Biome> resolveCustomHolder(BiomeResolution resolution) { private Holder<Biome> resolveCustomHolder(BiomeResolution resolution) {
IrisBiomeCustom customBiome = resolution.irisBiome.getCustomBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ); IrisBiomeCustom customBiome = resolution.irisBiome.getCustomBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ);
if (customBiome != null) { if (customBiome != null) {
Holder<Biome> holder = customBiomes.get(customBiome.getId()); Holder<Biome> holder = customBiomes.get(customBiomeKey(
resolution.dimension, customBiome.getId()));
if (holder != null) { if (holder != null) {
return holder; return holder;
} }
@@ -651,8 +868,8 @@ public class CustomBiomeSource extends BiomeSource {
return getFallbackBiome(); return getFallbackBiome();
} }
private BiomeResolution resolveBiomeResolution(int x, int y, int z) { private BiomeResolution resolveStructureBiomeResolution(int x, int y, int z) {
return resolveBiomeResolution(x, y, z, null); return resolveBiomeResolution(x, y, z, null, false);
} }
private BiomeResolution resolveBiomeResolution( private BiomeResolution resolveBiomeResolution(
@@ -660,6 +877,28 @@ public class CustomBiomeSource extends BiomeSource {
int y, int y,
int z, int z,
IrisDimensionCarvingResolver.State resolverState 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()) { if (engine == null || engine.isClosed()) {
return null; return null;
@@ -675,31 +914,63 @@ public class CustomBiomeSource extends BiomeSource {
int worldMinHeight = engine.getWorld().minHeight(); int worldMinHeight = engine.getWorld().minHeight();
int internalY = blockY - worldMinHeight; int internalY = blockY - worldMinHeight;
int caveSwitchInternalY = Math.max(-8 - worldMinHeight, 40); 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 deepUnderground = internalY <= caveSwitchInternalY;
boolean underground = false; boolean underground = false;
IrisBiome irisBiome; IrisBiome irisBiome;
if (deepUnderground) { if (stackLayer != null) {
int surfaceInternalY = engine.getComplex().getHeightStream().get(blockX, blockZ).intValue(); 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; underground = internalY <= surfaceInternalY - 8;
irisBiome = underground irisBiome = underground
? engine.getCaveBiome(blockX, internalY, blockZ, resolverState) ? engine.getCaveBiome(blockX, internalY, blockZ, resolverState)
: engine.getComplex().getTrueBiomeStream().get(blockX, blockZ); : engine.getHostSurfaceBiome(blockX, blockZ);
} else { } else {
irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ); irisBiome = engine.getHostSurfaceBiome(blockX, blockZ);
} }
if (irisBiome == null && underground) { if (irisBiome == null && underground) {
irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ); irisBiome = stackLayer == null
? engine.getHostSurfaceBiome(blockX, blockZ)
: stackLayer.biome();
} }
if (irisBiome == null) { if (irisBiome == null) {
return null; return null;
} }
return createBiomeResolution(irisBiome, underground, blockX, blockY, blockZ); return createBiomeResolution(
irisBiome, underground, owningDimension, blockX, blockY, blockZ);
} }
private BiomeResolution createBiomeResolution( private BiomeResolution createBiomeResolution(
IrisBiome irisBiome, IrisBiome irisBiome,
boolean underground, boolean underground,
IrisDimension dimension,
int blockX, int blockX,
int blockY, int blockY,
int blockZ int blockZ
@@ -708,7 +979,8 @@ public class CustomBiomeSource extends BiomeSource {
^ (((long) blockX) * 341873128712L) ^ (((long) blockX) * 341873128712L)
^ (((long) blockY) * 132897987541L) ^ (((long) blockY) * 132897987541L)
^ (((long) blockZ) * 42317861L)); ^ (((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() { private Holder<Biome> getFallbackBiome() {
@@ -734,15 +1006,20 @@ public class CustomBiomeSource extends BiomeSource {
return ((long) x << 32) ^ ((long) z & 4294967295L); return ((long) x << 32) ^ ((long) z & 4294967295L);
} }
private static Holder<Biome> resolveCustomBiomeHolder(Registry<Biome> customRegistry, Engine engine, String customBiomeId) { private static Holder<Biome> resolveCustomBiomeHolder(
if (customRegistry == null || engine == null || customBiomeId == null || customBiomeId.isBlank()) { Registry<Biome> customRegistry,
IrisDimension dimension,
String customBiomeId
) {
if (customRegistry == null || dimension == null
|| customBiomeId == null || customBiomeId.isBlank()) {
return null; return null;
} }
Identifier resourceLocation = Identifier.fromNamespaceAndPath( Identifier resourceLocation = Identifier.tryParse(customBiomeKey(dimension, customBiomeId));
engine.getDimension().getLoadKey().toLowerCase(java.util.Locale.ROOT), if (resourceLocation == null) {
customBiomeId.toLowerCase(java.util.Locale.ROOT) return null;
); }
Biome biome = customRegistry.getValue(resourceLocation); Biome biome = customRegistry.getValue(resourceLocation);
if (biome == null) { if (biome == null) {
return null; return null;
@@ -761,6 +1038,63 @@ public class CustomBiomeSource extends BiomeSource {
return optionalReferenceHolder.get(); 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) { private static Holder<Biome> resolveBiomeHolder(Registry<Biome> registry, String biomeKey) {
if (registry == null || biomeKey == null || biomeKey.isBlank()) { if (registry == null || biomeKey == null || biomeKey.isBlank()) {
return null; return null;
@@ -809,18 +1143,31 @@ public class CustomBiomeSource extends BiomeSource {
private static final class BiomeResolution { private static final class BiomeResolution {
private final IrisBiome irisBiome; private final IrisBiome irisBiome;
private final boolean underground; private final boolean underground;
private final IrisDimension dimension;
private final int blockX; private final int blockX;
private final int blockY; private final int blockY;
private final int blockZ; private final int blockZ;
private final RNG rng; 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.irisBiome = irisBiome;
this.underground = underground; this.underground = underground;
this.dimension = dimension;
this.blockX = blockX; this.blockX = blockX;
this.blockY = blockY; this.blockY = blockY;
this.blockZ = blockZ; this.blockZ = blockZ;
this.rng = rng; this.rng = rng;
} }
} }
private record OwnedBiome(IrisDimension dimension, IrisBiome biome) {
}
} }
@@ -1,5 +1,7 @@
package art.arcane.iris.core.nms.v26_2_R1; 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.Engine;
import art.arcane.iris.engine.framework.NativeFeatureGenerationPolicy; import art.arcane.iris.engine.framework.NativeFeatureGenerationPolicy;
import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisBiome;
@@ -40,6 +42,9 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Set; 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 * 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() { private Set<String> visibleBiomeKeys() {
Set<String> keys = new LinkedHashSet<>(); Set<String> keys = new LinkedHashSet<>();
String namespace = engine.getDimension().getLoadKey().toLowerCase(Locale.ROOT); for (IrisBiome irisBiome : engine.getDimension().getReachableBiomes(engine)) {
for (IrisBiome irisBiome : engine.getAllBiomes()) {
addKey(keys, irisBiome.getStructureDerivativeKey()); addKey(keys, irisBiome.getStructureDerivativeKey());
addKey(keys, irisBiome.getDerivativeKey()); addKey(keys, irisBiome.getDerivativeKey());
addKey(keys, irisBiome.getVanillaDerivativeKey()); addKey(keys, irisBiome.getVanillaDerivativeKey());
@@ -213,7 +217,7 @@ final class ImportedFeatureStage {
continue; continue;
} }
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) { for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
addKey(keys, namespace + ":" + customBiome.getId()); addKey(keys, engine.getDimension().getCustomBiomeKey(customBiome.getId()));
} }
} }
return keys; return keys;
@@ -226,8 +230,7 @@ final class ImportedFeatureStage {
private Map<String, Holder<Biome>> customBiomeDerivatives(Registry<Biome> registry, private Map<String, Holder<Biome>> customBiomeDerivatives(Registry<Biome> registry,
Map<String, Holder<Biome>> byKey) { Map<String, Holder<Biome>> byKey) {
Map<String, Holder<Biome>> derivatives = new HashMap<>(); Map<String, Holder<Biome>> derivatives = new HashMap<>();
String namespace = engine.getDimension().getLoadKey().toLowerCase(Locale.ROOT); for (IrisBiome irisBiome : engine.getDimension().getReachableBiomes(engine)) {
for (IrisBiome irisBiome : engine.getAllBiomes()) {
if (!irisBiome.isCustom()) { if (!irisBiome.isCustom()) {
continue; continue;
} }
@@ -246,7 +249,7 @@ final class ImportedFeatureStage {
continue; continue;
} }
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) { for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
derivatives.put(namespace + ":" + customBiome.getId().toLowerCase(Locale.ROOT), derivative); derivatives.put(engine.getDimension().getCustomBiomeKey(customBiome.getId()), derivative);
} }
} }
return derivatives; return derivatives;
@@ -288,14 +291,24 @@ final class ImportedFeatureStage {
List<FeatureSorter.StepFeatureData> steps = table.steps(); List<FeatureSorter.StepFeatureData> steps = table.steps();
WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed())); WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed()));
long decorationSeed = random.setDecorationSeed(level.getSeed(), origin.getX(), origin.getZ()); 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()); IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData());
int staticMinY = engine.getMinHeight(); 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, level, centerPos,
(x, z) -> level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z), surfaceFirstFreeY,
(x, z) -> level.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, x, z), floorFirstFreeY,
position -> staticObjects.contains(position.getX(), position.getY() - staticMinY, position.getZ())); stackContext != null,
importedFeatureProtection(staticObjects, stackContext, staticMinY));
try { try {
for (int stepIndex = 0; stepIndex < steps.size(); stepIndex++) { 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, private void placeStep(WorldGenLevel level, FeatureTable table, FeatureSorter.StepFeatureData stepData,
Registry<PlacedFeature> featureRegistry, Set<Holder<Biome>> chunkBiomes, Registry<PlacedFeature> featureRegistry, Set<Holder<Biome>> chunkBiomes,
ChunkGenerator owner, WorldgenRandom random, long decorationSeed, ChunkGenerator owner, WorldgenRandom random, long decorationSeed,
@@ -353,12 +390,37 @@ final class ImportedFeatureStage {
return id == null ? feature.toString() : id.toString(); 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<>(); List<Holder<Biome>> collected = new ArrayList<>();
ChunkPos.rangeClosed(sectionPos.chunk(), 1).forEach((ChunkPos chunkPos) -> { ChunkPos.rangeClosed(sectionPos.chunk(), 1).forEach((ChunkPos chunkPos) -> {
ChunkAccess neighbour = level.getChunk(chunkPos.x(), chunkPos.z()); ChunkAccess neighbour = level.getChunk(chunkPos.x(), chunkPos.z());
for (LevelChunkSection section : neighbour.getSections()) { LevelChunkSection[] sections = neighbour.getSections();
section.getBiomes().getAll(collected::add); 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<>(); Set<Holder<Biome>> present = new LinkedHashSet<>();
@@ -1,5 +1,7 @@
package art.arcane.iris.core.nms.v26_2_R1; 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.Engine;
import art.arcane.iris.engine.framework.GenerationSessionException; import art.arcane.iris.engine.framework.GenerationSessionException;
import art.arcane.iris.engine.framework.GenerationSessionLease; 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.CompletionException;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import java.util.function.IntBinaryOperator; import java.util.function.IntBinaryOperator;
import java.util.function.Predicate;
public class IrisChunkGenerator extends CustomChunkGenerator { public class IrisChunkGenerator extends CustomChunkGenerator {
private static final WrappedField<ChunkGenerator, BiomeSource> BIOME_SOURCE; private static final WrappedField<ChunkGenerator, BiomeSource> BIOME_SOURCE;
@@ -429,7 +432,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
undergroundStep, undergroundStep,
decision.preserveSourceY(), decision.preserveSourceY(),
decision.yBand(), 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( StructureStart wrapped = NativeStructureReferenceEnvelope.wrapForPublication(
start, structure, start.getReferences(), start, structure, start.getReferences(),
NativeStructureTerrainIntegrator.resolveNativeTerrain(start, decision.terrain()), NativeStructureTerrainIntegrator.resolveNativeTerrain(start, decision.terrain()),
@@ -726,6 +729,14 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
return (x, z) -> engine.getHeight(x, z, true) + runtimeMinY + 1; 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 @Override
public WeightedList<MobSpawnSettings.SpawnerData> getMobsAt(Holder<Biome> holder, StructureManager structuremanager, MobCategory enumcreaturetype, BlockPos blockposition) { public WeightedList<MobSpawnSettings.SpawnerData> getMobsAt(Holder<Biome> holder, StructureManager structuremanager, MobCategory enumcreaturetype, BlockPos blockposition) {
Holder<Biome> vanillaSpawnBiome = customBiomeSource.getVanillaSpawnBiome(holder); Holder<Biome> vanillaSpawnBiome = customBiomeSource.getVanillaSpawnBiome(holder);
@@ -868,17 +879,22 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
} }
try { try {
WorldgenTerrainHeightmaps.primeStructurePlacement( WorldgenTerrainHeightmaps.primeStructurePlacement(
world, chunkPos, heightmapStarts, worldgenSurfaceHeight(), worldgenFloorHeight()); world, chunkPos, heightmapStarts,
hostWorldgenSurfaceHeight(), hostWorldgenFloorHeight());
} catch (Throwable error) { } catch (Throwable error) {
throw NativeStructureGenerationException.failure( throw NativeStructureGenerationException.failure(
"heightmap priming", nativeStructureBatchContext(placementGroups), "heightmap priming", nativeStructureBatchContext(placementGroups),
chunkPos.x(), chunkPos.z(), error); chunkPos.x(), chunkPos.z(), error);
} }
IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData()); IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData());
int staticMinY = engine.getMinHeight(); Predicate<BlockPos> protectedPosition = nativeStructureProtection(staticObjects);
WorldGenLevel boundedWorld = staticObjects.isEmpty() ? world : NativeStructureWorldgenAccess.create( WorldGenLevel boundedWorld = staticObjects.isEmpty()
world, chunkPos, worldgenSurfaceHeight(), worldgenFloorHeight(), && engine.getDimensionStackContext() == null
position -> staticObjects.contains(position.getX(), position.getY() - staticMinY, position.getZ())); ? world
: NativeStructureWorldgenAccess.create(
world, chunkPos, hostWorldgenSurfaceHeight(), hostWorldgenFloorHeight(),
engine.getDimensionStackContext() != null,
protectedPosition);
try { try {
NativeStructureVegetationClearer.clearIntersectingVegetation( NativeStructureVegetationClearer.clearIntersectingVegetation(
boundedWorld, chunk, area, vegetationTargets); boundedWorld, chunk, area, vegetationTargets);
@@ -891,7 +907,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
try { try {
vacuumFoundationPlan = NativeStructureSurfaceFitter.prepareSurfaceStructures( vacuumFoundationPlan = NativeStructureSurfaceFitter.prepareSurfaceStructures(
boundedWorld, area, terrainTargets, 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) { } catch (Throwable error) {
throw NativeStructureGenerationException.failure( throw NativeStructureGenerationException.failure(
"terrain integration", nativeStructureBatchContext(placementGroups), "terrain integration", nativeStructureBatchContext(placementGroups),
@@ -945,17 +961,45 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
BoundingBox area, ChunkPos chunkPos, String structureId, StructureStart start, BoundingBox area, ChunkPos chunkPos, String structureId, StructureStart start,
IrisNativeStructureDecision decision) { IrisNativeStructureDecision decision) {
WorldGenLevel boundedWorld = world instanceof NativeStructureWorldgenAccess ? world : NativeStructureWorldgenAccess.create( 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); world.setCurrentlyGenerating(() -> "Iris native structure " + structureId);
try { try {
NativeStructurePostProcessor.place(boundedWorld, structureManager, this, random, area, chunkPos, NativeStructurePostProcessor.place(boundedWorld, structureManager, this, random, area, chunkPos,
structureId, start, decision, this::resolvePaletteBlock, 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 { } finally {
world.setCurrentlyGenerating(null); 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) { private List<List<Structure>> structuresByStep(Registry<Structure> registry) {
StructureStepCache cached = structureStepCache; StructureStepCache cached = structureStepCache;
if (cached != null && cached.registry() == registry) { if (cached != null && cached.registry() == registry) {
@@ -1041,7 +1085,17 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
@Override @Override
public void spawnOriginalMobs(WorldGenRegion region) { public void spawnOriginalMobs(WorldGenRegion region) {
ChunkPos center = region.getCenter(); 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); Holder<Biome> vanillaBiome = customBiomeSource.getVanillaSpawnBiome(visibleBiome);
WorldgenRandom random = new WorldgenRandom(new LegacyRandomSource(RandomSupport.generateUniqueSeed())); WorldgenRandom random = new WorldgenRandom(new LegacyRandomSource(RandomSupport.generateUniqueSeed()));
random.setDecorationSeed(region.getSeed(), center.getMinBlockX(), center.getMinBlockZ()); 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) { public int getBaseHeight(int i, int j, Heightmap.Types heightmap_type, LevelHeightAccessor levelheightaccessor, RandomState randomstate) {
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_base_height"); try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_base_height");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { 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() return levelheightaccessor.getMinY()
+ engine.getHeight(i, j, !heightmap_type.isOpaque().test(Blocks.WATER.defaultBlockState())) + height + 1;
+ 1;
} catch (GenerationSessionException e) { } catch (GenerationSessionException e) {
throw new IllegalStateException("Iris base height query could not acquire its engine runtime.", 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) { public NoiseColumn getBaseColumn(int i, int j, LevelHeightAccessor levelheightaccessor, RandomState randomstate) {
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_base_column"); try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_base_column");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
int block = engine.getHeight(i, j, true); boolean dimensionStack = engine.getDimensionStackContext() != null;
int water = engine.getHeight(i, j, false); 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()]; BlockState[] column = new BlockState[levelheightaccessor.getHeight()];
for (int k = 0; k < column.length; k++) { for (int k = 0; k < column.length; k++) {
if (k <= block) { if (k <= block) {
@@ -824,6 +824,11 @@ public class NMSBinding implements INMSBinding {
return NativeStructureVolumeIndex.volumes(engine, minX, minZ, maxX, maxZ); return NativeStructureVolumeIndex.volumes(engine, minX, minZ, maxX, maxZ);
} }
@Override
public void invalidateNativeStructureVolumeIndex(Engine engine) {
NativeStructureVolumeIndex.invalidate(engine);
}
@Override @Override
public int getBiomeId(Biome biome) { public int getBiomeId(Biome biome) {
for (World i : Bukkit.getWorlds()) { for (World i : Bukkit.getWorlds()) {
@@ -64,6 +64,7 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
private final ChunkPos generationCenter; private final ChunkPos generationCenter;
private final IntBinaryOperator surfaceFirstFreeY; private final IntBinaryOperator surfaceFirstFreeY;
private final IntBinaryOperator floorFirstFreeY; private final IntBinaryOperator floorFirstFreeY;
private final boolean virtualizeHeights;
private final Predicate<BlockPos> protectedPosition; private final Predicate<BlockPos> protectedPosition;
private final Holder<Biome> fallbackBiome; private final Holder<Biome> fallbackBiome;
private final BiomeManager biomeManager; private final BiomeManager biomeManager;
@@ -81,6 +82,7 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
boundary.surfaceFirstFreeY(), "Native structure world access requires a surface resolver"); boundary.surfaceFirstFreeY(), "Native structure world access requires a surface resolver");
this.floorFirstFreeY = Objects.requireNonNull( this.floorFirstFreeY = Objects.requireNonNull(
boundary.floorFirstFreeY(), "Native structure world access requires an ocean-floor resolver"); boundary.floorFirstFreeY(), "Native structure world access requires an ocean-floor resolver");
this.virtualizeHeights = boundary.virtualizeHeights();
this.protectedPosition = Objects.requireNonNull( this.protectedPosition = Objects.requireNonNull(
boundary.protectedPosition(), "World access requires a protected-position predicate"); boundary.protectedPosition(), "World access requires a protected-position predicate");
BlockPos biomeSample = generationCenter.getMiddleBlockPosition(delegate.getSeaLevel()); BlockPos biomeSample = generationCenter.getMiddleBlockPosition(delegate.getSeaLevel());
@@ -103,9 +105,10 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
static NativeStructureWorldgenAccess create(WorldGenLevel delegate, ChunkPos generationCenter, static NativeStructureWorldgenAccess create(WorldGenLevel delegate, ChunkPos generationCenter,
IntBinaryOperator surfaceFirstFreeY, IntBinaryOperator surfaceFirstFreeY,
IntBinaryOperator floorFirstFreeY, IntBinaryOperator floorFirstFreeY,
boolean virtualizeHeights,
Predicate<BlockPos> protectedPosition) { Predicate<BlockPos> protectedPosition) {
return new NativeStructureWorldgenAccess(delegate, new Boundary( return new NativeStructureWorldgenAccess(delegate, new Boundary(
generationCenter, surfaceFirstFreeY, floorFirstFreeY, protectedPosition)); generationCenter, surfaceFirstFreeY, floorFirstFreeY, virtualizeHeights, protectedPosition));
} }
@Override @Override
@@ -250,7 +253,7 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
@Override @Override
public int getHeight(Heightmap.Types type, int x, int z) { 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 delegate.getHeight(type, x, z);
} }
return height(type, x, z); return height(type, x, z);
@@ -549,6 +552,7 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
private record Boundary(ChunkPos generationCenter, private record Boundary(ChunkPos generationCenter,
IntBinaryOperator surfaceFirstFreeY, IntBinaryOperator surfaceFirstFreeY,
IntBinaryOperator floorFirstFreeY, IntBinaryOperator floorFirstFreeY,
boolean virtualizeHeights,
Predicate<BlockPos> protectedPosition) { Predicate<BlockPos> protectedPosition) {
} }
@@ -1,15 +1,29 @@
package art.arcane.iris.core.nms.v26_2_R1; 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 org.junit.Test;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Proxy;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
public class CustomBiomeSourceStructureContractTest { public class CustomBiomeSourceStructureContractTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test @Test
public void nativeStructuresUseTerrainSafeDerivativeAtEveryBiomeBoundary() throws IOException { public void nativeStructuresUseTerrainSafeDerivativeAtEveryBiomeBoundary() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource"))); String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource")));
@@ -69,14 +83,53 @@ public class CustomBiomeSourceStructureContractTest {
String realResolution = source.substring(realResolutionStart, naturalResolutionStart); String realResolution = source.substring(realResolutionStart, naturalResolutionStart);
String naturalResolution = source.substring(naturalResolutionStart, resolutionEnd); 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()")); assertFalse(realResolution.contains("getNaturalTrueBiomeStream()"));
assertTrue(naturalResolution.contains( assertTrue(naturalResolution.contains(
"engine.getComplex().getNaturalTrueBiomeStream().get(blockX, blockZ)")); "engine.getComplex().getNaturalTrueBiomeStream().get(blockX, blockZ)"));
assertFalse(naturalResolution.contains("getTrueBiomeStream()")); 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")); 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 @Test
public void visibleBiomeBatchReusesTheCallersGenerationLease() throws IOException { public void visibleBiomeBatchReusesTheCallersGenerationLease() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource"))); String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource")));
@@ -94,4 +147,126 @@ public class CustomBiomeSourceStructureContractTest {
< lookup.indexOf("getVisibleNoiseBiomeWithActiveGenerationLease(x, y, z, sampler)")); < lookup.indexOf("getVisibleNoiseBiomeWithActiveGenerationLease(x, y, z, sampler)"));
assertFalse(source.contains("studioBootstrapNoiseBiomeCache")); 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")); < placement.indexOf("for (NativePlacementGroup group"));
assertTrue(placement.indexOf("repairVacuumFoundations") assertTrue(placement.indexOf("repairVacuumFoundations")
> placement.indexOf("for (NativePlacementGroup group")); > placement.indexOf("for (NativePlacementGroup group"));
assertTrue(placement.contains("nativeStructureProtection(staticObjects)"));
assertTrue(placement.contains("Engine.hostHeight(engine, x, z, true)"));
assertFalse(placement.contains("IrisLogging.reportError")); assertFalse(placement.contains("IrisLogging.reportError"));
} }
@@ -542,8 +544,8 @@ public class IrisChunkGeneratorFailureContractTest {
String spawn = source.substring(spawnStart, spawnEnd); String spawn = source.substring(spawnStart, spawnEnd);
assertTrue(spawn.contains("customBiomeSource.getVanillaSpawnBiome(visibleBiome)")); assertTrue(spawn.contains("customBiomeSource.getVanillaSpawnBiome(visibleBiome)"));
assertTrue(spawn.contains("customBiomeSource.getVisibleSurfaceBiome("));
assertTrue(spawn.contains("NaturalSpawner.spawnMobsForChunkGeneration(")); 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("new LegacyRandomSource(RandomSupport.generateUniqueSeed())"));
assertTrue(spawn.contains("random.setDecorationSeed(region.getSeed()")); assertTrue(spawn.contains("random.setDecorationSeed(region.getSeed()"));
assertFalse(spawn.contains("delegate.spawnOriginalMobs")); assertFalse(spawn.contains("delegate.spawnOriginalMobs"));
@@ -135,6 +135,24 @@ public class NativeStructureWorldgenAccessTest {
assertEquals(1, recording.blockTicks.count()); 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 @Test
public void protectedCellsRemainReadableButRejectWritesTilesAndTicks() { public void protectedCellsRemainReadableButRejectWritesTilesAndTicks() {
RecordingDelegate recording = new RecordingDelegate(); RecordingDelegate recording = new RecordingDelegate();
@@ -142,7 +160,9 @@ public class NativeStructureWorldgenAccessTest {
NativeStructureWorldgenAccess access = NativeStructureWorldgenAccess.create( NativeStructureWorldgenAccess access = NativeStructureWorldgenAccess.create(
recording.world(), generationCenter(), recording.world(), generationCenter(),
(x, z) -> SURFACE_FIRST_FREE_Y, (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()); assertSame(Blocks.DIRT, access.getBlockState(position).getBlock());
assertFalse(access.ensureCanWrite(position)); assertFalse(access.ensureCanWrite(position));
@@ -211,7 +231,9 @@ public class NativeStructureWorldgenAccessTest {
return NativeStructureWorldgenAccess.create( return NativeStructureWorldgenAccess.create(
recording.world(), generationCenter(), recording.world(), generationCenter(),
(x, z) -> SURFACE_FIRST_FREE_Y, (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() { private static ChunkPos generationCenter() {
@@ -332,6 +332,18 @@ public class NativeStructureFactoryTest {
assertTrue(failureStart < source.indexOf("NativeStructureOwnershipStore.record(")); 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 @Test
public void failedManualStartPublicationClearsRecordedOwnershipAndPreservesTheFailure() throws Exception { public void failedManualStartPublicationClearsRecordedOwnershipAndPreservesTheFailure() throws Exception {
String source = Files.readString( String source = Files.readString(
@@ -84,6 +84,11 @@ public final class BukkitEnginePlatformHooks implements EnginePlatformHooks {
"iris"); "iris");
} }
@Override
public void prepareRuntimeHotload(Engine engine) {
INMS.get().invalidateNativeStructureVolumeIndex(engine);
}
@Override @Override
public void applyWorldBoundary(Engine engine) { public void applyWorldBoundary(Engine engine) {
IrisDimension expectedDimension = engine.getDimension(); IrisDimension expectedDimension = engine.getDimension();
@@ -163,7 +163,7 @@ public final class NativeStructureOwnershipRecovery {
level, level,
biome -> true, biome -> true,
generator.getSeaLevel(), 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); return NativeStructureFactory.generate(generationContext, holder, plan, references);
} }
@@ -90,7 +90,7 @@ public final class NativeStructureStartInjector {
chunk, chunk,
biome -> true, biome -> true,
context.generator().getSeaLevel(), context.generator().getSeaLevel(),
(x, z) -> context.engine().getHeight(x, z, true) (x, z) -> Engine.hostHeight(context.engine(), x, z, true)
+ context.engine().getMinHeight() + context.engine().getMinHeight()
); );
StructureStart generated = NativeStructureFactory.generate( 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) { public static KList<NativeStructureVolume> volumes(Engine engine, int minX, int minZ, int maxX, int maxZ) {
NativeStructureVolumeIndex index = engine == null ? null : INDEXES.get(engine); NativeStructureVolumeIndex index = engine == null ? null : INDEXES.get(engine);
return index == null ? NativeStructureVolume.NONE : index.resolve(engine, minX, minZ, maxX, maxZ); return index == null ? NativeStructureVolume.NONE : index.resolve(engine, minX, minZ, maxX, maxZ);
@@ -102,6 +114,12 @@ public final class NativeStructureVolumeIndex {
return ORIGIN_REACH_CHUNKS; 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) { KList<NativeStructureVolume> resolve(Engine engine, int minX, int minZ, int maxX, int maxZ) {
int fromChunkX = Math.min(minX, maxX) >> 4; int fromChunkX = Math.min(minX, maxX) >> 4;
int toChunkX = Math.max(minX, maxX) >> 4; int toChunkX = Math.max(minX, maxX) >> 4;
@@ -323,7 +341,7 @@ public final class NativeStructureVolumeIndex {
} }
private static IntBinaryOperator surfaceHeight(Engine engine) { 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, static KList<NativeStructureVolume> appendPieces(KList<NativeStructureVolume> volumes,
@@ -18,16 +18,22 @@
package art.arcane.iris.modded; 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.Engine;
import art.arcane.iris.engine.framework.GenerationSessionException; import art.arcane.iris.engine.framework.GenerationSessionException;
import art.arcane.iris.engine.framework.GenerationSessionLease; import art.arcane.iris.engine.framework.GenerationSessionLease;
import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom; 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.iris.util.project.context.IrisContext;
import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.math.RNG;
import com.mojang.datafixers.util.Pair; import com.mojang.datafixers.util.Pair;
import com.mojang.serialization.MapCodec; import com.mojang.serialization.MapCodec;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.Holder; import net.minecraft.core.Holder;
import net.minecraft.core.HolderLookup; import net.minecraft.core.HolderLookup;
import net.minecraft.core.QuartPos; import net.minecraft.core.QuartPos;
@@ -37,12 +43,15 @@ import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer; import net.minecraft.server.MinecraftServer;
import net.minecraft.util.RandomSource; 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.Biome;
import net.minecraft.world.level.biome.BiomeSource; import net.minecraft.world.level.biome.BiomeSource;
import net.minecraft.world.level.biome.Climate; import net.minecraft.world.level.biome.Climate;
import net.minecraft.world.level.levelgen.structure.Structure; import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureSet; import net.minecraft.world.level.levelgen.structure.StructureSet;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
@@ -196,11 +205,16 @@ final class IrisModdedBiomeSource extends BiomeSource {
private List<Holder<Biome>> collectPossibleBiomeHolders() { private List<Holder<Biome>> collectPossibleBiomeHolders() {
BiomeKeySets keys = biomeKeySets(); BiomeKeySets keys = biomeKeySets();
Set<String> generatedBiomeKeys = requireConfiguredStructureBiomeKeys(keys.required()); LinkedHashSet<String> generatedBiomeKeys = new LinkedHashSet<>(
Set<String> visibleBiomeKeys = keys.visibleOnly(); requireConfiguredStructureBiomeKeys(keys.structureRequired()));
generatedBiomeKeys.addAll(keys.visibleRequired());
LinkedHashSet<String> visibleBiomeKeys = new LinkedHashSet<>(keys.visibleOnly());
Registry<Biome> registry = biomeRegistry(); Registry<Biome> registry = biomeRegistry();
LinkedHashSet<Holder<Biome>> possible = new LinkedHashSet<>(); LinkedHashSet<Holder<Biome>> possible = new LinkedHashSet<>();
if (registry == null) { if (registry == null) {
if (keys.visibleFallbackPossible()) {
visibleBiomeKeys.add("minecraft:plains");
}
for (Holder<Biome> biome : serializedSource.possibleBiomes()) { for (Holder<Biome> biome : serializedSource.possibleBiomes()) {
String key = holderKey(biome); String key = holderKey(biome);
if (isGeneratedBiomeKey(key, generatedBiomeKeys) if (isGeneratedBiomeKey(key, generatedBiomeKeys)
@@ -215,6 +229,9 @@ final class IrisModdedBiomeSource extends BiomeSource {
throw new IllegalStateException("Iris structure biomes are not registered: " throw new IllegalStateException("Iris structure biomes are not registered: "
+ missingBiomeKeys); + missingBiomeKeys);
} }
if (needsVisibleFallback(keys, registry)) {
visibleBiomeKeys.add("minecraft:plains");
}
// Registry-ordered, never a hash-ordered walk: see possibleBiomes(). // Registry-ordered, never a hash-ordered walk: see possibleBiomes().
registry.listElements().forEach((Holder.Reference<Biome> reference) -> { registry.listElements().forEach((Holder.Reference<Biome> reference) -> {
String key = holderKey(reference); String key = holderKey(reference);
@@ -223,7 +240,7 @@ final class IrisModdedBiomeSource extends BiomeSource {
possible.add(reference); possible.add(reference);
} }
}); });
warnUnregisteredVisibleBiomes(registry, visibleBiomeKeys); warnUnregisteredVisibleBiomes(registry, keys.visibleOnly());
} }
if (possible.isEmpty()) { if (possible.isEmpty()) {
String phase = registry == null ? "serialized biome bootstrap" : "biome registry"; String phase = registry == null ? "serialized biome bootstrap" : "biome registry";
@@ -233,6 +250,36 @@ final class IrisModdedBiomeSource extends BiomeSource {
return List.copyOf(possible); 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 * 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. * 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, private Holder<Biome> getNoiseBiome(Engine engine, int quartX, int quartY, int quartZ,
Climate.Sampler sampler) { Climate.Sampler sampler) {
if (isGuaranteedSurfaceBiome(quartY, engine.getMinHeight())) { if (isGuaranteedSurfaceBiome(quartY, engine.getMinHeight())) {
return getSurfaceStructureBiome(engine, quartX, quartZ, sampler); return getSurfaceStructureBiome(engine, quartX, quartZ, sampler);
} }
boolean cacheable = isBiomeCacheable(
engine,
QuartPos.toBlock(quartX),
QuartPos.toBlock(quartZ)
);
long key = packNoiseKey(quartX, quartY, quartZ); long key = packNoiseKey(quartX, quartY, quartZ);
BiomeHolderTable cache = structureBiomeCache; BiomeHolderTable cache = structureBiomeCache;
Holder<Biome> cached = cache.get(key); if (cacheable) {
if (cached != null) { Holder<Biome> cached = cache.get(key);
return cached; if (cached != null) {
return cached;
}
} }
Holder<Biome> resolved = resolveStructureBiome(engine, quartX, quartY, quartZ, sampler); Holder<Biome> resolved = resolveStructureBiome(engine, quartX, quartY, quartZ, sampler);
cache.put(key, resolved); if (cacheable) {
cache.put(key, resolved);
}
return resolved; return resolved;
} }
@@ -302,14 +421,81 @@ final class IrisModdedBiomeSource extends BiomeSource {
if (!isReady(engine)) { if (!isReady(engine)) {
throw new IllegalStateException("Iris visible biome lookup has no active engine runtime"); throw new IllegalStateException("Iris visible biome lookup has no active engine runtime");
} }
long key = packNoiseKey(quartX, quartY, quartZ); return getVisibleNoiseBiomeWithActiveGenerationLease(
BiomeHolderTable cache = visibleBiomeCache; 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); Holder<Biome> cached = cache.get(key);
if (cached != null) { if (cached != null) {
return cached; 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); 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; return resolved;
} }
} }
@@ -395,7 +581,9 @@ final class IrisModdedBiomeSource extends BiomeSource {
if (registry == null) { if (registry == null) {
throw new IllegalStateException("Iris structure biome lookup has no biome registry"); 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) { if (irisBiome == null) {
throw new IllegalStateException("Iris returned no surface structure biome at quart " throw new IllegalStateException("Iris returned no surface structure biome at quart "
+ quartX + "," + quartZ); + quartX + "," + quartZ);
@@ -412,7 +600,8 @@ final class IrisModdedBiomeSource extends BiomeSource {
private Holder<Biome> resolveStructureBiome(Engine engine, int quartX, int quartY, int quartZ, private Holder<Biome> resolveStructureBiome(Engine engine, int quartX, int quartY, int quartZ,
Climate.Sampler sampler) { Climate.Sampler sampler) {
Registry<Biome> registry = biomeRegistry(); Registry<Biome> registry = biomeRegistry();
BiomeResolution resolution = resolveBiomeResolution(engine, quartX, quartY, quartZ); BiomeResolution resolution = resolveStructureBiomeResolution(
engine, quartX, quartY, quartZ);
if (resolution == null) { if (resolution == null) {
throw new IllegalStateException("Iris returned no structure biome at quart " throw new IllegalStateException("Iris returned no structure biome at quart "
+ quartX + "," + quartY + "," + quartZ); + quartX + "," + quartY + "," + quartZ);
@@ -431,11 +620,36 @@ final class IrisModdedBiomeSource extends BiomeSource {
private Holder<Biome> resolveVisibleBiome(Engine engine, int quartX, int quartY, int quartZ, private Holder<Biome> resolveVisibleBiome(Engine engine, int quartX, int quartY, int quartZ,
Climate.Sampler sampler) { 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(); 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) { if (resolution == null || registry == null) {
return serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler); 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; String biomeKey;
if (resolution.irisBiome().isCustom()) { if (resolution.irisBiome().isCustom()) {
IrisBiomeCustom customBiome = resolution.irisBiome().getCustomBiome( IrisBiomeCustom customBiome = resolution.irisBiome().getCustomBiome(
@@ -444,7 +658,8 @@ final class IrisModdedBiomeSource extends BiomeSource {
return fallbackBiome(registry, "custom derivative of '" return fallbackBiome(registry, "custom derivative of '"
+ resolution.irisBiome().getLoadKey() + "'", quartX, quartY, quartZ, sampler); + 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()) { } else if (resolution.underground()) {
biomeKey = resolution.irisBiome().getGroundBiomeKey( biomeKey = resolution.irisBiome().getGroundBiomeKey(
resolution.rng(), engine, resolution.blockX(), resolution.blockY(), resolution.blockZ()); 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) { 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)) { if (!isReady(engine)) {
return null; return null;
} }
@@ -467,27 +713,75 @@ final class IrisModdedBiomeSource extends BiomeSource {
int blockZ = quartZ << 2; int blockZ = quartZ << 2;
int internalY = blockY - engine.getMinHeight(); int internalY = blockY - engine.getMinHeight();
int caveSwitchY = Math.max(-8 - engine.getMinHeight(), 40); 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; boolean underground = false;
IrisBiome irisBiome; IrisBiome irisBiome;
if (internalY <= caveSwitchY) { if (stackLayer != null) {
int surfaceY = engine.getComplex().getHeightStream().get(blockX, blockZ).intValue(); 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); underground = isUnderground(internalY, surfaceY);
irisBiome = underground irisBiome = underground
? engine.getCaveBiome(blockX, internalY, blockZ) ? engine.getCaveBiome(blockX, internalY, blockZ)
: engine.getComplex().getTrueBiomeStream().get(blockX, blockZ); : engine.getHostSurfaceBiome(blockX, blockZ);
} else { } else {
irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ); irisBiome = engine.getHostSurfaceBiome(blockX, blockZ);
} }
if (irisBiome == null && underground) { if (irisBiome == null && underground) {
irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ); irisBiome = stackLayer == null
? engine.getHostSurfaceBiome(blockX, blockZ)
: stackLayer.biome();
} }
if (irisBiome == null) { if (irisBiome == null) {
return null; return null;
} }
IrisModdedChunkGenerator current = generator; return createBiomeResolution(
long worldSeed = current == null ? engine.getWorld().getRawWorldSeed() : current.visibleBiomeSeed(); engine,
long seed = biomeResolutionSeed(worldSeed, blockX, blockY, blockZ); irisBiome,
return new BiomeResolution(irisBiome, underground, blockX, blockY, blockZ, new RNG(seed)); underground,
owningDimension,
owningData,
blockX,
blockY,
blockZ
);
} }
private Holder<Biome> resolveRequiredStructureBiome(int quartX, int quartY, int quartZ) { private Holder<Biome> resolveRequiredStructureBiome(int quartX, int quartY, int quartZ) {
@@ -510,9 +804,10 @@ final class IrisModdedBiomeSource extends BiomeSource {
} }
IrisBiome irisBiome; IrisBiome irisBiome;
if (isGuaranteedSurfaceBiome(quartY, engine.getMinHeight())) { if (isGuaranteedSurfaceBiome(quartY, engine.getMinHeight())) {
irisBiome = engine.getComplex().getTrueBiomeStream().get(quartX << 2, quartZ << 2); irisBiome = resolveSurfaceStructureBiome(engine, quartX << 2, quartZ << 2);
} else { } else {
BiomeResolution resolution = resolveBiomeResolution(engine, quartX, quartY, quartZ); BiomeResolution resolution = resolveStructureBiomeResolution(
engine, quartX, quartY, quartZ);
irisBiome = resolution == null ? null : resolution.irisBiome(); irisBiome = resolution == null ? null : resolution.irisBiome();
} }
if (irisBiome == null) { if (irisBiome == null) {
@@ -557,6 +852,10 @@ final class IrisModdedBiomeSource extends BiomeSource {
return internalY <= surfaceY - 8; 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) { static boolean isMonumentSurfaceBiomeQuery(int blockY, int radius, int minHeight, int fluidHeight) {
return radius == 29 && blockY == minHeight + fluidHeight; return radius == 29 && blockY == minHeight + fluidHeight;
} }
@@ -612,7 +911,7 @@ final class IrisModdedBiomeSource extends BiomeSource {
} }
private Set<String> exactStructureBiomeKeys() { private Set<String> exactStructureBiomeKeys() {
return biomeKeySets().required(); return biomeKeySets().structureRequired();
} }
/** /**
@@ -636,11 +935,12 @@ final class IrisModdedBiomeSource extends BiomeSource {
private BiomeKeySets collectBiomeKeySets(long generation) { private BiomeKeySets collectBiomeKeySets(long generation) {
IrisModdedChunkGenerator current = generator; IrisModdedChunkGenerator current = generator;
if (current == null) { 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(); Engine engine = current.structureEngineOrNull();
if (engine == null) { 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"); GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_structure_biome_keys");
if (lease == null) { if (lease == null) {
@@ -650,36 +950,154 @@ final class IrisModdedBiomeSource extends BiomeSource {
if (!isReady(engine)) { if (!isReady(engine)) {
throw new IllegalStateException("Iris structure biome key lookup has no active engine runtime"); throw new IllegalStateException("Iris structure biome key lookup has no active engine runtime");
} }
LinkedHashSet<String> required = new LinkedHashSet<>(); Set<String> structureRequired = collectStructureBiomeKeys(
LinkedHashSet<String> visible = new LinkedHashSet<>(); engine.getDimension(), engine.getData());
for (IrisBiome irisBiome : engine.getAllBiomes()) { LinkedHashSet<String> visibleRequired = new LinkedHashSet<>();
String derivative = normalizeKey(irisBiome.getStructureDerivativeKey()); LinkedHashSet<String> visibleEmittable = new LinkedHashSet<>();
if (derivative != null) { boolean visibleFallbackPossible = false;
required.add(derivative); for (OwnedBiome ownedBiome : getOwnedBiomes(engine)) {
} IrisBiome irisBiome = ownedBiome.biome();
if (irisBiome.isCustom()) { if (irisBiome.isCustom()) {
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) { 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()) { for (String scatter : irisBiome.getBiomeScatter()) {
addVisibleBiomeKey(visible, scatter); visibleFallbackPossible |= !addVisibleBiomeKey(visibleEmittable, scatter);
} }
for (String scatter : irisBiome.getBiomeSkyScatter()) { for (String scatter : irisBiome.getBiomeSkyScatter()) {
addVisibleBiomeKey(visible, scatter); visibleFallbackPossible |= !addVisibleBiomeKey(visibleEmittable, scatter);
} }
} }
visible.removeAll(required); LinkedHashSet<String> visible = new LinkedHashSet<>(visibleEmittable);
return new BiomeKeySets(generation, Set.copyOf(required), Set.copyOf(visible)); 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) { static Set<String> collectStructureBiomeKeys(IrisDimension dimension, IrisData data) {
String normalized = normalizeKey(key); LinkedHashSet<String> keys = new LinkedHashSet<>();
if (normalized != null) { for (IrisBiome irisBiome : dimension.getReachableBiomes(() -> data)) {
target.add(normalized); 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) { private static Set<String> registeredBiomeKeys(Registry<Biome> registry) {
@@ -726,7 +1144,13 @@ final class IrisModdedBiomeSource extends BiomeSource {
Holder<Biome> plains = resolveHolder(registry, "minecraft:plains"); Holder<Biome> plains = resolveHolder(registry, "minecraft:plains");
warnUnresolvedBiome(unresolvedKey, plains == null ? "the serialized biome source" : "minecraft:plains", warnUnresolvedBiome(unresolvedKey, plains == null ? "the serialized biome source" : "minecraft:plains",
quartX, quartY, quartZ); 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, private void warnUnresolvedBiome(String unresolvedKey, String fallback,
@@ -753,11 +1177,29 @@ final class IrisModdedBiomeSource extends BiomeSource {
return ((long) x << 32) ^ ((long) z & 4294967295L); return ((long) x << 32) ^ ((long) z & 4294967295L);
} }
private record BiomeResolution(IrisBiome irisBiome, boolean underground, int blockX, int blockY, private record BiomeResolution(
int blockZ, RNG rng) { 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) { 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.NativeStructureStartInjector;
import art.arcane.iris.nativegen.NativeStructureReferenceRepair; import art.arcane.iris.nativegen.NativeStructureReferenceRepair;
import art.arcane.iris.nativegen.NativeStructureVanillaLocator; import art.arcane.iris.nativegen.NativeStructureVanillaLocator;
import art.arcane.iris.nativegen.NativeStructureVolumeIndex;
import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBiome; import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState; import art.arcane.iris.spi.PlatformBlockState;
@@ -89,7 +90,9 @@ import java.util.EnumSet;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
@@ -181,11 +184,18 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
} }
requireGlobalStructureGeneration( requireGlobalStructureGeneration(
level.getServer().getWorldGenSettings().options().generateStructures(), dimensionKey); level.getServer().getWorldGenSettings().options().generateStructures(), dimensionKey);
Engine activeEngine = engineIfBound();
Engine replacement = ModdedWorldEngines.prepareReplacement(level, pack, packDimensionKey, seed); Engine replacement = ModdedWorldEngines.prepareReplacement(level, pack, packDimensionKey, seed);
try { try {
if (activeEngine != null) {
requireStructureBiomeUniverseCompatible(
activeEngine.getDimension(), replacement.getDimension());
}
nativeStructures.installVolumeIndex(level, replacement);
ModdedWorldEngines.installReplacement(level, replacement); ModdedWorldEngines.installReplacement(level, replacement);
replacement.getPlatformHooks().applyWorldBoundary(replacement); replacement.getPlatformHooks().applyWorldBoundary(replacement);
} catch (Throwable error) { } catch (Throwable error) {
NativeStructureVolumeIndex.uninstall(replacement);
try { try {
ModdedWorldEngines.closeUnregistered(replacement); ModdedWorldEngines.closeUnregistered(replacement);
} catch (Throwable cleanupError) { } catch (Throwable cleanupError) {
@@ -203,17 +213,14 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
this.activePack = pack; this.activePack = pack;
this.activeDimensionKey = packDimensionKey; this.activeDimensionKey = packDimensionKey;
this.seedOverride = seed; this.seedOverride = seed;
this.engine = replacement;
this.configuredStructureBiomeKeys = null; this.configuredStructureBiomeKeys = null;
this.configuredPack = null; this.configuredPack = null;
this.heightMetadata = engineHeights(replacement); this.heightMetadata = engineHeights(replacement);
this.engineBinding.reset(); this.engineBinding.reset();
this.engineBinding.complete(replacement);
this.announced.set(false); this.announced.set(false);
this.structureBiomeSource.clearCaches(); resetRuntimeCaches();
this.importedFeatures.invalidate(); this.engine = replacement;
this.nativeStructures.clearWorldCheckStructureShifts(); this.engineBinding.complete(replacement);
this.spawnTables.resetVanillaSpawnBiomes();
// Bind time: a feature-order cycle in the new pack is reported here, once, and degrades to features-off. // 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 // Never waits on a build owned by another thread: this method owns the generator monitor and the build
// path can need it. // path can need it.
@@ -751,7 +758,38 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
return lastChunkGenAt; 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; configuredStructureBiomeKeys = null;
structureBiomeSource.clearCaches(); structureBiomeSource.clearCaches();
importedFeatures.invalidate(); importedFeatures.invalidate();
@@ -986,7 +1024,17 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
Registry<Biome> registry = region.registryAccess().lookupOrThrow(Registries.BIOME); Registry<Biome> registry = region.registryAccess().lookupOrThrow(Registries.BIOME);
spawnTables.initializeVanillaSpawnBiomes(registry); spawnTables.initializeVanillaSpawnBiomes(registry);
ChunkPos center = region.getCenter(); 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()); Holder<Biome> vanillaBiome = spawnTables.vanillaSpawnBiome(visibleBiome.value());
WorldgenRandom random = new WorldgenRandom(new LegacyRandomSource(RandomSupport.generateUniqueSeed())); WorldgenRandom random = new WorldgenRandom(new LegacyRandomSource(RandomSupport.generateUniqueSeed()));
random.setDecorationSeed(region.getSeed(), center.getMinBlockX(), center.getMinBlockZ()); 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()); boolean ignoreFluid = !type.isOpaque().test(Blocks.WATER.defaultBlockState());
try (GenerationSessionLease lease = current.acquireGenerationLease("modded_base_height"); try (GenerationSessionLease lease = current.acquireGenerationLease("modded_base_height");
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) { 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) { } catch (GenerationSessionException e) {
throw new IllegalStateException("Iris base height query could not acquire its engine runtime.", 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"); try (GenerationSessionLease lease = current.acquireGenerationLease("modded_base_column");
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) { IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
BlockState airState = Blocks.AIR.defaultBlockState(); BlockState airState = Blocks.AIR.defaultBlockState();
int surface = current.getHeight(x, z, true); boolean dimensionStack = current.getDimensionStackContext() != null;
int fluid = current.getHeight(x, z, false); 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 stone = Blocks.STONE.defaultBlockState();
BlockState water = Blocks.WATER.defaultBlockState(); BlockState water = Blocks.WATER.defaultBlockState();
int solidEnd = Math.max(0, Math.min(states.length, surface + 1)); 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.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom; 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.PlatformBiome;
import art.arcane.iris.spi.PlatformBiomeWriter; import art.arcane.iris.spi.PlatformBiomeWriter;
import art.arcane.iris.util.project.context.IrisContext; import art.arcane.iris.util.project.context.IrisContext;
@@ -97,7 +98,7 @@ public final class ModdedBiomeWriter implements PlatformBiomeWriter {
return key; return key;
} }
Engine engine = context.getEngine(); Engine engine = context.getEngine();
String prefix = engine.getDimension().getLoadKey() + ":"; String prefix = engine.getDimension().getCustomBiomeKeyPrefix();
if (!key.regionMatches(true, 0, prefix, 0, prefix.length())) { if (!key.regionMatches(true, 0, prefix, 0, prefix.length())) {
return key; return key;
} }
@@ -138,13 +139,7 @@ public final class ModdedBiomeWriter implements PlatformBiomeWriter {
} }
private int idForDerivative(Registry<Biome> registry, String key) { private int idForDerivative(Registry<Biome> registry, String key) {
int colon = key.indexOf(':'); IrisBiome owner = findCustomBiomeOwner(key);
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);
if (owner == null) { if (owner == null) {
return -1; return -1;
} }
@@ -155,20 +150,18 @@ public final class ModdedBiomeWriter implements PlatformBiomeWriter {
return idForKey(registry, derivativeKey); return idForKey(registry, derivativeKey);
} }
private IrisBiome findCustomBiomeOwner(String dimensionLoadKey, String customBiomeId) { private IrisBiome findCustomBiomeOwner(String key) {
for (Engine engine : ModdedWorldEngines.activeEngines()) { for (Engine engine : ModdedWorldEngines.activeEngines()) {
if (engine == null || engine.isClosed()) { if (engine == null || engine.isClosed()) {
continue; continue;
} }
if (!dimensionLoadKey.equalsIgnoreCase(engine.getDimension().getLoadKey())) { IrisDimension dimension = engine.getDimension();
continue; for (IrisBiome biome : dimension.getAllBiomes(engine)) {
}
for (IrisBiome biome : engine.getDimension().getAllBiomes(engine)) {
if (!biome.isCustom()) { if (!biome.isCustom()) {
continue; continue;
} }
for (IrisBiomeCustom custom : biome.getCustomDerivitives()) { for (IrisBiomeCustom custom : biome.getCustomDerivitives()) {
if (customBiomeId.equals(custom.getId())) { if (key.equalsIgnoreCase(dimension.getCustomBiomeKey(custom.getId()))) {
return biome; return biome;
} }
} }
@@ -258,7 +258,7 @@ public final class ModdedBlockBreakHandler {
if (skipsParents(providers)) { if (skipsParents(providers)) {
return providers; return providers;
} }
IrisRegion region = engine.getRegion(position.getX(), position.getZ()); IrisRegion region = engine.getRegion(position.getX(), relativeY, position.getZ());
if (region != null) { if (region != null) {
addMatching(providers, region.getBlockDrops(), brokenState, data); addMatching(providers, region.getBlockDrops(), brokenState, data);
} }
@@ -62,7 +62,6 @@ final class ModdedDimensionMetadata {
static Set<String> collectConfiguredBiomeKeys(Iterable<IrisBiome> biomes, String dimensionLoadKey) { static Set<String> collectConfiguredBiomeKeys(Iterable<IrisBiome> biomes, String dimensionLoadKey) {
LinkedHashSet<String> keys = new LinkedHashSet<>(); LinkedHashSet<String> keys = new LinkedHashSet<>();
String namespace = dimensionLoadKey.toLowerCase(Locale.ROOT);
for (IrisBiome irisBiome : biomes) { for (IrisBiome irisBiome : biomes) {
if (irisBiome == null) { if (irisBiome == null) {
continue; continue;
@@ -75,7 +74,7 @@ final class ModdedDimensionMetadata {
continue; continue;
} }
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) { for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
keys.add(namespace + ":" + customBiome.getId().toLowerCase(Locale.ROOT)); keys.add(IrisDimension.customBiomeKey(dimensionLoadKey, customBiome.getId()));
} }
} }
return Set.copyOf(keys); return Set.copyOf(keys);
@@ -226,7 +226,7 @@ public final class ModdedEngineEffects extends EngineAssignedComponent implement
int blockY = floor(player.getY()) - getEngine().getWorld().minHeight(); int blockY = floor(player.getY()) - getEngine().getWorld().minHeight();
int blockZ = floor(player.getZ()); int blockZ = floor(player.getZ());
state.biome(getEngine().getBiome(blockX, blockY, blockZ)); 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) { 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.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator; import java.util.Comparator;
import java.util.HexFormat; import java.util.HexFormat;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
@@ -514,18 +513,13 @@ public final class ModdedForcedDatapack {
if (!dimensionsDirectory.isDirectory()) { if (!dimensionsDirectory.isDirectory()) {
return false; return false;
} }
File[] dimensionFiles = dimensionsDirectory.listFiles( IrisData data = IrisData.get(packFolder);
(File file) -> file.isFile() && file.getName().endsWith(".json")); String[] dimensionKeys = data.getDimensionLoader().getPossibleKeys();
if (dimensionFiles == null) { if (dimensionKeys == null || dimensionKeys.length == 0) {
throw new IOException("Iris could not read dimensions for pack '" + packName + "'");
}
if (dimensionFiles.length == 0) {
return false; return false;
} }
Arrays.sort(dimensionFiles, Comparator.comparing(File::getName)); List<String> sortedDimensionKeys = Stream.of(dimensionKeys).sorted().toList();
IrisData data = IrisData.get(packFolder); for (String dimensionKey : sortedDimensionKeys) {
for (File dimensionFile : dimensionFiles) {
String dimensionKey = dimensionFile.getName().substring(0, dimensionFile.getName().length() - ".json".length());
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey); IrisDimension dimension = data.getDimensionLoader().load(dimensionKey);
if (dimension == null) { if (dimension == null) {
throw new IllegalStateException("Iris pack '" + packName + "' dimension '" throw new IllegalStateException("Iris pack '" + packName + "' dimension '"
@@ -18,6 +18,8 @@
package art.arcane.iris.modded; 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.Engine;
import art.arcane.iris.engine.framework.GenerationSessionLease; import art.arcane.iris.engine.framework.GenerationSessionLease;
import art.arcane.iris.engine.framework.NativeFeatureGenerationPolicy; import art.arcane.iris.engine.framework.NativeFeatureGenerationPolicy;
@@ -57,7 +59,10 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock; 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}. * 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) { private FeatureTable buildTable(Engine engine, IrisImportedFeatureControl control, long generation) {
// Registry-ordered biome list. FeatureSorter's cycle detection walks it, so an unordered list makes // 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. // 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()) { if (biomes.isEmpty()) {
ModdedIrisLog.error("Iris importedFeatures is on but {} exposes no biomes; features off", ModdedIrisLog.error("Iris importedFeatures is on but {} exposes no biomes; features off",
dimensionKey(engine)); dimensionKey(engine));
@@ -262,7 +274,7 @@ final class ModdedImportedFeatureStage {
*/ */
private Map<String, Holder<Biome>> customBiomeDerivatives(Engine engine, Map<String, Holder<Biome>> byKey) { private Map<String, Holder<Biome>> customBiomeDerivatives(Engine engine, Map<String, Holder<Biome>> byKey) {
Map<String, Holder<Biome>> derivatives = new HashMap<>(); Map<String, Holder<Biome>> derivatives = new HashMap<>();
for (IrisBiome irisBiome : engine.getAllBiomes()) { for (IrisBiome irisBiome : engine.getDimension().getReachableBiomes(engine)) {
if (!irisBiome.isCustom()) { if (!irisBiome.isCustom()) {
continue; continue;
} }
@@ -322,14 +334,24 @@ final class ModdedImportedFeatureStage {
List<FeatureSorter.StepFeatureData> steps = table.steps(); List<FeatureSorter.StepFeatureData> steps = table.steps();
WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed())); WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed()));
long decorationSeed = random.setDecorationSeed(level.getSeed(), origin.getX(), origin.getZ()); 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()); IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData());
int staticMinY = engine.getMinHeight(); 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, level, centerPos,
(x, z) -> level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z), surfaceFirstFreeY,
(x, z) -> level.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, x, z), floorFirstFreeY,
position -> staticObjects.contains(position.getX(), position.getY() - staticMinY, position.getZ())); stackContext != null,
importedFeatureProtection(staticObjects, stackContext, staticMinY));
try { try {
for (int stepIndex = 0; stepIndex < steps.size(); stepIndex++) { for (int stepIndex = 0; stepIndex < steps.size(); stepIndex++) {
@@ -349,6 +371,58 @@ final class ModdedImportedFeatureStage {
WorldCheckFeaturePlacement.recordPlacementPass(); 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, private void placeStep(WorldGenLevel level, FeatureTable table, FeatureSorter.StepFeatureData stepData,
Registry<PlacedFeature> featureRegistry, Set<Holder<Biome>> chunkBiomes, Registry<PlacedFeature> featureRegistry, Set<Holder<Biome>> chunkBiomes,
IrisModdedChunkGenerator owner, WorldgenRandom random, long decorationSeed, 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 * 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. * 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<>(); List<Holder<Biome>> collected = new ArrayList<>();
ChunkPos.rangeClosed(sectionPos.chunk(), 1).forEach((ChunkPos chunkPos) -> { ChunkPos.rangeClosed(sectionPos.chunk(), 1).forEach((ChunkPos chunkPos) -> {
ChunkAccess neighbour = level.getChunk(chunkPos.x(), chunkPos.z()); ChunkAccess neighbour = level.getChunk(chunkPos.x(), chunkPos.z());
for (LevelChunkSection section : neighbour.getSections()) { LevelChunkSection[] sections = neighbour.getSections();
section.getBiomes().getAll(collected::add); 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<>(); Set<Holder<Biome>> present = new LinkedHashSet<>();
@@ -18,6 +18,8 @@
package art.arcane.iris.modded; 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.Engine;
import art.arcane.iris.engine.framework.IrisStructureLocator; import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy; import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
@@ -78,6 +80,7 @@ import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.IntBinaryOperator; import java.util.function.IntBinaryOperator;
import java.util.function.Predicate;
/** /**
* Native (vanilla registry) structure stage for {@link IrisModdedChunkGenerator}. The generator keeps the * Native (vanilla registry) structure stage for {@link IrisModdedChunkGenerator}. The generator keeps the
@@ -266,7 +269,7 @@ final class ModdedNativeStructureStage {
undergroundStep, undergroundStep,
decision.preserveSourceY(), decision.preserveSourceY(),
decision.yBand(), 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( StructureStart wrapped = NativeStructureReferenceEnvelope.wrapForPublication(
start, structure, start.getReferences(), start, structure, start.getReferences(),
NativeStructureTerrainIntegrator.resolveNativeTerrain(start, decision.terrain()), NativeStructureTerrainIntegrator.resolveNativeTerrain(start, decision.terrain()),
@@ -367,10 +370,14 @@ final class ModdedNativeStructureStage {
chunkPos.x(), chunkPos.z(), error); chunkPos.x(), chunkPos.z(), error);
} }
IrisStaticObjectLayer staticObjects = current.getDimension().getStaticObjectLayer(current.getData()); IrisStaticObjectLayer staticObjects = current.getDimension().getStaticObjectLayer(current.getData());
int staticMinY = current.getMinHeight(); Predicate<BlockPos> protectedPosition = nativeStructureProtection(current, staticObjects);
WorldGenLevel boundedWorld = staticObjects.isEmpty() ? world : ModdedNativeStructureWorldgenAccess.create( WorldGenLevel boundedWorld = staticObjects.isEmpty()
&& current.getDimensionStackContext() == null
? world
: ModdedNativeStructureWorldgenAccess.create(
world, chunkPos, worldgenSurfaceHeight(current, world.getMinY()), worldgenFloorHeight(current, world.getMinY()), 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 { try {
NativeStructureVegetationClearer.clearIntersectingVegetation( NativeStructureVegetationClearer.clearIntersectingVegetation(
boundedWorld, chunk, area, vegetationTargets); boundedWorld, chunk, area, vegetationTargets);
@@ -383,7 +390,7 @@ final class ModdedNativeStructureStage {
try { try {
vacuumFoundationPlan = NativeStructureSurfaceFitter.prepareSurfaceStructures( vacuumFoundationPlan = NativeStructureSurfaceFitter.prepareSurfaceStructures(
boundedWorld, area, terrainTargets, 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) { } catch (Throwable error) {
throw NativeStructureGenerationException.failure( throw NativeStructureGenerationException.failure(
"terrain integration", nativeStructureBatchContext(placementGroups), "terrain integration", nativeStructureBatchContext(placementGroups),
@@ -443,18 +450,50 @@ final class ModdedNativeStructureStage {
IrisNativeStructureDecision decision) { IrisNativeStructureDecision decision) {
Engine current = generator.engine(); Engine current = generator.engine();
WorldGenLevel boundedWorld = world instanceof ModdedNativeStructureWorldgenAccess ? world : ModdedNativeStructureWorldgenAccess.create( 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); world.setCurrentlyGenerating(() -> "Iris native structure " + structureId);
try { try {
NativeStructurePostProcessor.place( NativeStructurePostProcessor.place(
boundedWorld, structureManager, generator, random, area, chunkPos, boundedWorld, structureManager, generator, random, area, chunkPos,
structureId, start, decision, this::resolvePaletteBlock, 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 { } finally {
world.setCurrentlyGenerating(null); 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) { private List<List<Structure>> structuresByStep(Registry<Structure> registry) {
StructureStepCache cached = structureStepCache; StructureStepCache cached = structureStepCache;
if (cached != null && cached.registry() == registry) { if (cached != null && cached.registry() == registry) {
@@ -523,11 +562,11 @@ final class ModdedNativeStructureStage {
} }
private IntBinaryOperator worldgenSurfaceHeight(Engine generationEngine, int runtimeMinY) { 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) { 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) { private record NativeStructureStartKey(String structureId, long chunkPosition) {
@@ -63,6 +63,7 @@ final class ModdedNativeStructureWorldgenAccess implements WorldGenLevel {
private final ChunkPos generationCenter; private final ChunkPos generationCenter;
private final IntBinaryOperator surfaceFirstFreeY; private final IntBinaryOperator surfaceFirstFreeY;
private final IntBinaryOperator floorFirstFreeY; private final IntBinaryOperator floorFirstFreeY;
private final boolean virtualizeHeights;
private final Predicate<BlockPos> protectedPosition; private final Predicate<BlockPos> protectedPosition;
private final Holder<Biome> fallbackBiome; private final Holder<Biome> fallbackBiome;
private final BiomeManager biomeManager; private final BiomeManager biomeManager;
@@ -80,6 +81,7 @@ final class ModdedNativeStructureWorldgenAccess implements WorldGenLevel {
boundary.surfaceFirstFreeY(), "Native structure world access requires a surface resolver"); boundary.surfaceFirstFreeY(), "Native structure world access requires a surface resolver");
this.floorFirstFreeY = Objects.requireNonNull( this.floorFirstFreeY = Objects.requireNonNull(
boundary.floorFirstFreeY(), "Native structure world access requires an ocean-floor resolver"); boundary.floorFirstFreeY(), "Native structure world access requires an ocean-floor resolver");
this.virtualizeHeights = boundary.virtualizeHeights();
this.protectedPosition = Objects.requireNonNull( this.protectedPosition = Objects.requireNonNull(
boundary.protectedPosition(), "World access requires a protected-position predicate"); boundary.protectedPosition(), "World access requires a protected-position predicate");
BlockPos biomeSample = generationCenter.getMiddleBlockPosition(delegate.getSeaLevel()); BlockPos biomeSample = generationCenter.getMiddleBlockPosition(delegate.getSeaLevel());
@@ -102,9 +104,10 @@ final class ModdedNativeStructureWorldgenAccess implements WorldGenLevel {
static ModdedNativeStructureWorldgenAccess create(WorldGenLevel delegate, ChunkPos generationCenter, static ModdedNativeStructureWorldgenAccess create(WorldGenLevel delegate, ChunkPos generationCenter,
IntBinaryOperator surfaceFirstFreeY, IntBinaryOperator surfaceFirstFreeY,
IntBinaryOperator floorFirstFreeY, IntBinaryOperator floorFirstFreeY,
boolean virtualizeHeights,
Predicate<BlockPos> protectedPosition) { Predicate<BlockPos> protectedPosition) {
return new ModdedNativeStructureWorldgenAccess(delegate, new Boundary( return new ModdedNativeStructureWorldgenAccess(delegate, new Boundary(
generationCenter, surfaceFirstFreeY, floorFirstFreeY, protectedPosition)); generationCenter, surfaceFirstFreeY, floorFirstFreeY, virtualizeHeights, protectedPosition));
} }
@Override @Override
@@ -241,7 +244,7 @@ final class ModdedNativeStructureWorldgenAccess implements WorldGenLevel {
@Override @Override
public int getHeight(Heightmap.Types type, int x, int z) { 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 delegate.getHeight(type, x, z);
} }
return height(type, x, z); return height(type, x, z);
@@ -526,6 +529,7 @@ final class ModdedNativeStructureWorldgenAccess implements WorldGenLevel {
private record Boundary(ChunkPos generationCenter, private record Boundary(ChunkPos generationCenter,
IntBinaryOperator surfaceFirstFreeY, IntBinaryOperator surfaceFirstFreeY,
IntBinaryOperator floorFirstFreeY, IntBinaryOperator floorFirstFreeY,
boolean virtualizeHeights,
Predicate<BlockPos> protectedPosition) { Predicate<BlockPos> protectedPosition) {
} }
@@ -108,12 +108,17 @@ public final class ModdedScheduler implements PlatformScheduler {
scheduler.drain(); scheduler.drain();
} }
public static boolean isMainThread() {
Thread running = mainThread;
return running != null && Thread.currentThread() == running;
}
@Override @Override
public void global(Runnable task) { public void global(Runnable task) {
if (task == null) { if (task == null) {
return; return;
} }
if (onMainThread()) { if (isMainThread()) {
runGuarded(task); runGuarded(task);
return; 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) { private void runGuarded(Runnable task) {
try { try {
task.run(); task.run();
@@ -18,10 +18,14 @@
package art.arcane.iris.modded; 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.Engine;
import art.arcane.iris.engine.framework.GenerationSessionLease; import art.arcane.iris.engine.framework.GenerationSessionLease;
import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom; 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.iris.util.project.context.IrisContext;
import net.minecraft.core.Holder; import net.minecraft.core.Holder;
import net.minecraft.core.Registry; 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.Biome;
import net.minecraft.world.level.biome.MobSpawnSettings; import net.minecraft.world.level.biome.MobSpawnSettings;
import java.util.Locale;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap; 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 { final class ModdedSpawnTableMerger {
private final IrisModdedChunkGenerator generator; private final IrisModdedChunkGenerator generator;
private final ConcurrentHashMap<Biome, Holder<Biome>> vanillaSpawnBiomes = new ConcurrentHashMap<>(); private volatile SpawnTableState state = new SpawnTableState();
private final ConcurrentHashMap<SpawnTableKey, WeightedList<MobSpawnSettings.SpawnerData>> mergedSpawnTables = new ConcurrentHashMap<>();
private volatile boolean vanillaSpawnBiomesInitialized;
ModdedSpawnTableMerger(IrisModdedChunkGenerator generator) { ModdedSpawnTableMerger(IrisModdedChunkGenerator generator) {
this.generator = generator; this.generator = generator;
} }
void initializeVanillaSpawnBiomes(Registry<Biome> registry) { void initializeVanillaSpawnBiomes(Registry<Biome> registry) {
// Volatile fast path BEFORE the monitor: this runs per mob category per spawn attempt SpawnTableState active = state;
// on the server thread, and the generator monitor is contended by binds/hotloads. if (active.initialized) {
if (vanillaSpawnBiomesInitialized) {
return; return;
} }
synchronized (generator) { synchronized (active) {
if (vanillaSpawnBiomesInitialized) { if (active != state || active.initialized) {
return; return;
} }
Engine current = generator.engineOrNull(); Engine current = generator.engineOrNull();
@@ -66,29 +62,70 @@ final class ModdedSpawnTableMerger {
try (GenerationSessionLease lease = generator.requireGenerationLease(current, "modded_spawn_biomes"); try (GenerationSessionLease lease = generator.requireGenerationLease(current, "modded_spawn_biomes");
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) { IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
String namespace = current.getDimension().getLoadKey().toLowerCase(Locale.ROOT); IrisDimension hostDimension = current.getDimension();
for (IrisBiome irisBiome : current.getDimension().getReachableBiomes(current)) { addVanillaSpawnBiomes(
if (irisBiome == null || !irisBiome.isCustom()) { registry,
continue; hostDimension,
} current.getData(),
Holder<Biome> vanillaHolder = resolveBiomeHolder(registry, irisBiome.getVanillaDerivativeKey()); hostDimension.getReachableBiomes(current),
if (vanillaHolder == null) { active
continue; );
} DimensionStackContext stackContext = current.getDimensionStackContext();
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) { if (stackContext != null) {
Holder<Biome> customHolder = resolveBiomeHolder(registry, namespace + ":" + customBiome.getId()); for (DimensionTerrainContext terrainContext : stackContext.getLayersBottomToTop()) {
if (customHolder != null) { if (terrainContext.isSelfReferencing()) {
vanillaSpawnBiomes.putIfAbsent(customHolder.value(), vanillaHolder); 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) { Holder<Biome> vanillaSpawnBiome(Biome biome) {
return vanillaSpawnBiomes.get(biome); return state.vanillaSpawnBiomes.get(biome);
} }
WeightedList<MobSpawnSettings.SpawnerData> mergedSpawnTable( WeightedList<MobSpawnSettings.SpawnerData> mergedSpawnTable(
@@ -96,7 +133,8 @@ final class ModdedSpawnTableMerger {
WeightedList<MobSpawnSettings.SpawnerData> vanillaSpawns, WeightedList<MobSpawnSettings.SpawnerData> vanillaSpawns,
WeightedList<MobSpawnSettings.SpawnerData> explicitSpawns) { WeightedList<MobSpawnSettings.SpawnerData> explicitSpawns) {
SpawnTableKey key = new SpawnTableKey(biome, category); 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) { private Holder<Biome> resolveBiomeHolder(Registry<Biome> registry, String key) {
@@ -112,13 +150,16 @@ final class ModdedSpawnTableMerger {
} }
void resetVanillaSpawnBiomes() { void resetVanillaSpawnBiomes() {
synchronized (generator) { state = new SpawnTableState();
vanillaSpawnBiomes.clear();
mergedSpawnTables.clear();
vanillaSpawnBiomesInitialized = false;
}
} }
private record SpawnTableKey(Biome biome, MobCategory category) { 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(); BlockPos pos = player.blockPosition();
try { try {
region = engine.getRegion(pos.getX(), pos.getZ()); region = engine.getRegion(
pos.getX(), pos.getY() - engine.getMinHeight(), pos.getZ());
} catch (Throwable e) { } catch (Throwable e) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_REGION_LOOKUP_FAILED, MessageArgument.untrusted("value", e.getClass().getSimpleName()))); IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_REGION_LOOKUP_FAILED, MessageArgument.untrusted("value", e.getClass().getSimpleName())));
return 0; return 0;
@@ -73,6 +73,8 @@ import org.zeroturnaround.zip.ZipUtil;
import java.awt.Desktop; import java.awt.Desktop;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
@@ -85,6 +87,7 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Stream;
import art.arcane.iris.core.localization.IrisLanguage; import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.ModdedCommandMessages; import art.arcane.iris.core.localization.ModdedCommandMessages;
@@ -891,21 +894,30 @@ public final class ModdedStudioCommands {
LinkedHashSet<String> objectKeys = new LinkedHashSet<>(); LinkedHashSet<String> objectKeys = new LinkedHashSet<>();
LinkedHashSet<String> markerKeys = new LinkedHashSet<>(); LinkedHashSet<String> markerKeys = new LinkedHashSet<>();
LinkedHashSet<String> structureKeys = new LinkedHashSet<>(); LinkedHashSet<String> structureKeys = new LinkedHashSet<>();
List<IrisDimension> dimensions = new ArrayList<>();
dimension.getAllRegions(() -> dm).forEach((IrisRegion region) -> { for (String dimensionKey : PackExportClosure.collectDimensionKeys(dimension).stream().sorted().toList()) {
if (region != null && region.getLoadKey() != null) { IrisDimension exportedDimension = dm.getDimensionLoader().load(dimensionKey);
regionKeys.add(region.getLoadKey()); if (exportedDimension != null) {
dimensions.add(exportedDimension);
} }
}); }
dimension.getReachableBiomes(() -> dm).forEach((IrisBiome biome) -> { for (IrisDimension exportedDimension : dimensions) {
if (biome != null && biome.getLoadKey() != null) { exportedDimension.getAllRegions(() -> dm).forEach((IrisRegion region) -> {
biomeKeys.add(biome.getLoadKey()); if (region != null && region.getLoadKey() != null) {
} regionKeys.add(region.getLoadKey());
}); }
lootKeys.addAll(dimension.getLoot().getTables()); });
spawnerKeys.addAll(dimension.getEntitySpawners()); exportedDimension.getReachableBiomes(() -> dm).forEach((IrisBiome biome) -> {
collectStructureKeys(structureKeys, dimension.getStructures()); if (biome != null && biome.getLoadKey() != null) {
objectKeys.addAll(PackExportClosure.collectStaticObjectKeys(dimension.getStaticObjects())); 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) { for (String regionKey : regionKeys) {
IrisRegion region = dm.getRegionLoader().load(regionKey); 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) { for (String key : generatorKeys) {
hashes.append(copyJson(folder, "generators", key, dm.getGeneratorLoader().findFile(key))); 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) { for (String key : regionKeys) {
hashes.append(copyJson(folder, "regions", key, dm.getRegionLoader().findFile(key))); 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) { private static int regions(CommandSourceStack source, int radius) {
ServerPlayer player = source.getPlayer(); ServerPlayer player = source.getPlayer();
if (player == null) { if (player == null) {
@@ -216,7 +216,8 @@ public final class ModdedWhatCommands {
int centerX = (pos.getX() & ~15) + 8; int centerX = (pos.getX() & ~15) + 8;
int centerZ = (pos.getZ() & ~15) + 8; int centerZ = (pos.getZ() & ~15) + 8;
try { try {
IrisRegion region = engine.getRegion(centerX, centerZ); IrisRegion region = engine.getRegion(
centerX, pos.getY() - engine.getMinHeight(), centerZ);
IrisModdedCommands.ok(source, IrisLanguage.plain( IrisModdedCommands.ok(source, IrisLanguage.plain(
RuntimeUiMessages.WHAT_IRIS_REGION, RuntimeUiMessages.WHAT_IRIS_REGION,
MessageArgument.untrusted("region", region.getLoadKey()), 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.IrisModdedChunkGenerator;
import art.arcane.iris.modded.ModdedDimensionManager; import art.arcane.iris.modded.ModdedDimensionManager;
import art.arcane.iris.modded.ModdedForcedDatapack; import art.arcane.iris.modded.ModdedForcedDatapack;
import art.arcane.iris.modded.ModdedScheduler;
import art.arcane.iris.modded.ModdedWorkspaceGenerator; import art.arcane.iris.modded.ModdedWorkspaceGenerator;
import art.arcane.iris.nativegen.NativeStructureVolumeIndex; import art.arcane.iris.nativegen.NativeStructureVolumeIndex;
import art.arcane.iris.modded.command.ModdedPregenJob; 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); return NativeStructureVolumeIndex.volumes(engine, minX, minZ, maxX, maxZ);
} }
@Override
public boolean isMainThread() {
return ModdedScheduler.isMainThread();
}
@Override @Override
public void refreshDatapackWorkspace(Engine engine) { public void refreshDatapackWorkspace(Engine engine) {
IrisData data = engine.getData(); IrisData data = engine.getData();
@@ -121,6 +127,20 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
engine.getDimension(), engine.getDimension(),
replacement, replacement,
"irisworldgen"); "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 @Override
@@ -285,7 +305,6 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
long start = System.currentTimeMillis(); long start = System.currentTimeMillis();
try { try {
engine.hotloadSilently(); engine.hotloadSilently();
generator.onHotload();
ModdedIrisLog.info("Iris studio hotload {} pack={} {}ms", dimensionId, engine.getDimension().getLoadKey(), System.currentTimeMillis() - start); ModdedIrisLog.info("Iris studio hotload {} pack={} {}ms", dimensionId, engine.getDimension().getLoadKey(), System.currentTimeMillis() - start);
} catch (Throwable e) { } catch (Throwable e) {
ModdedIrisLog.error("Iris studio hotload failed for {}", dimensionId, e); ModdedIrisLog.error("Iris studio hotload failed for {}", dimensionId, e);
@@ -59,6 +59,7 @@ public class IrisModdedChunkGeneratorSpawnTest {
String spawn = source.substring(spawnStart, spawnEnd); String spawn = source.substring(spawnStart, spawnEnd);
assertTrue(spawn.contains("spawnTables.initializeVanillaSpawnBiomes(registry)")); assertTrue(spawn.contains("spawnTables.initializeVanillaSpawnBiomes(registry)"));
assertTrue(spawn.contains("structureBiomeSource.getVisibleSurfaceBiome("));
assertTrue(spawn.contains("spawnTables.vanillaSpawnBiome(visibleBiome.value())")); assertTrue(spawn.contains("spawnTables.vanillaSpawnBiome(visibleBiome.value())"));
assertTrue(spawn.contains("NaturalSpawner.spawnMobsForChunkGeneration(")); assertTrue(spawn.contains("NaturalSpawner.spawnMobsForChunkGeneration("));
assertTrue(spawn.contains("new LegacyRandomSource(RandomSupport.generateUniqueSeed())")); 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.IrisDimension;
import art.arcane.iris.engine.object.IrisRange; import art.arcane.iris.engine.object.IrisRange;
import art.arcane.iris.engine.object.InferredType; 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.collection.KList;
import art.arcane.volmlib.util.mantle.flag.MantleFlag; import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import net.minecraft.SharedConstants; 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.assertSame;
import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisModdedStructureParityTest { public class IrisModdedStructureParityTest {
@BeforeClass @BeforeClass
@@ -110,6 +113,144 @@ public class IrisModdedStructureParityTest {
assertEquals(expected, IrisModdedBiomeSource.biomeResolutionSeed(worldSeed, blockX, blockY, blockZ)); 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 @Test
public void configuredBiomeKeysContainOnlyPackDerivativesAndCustomBiomes() { public void configuredBiomeKeysContainOnlyPackDerivativesAndCustomBiomes() {
IrisBiome ocean = new IrisBiome() IrisBiome ocean = new IrisBiome()
@@ -132,6 +273,21 @@ public class IrisModdedStructureParityTest {
"minecraft:the_void", "overworld:aurora"), keys); "minecraft:the_void", "overworld:aurora"), keys);
assertFalse(keys.contains("minecraft:desert")); assertFalse(keys.contains("minecraft:desert"));
assertFalse(keys.contains("minecraft:plains")); 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 @Test
@@ -159,6 +315,19 @@ public class IrisModdedStructureParityTest {
assertSame(configured, IrisModdedBiomeSource.requireConfiguredStructureBiomeKeys(configured)); 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 @Test
public void configuredDimensionMetadataIsExactBeforeEngineBinding() { public void configuredDimensionMetadataIsExactBeforeEngineBinding() {
IrisDimension dimension = new IrisDimension() IrisDimension dimension = new IrisDimension()
@@ -240,4 +409,20 @@ public class IrisModdedStructureParityTest {
assertSame(MantleFlag.INITIAL_SPAWNED_MARKER, ModdedWorldManager.INITIAL_SPAWN_COMPLETION_FLAG); 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()); 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 @Test
public void protectedCellsRemainReadableButRejectWritesTilesAndTicks() { public void protectedCellsRemainReadableButRejectWritesTilesAndTicks() {
RecordingDelegate recording = new RecordingDelegate(); RecordingDelegate recording = new RecordingDelegate();
@@ -142,7 +160,9 @@ public class ModdedNativeStructureWorldgenAccessTest {
ModdedNativeStructureWorldgenAccess access = ModdedNativeStructureWorldgenAccess.create( ModdedNativeStructureWorldgenAccess access = ModdedNativeStructureWorldgenAccess.create(
recording.world(), generationCenter(), recording.world(), generationCenter(),
(x, z) -> SURFACE_FIRST_FREE_Y, (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()); assertSame(Blocks.DIRT, access.getBlockState(position).getBlock());
assertFalse(access.ensureCanWrite(position)); assertFalse(access.ensureCanWrite(position));
@@ -211,7 +231,9 @@ public class ModdedNativeStructureWorldgenAccessTest {
return ModdedNativeStructureWorldgenAccess.create( return ModdedNativeStructureWorldgenAccess.create(
recording.world(), generationCenter(), recording.world(), generationCenter(),
(x, z) -> SURFACE_FIRST_FREE_Y, (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() { private static ChunkPos generationCenter() {
@@ -95,8 +95,9 @@ public class NativeStructureFailureContractTest {
assertTrue(placement.contains("heightmapStarts.add(start);")); assertTrue(placement.contains("heightmapStarts.add(start);"));
assertTrue(placement.indexOf("WorldgenTerrainHeightmaps.primeStructurePlacement(") assertTrue(placement.indexOf("WorldgenTerrainHeightmaps.primeStructurePlacement(")
< placement.indexOf("prepareSurfaceStructures")); < placement.indexOf("prepareSurfaceStructures"));
assertTrue(source.contains("generationEngine.getHeight(x, z, false) + runtimeMinY + 1")); assertTrue(source.contains("Engine.hostHeight(generationEngine, x, z, false) + runtimeMinY + 1"));
assertTrue(source.contains("generationEngine.getHeight(x, z, true) + 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;")); assertTrue(source.contains("int minY = chunk.getMinY() + 1;"));
} }
@@ -108,6 +109,8 @@ public class NativeStructureFailureContractTest {
String placement = source.substring(placementStart, placementEnd); String placement = source.substring(placementStart, placementEnd);
assertTrue(placement.contains("ModdedNativeStructureWorldgenAccess.create(")); 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")); assertTrue(placement.contains("placeVanillaStructure(boundedWorld, structureManager"));
assertFalse(placement.contains("NativeStructurePostProcessor.place(world,")); assertFalse(placement.contains("NativeStructurePostProcessor.place(world,"));
assertTrue(placement.contains("world.setCurrentlyGenerating(null);")); assertTrue(placement.contains("world.setCurrentlyGenerating(null);"));
@@ -1,10 +1,12 @@
package art.arcane.iris.modded.service; package art.arcane.iris.modded.service;
import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.modded.ModdedScheduler;
import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KList;
import org.junit.Test; import org.junit.Test;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
@@ -26,4 +28,21 @@ public class ModdedStudioHotloadServiceTest {
assertFalse(ModdedStudioHotloadService.hasDatapackImports(null)); assertFalse(ModdedStudioHotloadService.hasDatapackImports(null));
assertFalse(ModdedStudioHotloadService.hasDatapackImports(List.of(empty))); 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<>(), new LinkedHashSet<>(),
requiredDimension, requiredDimension,
requiredFixer); requiredFixer);
collectDimensionStackRegistryRequirements(
requirements,
requiredDimension,
requiredFixer);
return Map.copyOf(requirements); 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( private static void collectRegistryRequirements(
Map<String, String> requirements, Map<String, String> requirements,
Set<String> installedBiomeKeys, Set<String> installedBiomeKeys,
@@ -235,7 +271,6 @@ public final class IrisDatapackCompiler {
"dimension_type/iris:" + dimension.getDimensionTypeKey(), "dimension_type/iris:" + dimension.getDimensionTypeKey(),
fingerprintContent(dimension.getDimensionType().toJson(fixer))); fingerprintContent(dimension.getDimensionType().toJson(fixer)));
String namespace = dimension.getLoadKey().toLowerCase(Locale.ROOT);
ContentGate contentGate = dimension.getLoader() == null ? null : dimension.getLoader().getContentGate(); ContentGate contentGate = dimension.getLoader() == null ? null : dimension.getLoader().getContentGate();
for (IrisBiome biome : dimension.getAllBiomes(dimension::getLoader)) { for (IrisBiome biome : dimension.getAllBiomes(dimension::getLoader)) {
if (biome == null || !biome.isCustom()) { if (biome == null || !biome.isCustom()) {
@@ -246,7 +281,7 @@ public final class IrisDatapackCompiler {
if (customBiome == null) { if (customBiome == null) {
continue; continue;
} }
String biomeKey = namespace + ":" + customBiome.getId(); String biomeKey = dimension.getCustomBiomeKey(customBiome.getId());
if (!installedBiomeKeys.add(biomeKey)) { if (!installedBiomeKeys.add(biomeKey)) {
continue; continue;
} }
@@ -533,7 +568,7 @@ public final class IrisDatapackCompiler {
|| !Files.isDirectory(dimensions, LinkOption.NOFOLLOW_LINKS)) { || !Files.isDirectory(dimensions, LinkOption.NOFOLLOW_LINKS)) {
return false; return false;
} }
try (Stream<Path> stream = Files.list(dimensions)) { try (Stream<Path> stream = Files.walk(dimensions)) {
return stream.anyMatch(path -> !Files.isSymbolicLink(path) return stream.anyMatch(path -> !Files.isSymbolicLink(path)
&& Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS) && Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)
&& path.getFileName().toString().endsWith(".json")); && path.getFileName().toString().endsWith(".json"));
@@ -1121,7 +1121,7 @@ public class ServerConfigurator {
for (IrisBiome i : dimension.getAllBiomes(dimension::getLoader)) { for (IrisBiome i : dimension.getAllBiomes(dimension::getLoader)) {
if (i.isCustom()) { if (i.isCustom()) {
for (IrisBiomeCustom j : i.getCustomDerivitives()) { 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; if (access == null) return false;
Engine engine = access.getEngine(); Engine engine = access.getEngine();
if (engine == null) return false; 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()); return regions.contains(region.getLoadKey());
} }
} }
@@ -156,6 +156,9 @@ public interface INMSBinding {
return NativeStructureVolume.NONE; return NativeStructureVolume.NONE;
} }
default void invalidateNativeStructureVolumeIndex(Engine engine) {
}
int getBiomeId(Biome biome); int getBiomeId(Biome biome);
MCABiomeContainer newBiomeContainer(int min, int max, int[] data); 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.io.File;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Set;
final class PackDimensionValidator { final class PackDimensionValidator {
private PackDimensionValidator() { private PackDimensionValidator() {
@@ -35,9 +37,14 @@ final class PackDimensionValidator {
static void validateDimensions(File packFolder, File[] dimensionFiles, List<String> blockingErrors, List<String> warnings) { static void validateDimensions(File packFolder, File[] dimensionFiles, List<String> blockingErrors, List<String> warnings) {
File regionsFolder = new File(packFolder, "regions"); File regionsFolder = new File(packFolder, "regions");
File biomesFolder = new File(packFolder, "biomes"); 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) { for (File dimFile : dimensionFiles) {
String dimensionKey = PackValidationIo.stripExtension(dimFile.getName()); String dimensionKey = PackValidationIo.deriveKey(dimensionsFolder, dimFile);
JSONObject dimJson; JSONObject dimJson;
try { try {
dimJson = new JSONObject(Files.readString(dimFile.toPath(), StandardCharsets.UTF_8)); dimJson = new JSONObject(Files.readString(dimFile.toPath(), StandardCharsets.UTF_8));
@@ -50,6 +57,7 @@ final class PackDimensionValidator {
validateDimensionHeights(packFolder, dimensionKey, dimJson, blockingErrors); validateDimensionHeights(packFolder, dimensionKey, dimJson, blockingErrors);
validateWorldBoundary(dimensionKey, dimJson, blockingErrors); validateWorldBoundary(dimensionKey, dimJson, blockingErrors);
validateStaticObjects(packFolder, dimensionKey, dimJson, blockingErrors); validateStaticObjects(packFolder, dimensionKey, dimJson, blockingErrors);
validateDimensionStack(packFolder, dimensionKey, dimJson, dimensionKeys, blockingErrors);
JSONArray regionsArray = dimJson.optJSONArray("regions"); JSONArray regionsArray = dimJson.optJSONArray("regions");
if (regionsArray == null || regionsArray.length() == 0) { 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, static void validateStaticObjects(File packFolder, String dimensionKey, JSONObject dimension,
List<String> blockingErrors) { List<String> blockingErrors) {
if (!dimension.has("staticObjects")) { if (!dimension.has("staticObjects")) {
@@ -18,11 +18,16 @@
package art.arcane.iris.core.pack; 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.IrisObjectMarker;
import art.arcane.iris.engine.object.IrisObjectPlacement; import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisStaticObject; import art.arcane.iris.engine.object.IrisStaticObject;
import art.arcane.volmlib.util.collection.KSet; 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 * 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 * modded verbatim-copy compiler must export the complete ambient-spawning graph: object placements
@@ -33,6 +38,48 @@ public final class PackExportClosure {
private 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) { public static KSet<String> collectMarkerKeys(Iterable<IrisObjectPlacement> placements) {
KSet<String> markerKeys = new KSet<>(); KSet<String> markerKeys = new KSet<>();
if (placements == null) { if (placements == null) {
@@ -80,12 +80,13 @@ final class PackHydrologyValidator {
List<File> sortedDimensions = new ArrayList<>(List.of(dimensionFiles)); List<File> sortedDimensions = new ArrayList<>(List.of(dimensionFiles));
sortedDimensions.sort(Comparator.comparing(File::getPath)); sortedDimensions.sort(Comparator.comparing(File::getPath));
File dimensionsFolder = new File(packFolder, PackValidator.DIMENSIONS_FOLDER);
for (File dimensionFile : sortedDimensions) { for (File dimensionFile : sortedDimensions) {
JSONObject dimension = PackValidationIo.readJson(dimensionFile); JSONObject dimension = PackValidationIo.readJson(dimensionFile);
if (dimension == null) { if (dimension == null) {
continue; continue;
} }
String dimensionKey = PackValidationIo.stripExtension(dimensionFile.getName()); String dimensionKey = PackValidationIo.deriveKey(dimensionsFolder, dimensionFile);
String dimensionPath = "Dimension '" + dimensionKey + "'"; String dimensionPath = "Dimension '" + dimensionKey + "'";
JSONObject dimensionPolicy = validatePolicy( JSONObject dimensionPolicy = validatePolicy(
dimensionPath, 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.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeGeneratorLink; import art.arcane.iris.engine.object.IrisBiomeGeneratorLink;
import art.arcane.iris.engine.object.IrisDimension; 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.IrisExpression;
import art.arcane.iris.engine.object.IrisGenerator; import art.arcane.iris.engine.object.IrisGenerator;
import art.arcane.iris.engine.object.IrisImage; import art.arcane.iris.engine.object.IrisImage;
@@ -217,11 +218,12 @@ final class PackImageMapValidator {
Set<String> warnings Set<String> warnings
) { ) {
File[] sortedFiles = Arrays.copyOf(dimensionFiles, dimensionFiles.length); 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<>(); Map<String, IrisDimension> dimensions = new LinkedHashMap<>();
Set<String> bindingReadyDimensions = new LinkedHashSet<>(); Set<String> bindingReadyDimensions = new LinkedHashSet<>();
File dimensionsFolder = new File(packFolder, PackValidator.DIMENSIONS_FOLDER);
for (File dimensionFile : sortedFiles) { for (File dimensionFile : sortedFiles) {
String dimensionKey = PackValidationIo.stripExtension(dimensionFile.getName()); String dimensionKey = PackValidationIo.deriveKey(dimensionsFolder, dimensionFile);
JSONObject raw = PackValidationIo.readJson(dimensionFile); JSONObject raw = PackValidationIo.readJson(dimensionFile);
if (raw == null) { if (raw == null) {
continue; continue;
@@ -247,19 +249,35 @@ final class PackImageMapValidator {
validateGeneratorStyleReferences(dimensionKey, dimension, data, resources, blockingErrors); validateGeneratorStyleReferences(dimensionKey, dimension, data, resources, blockingErrors);
} }
Map<String, List<Map.Entry<String, IrisDimension>>> upperParents = new TreeMap<>(); 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()) { for (Map.Entry<String, IrisDimension> entry : dimensions.entrySet()) {
String upperKey = normalized(entry.getValue().getUpperDimension()); 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; 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()) { for (Map.Entry<String, IrisDimension> entry : dimensions.entrySet()) {
if (!bindingReadyDimensions.contains(entry.getKey())) { if (!bindingReadyDimensions.contains(entry.getKey())) {
continue; continue;
} }
List<Map.Entry<String, IrisDimension>> parents = upperParents.get(entry.getKey()); 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(), validateBindings(packFolder, "Dimension '" + entry.getKey() + "'", entry.getValue(),
entry.getValue().getWorldBoundary(), data, resources, registries, blockingErrors, warnings); entry.getValue().getWorldBoundary(), data, resources, registries, blockingErrors, warnings);
continue; continue;
@@ -268,12 +286,23 @@ final class PackImageMapValidator {
validateBindings(packFolder, "Dimension '" + entry.getKey() + "'", entry.getValue(), validateBindings(packFolder, "Dimension '" + entry.getKey() + "'", entry.getValue(),
entry.getValue().getWorldBoundary(), data, resources, registries, blockingErrors, warnings); entry.getValue().getWorldBoundary(), data, resources, registries, blockingErrors, warnings);
} }
parents.sort(Map.Entry.comparingByKey()); if (hasUpperParents) {
for (Map.Entry<String, IrisDimension> parent : parents) { parents.sort(Map.Entry.comparingByKey());
validateBindings(packFolder, for (Map.Entry<String, IrisDimension> parent : parents) {
"Dimension '" + parent.getKey() + "' upper dimension '" + entry.getKey() + "'", validateBindings(packFolder,
entry.getValue(), parent.getValue().getWorldBoundary(), data, resources, registries, "Dimension '" + parent.getKey() + "' upper dimension '" + entry.getKey() + "'",
blockingErrors, warnings); 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); return new PackValidationResult(packName, blockingErrors, warnings, validatedAt);
} }
File[] dimensionFiles = dimensionsFolder.listFiles(f -> f.isFile() && f.getName().endsWith(".json")); List<File> discoveredDimensions = PackValidationIo.listJsonRecursive(dimensionsFolder);
if (dimensionFiles == null || dimensionFiles.length == 0) { if (discoveredDimensions.isEmpty()) {
blockingErrors.add("No dimension JSON files under dimensions/."); blockingErrors.add("No dimension JSON files under dimensions/.");
return new PackValidationResult(packName, blockingErrors, warnings, validatedAt); 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); PackDimensionValidator.validateDimensions(packFolder, dimensionFiles, blockingErrors, warnings);
PackImageMapValidator.Validation imageMaps = PackImageMapValidator.validate( PackImageMapValidator.Validation imageMaps = PackImageMapValidator.validate(
@@ -195,7 +197,7 @@ public final class PackValidator {
if (!data.getContentGate().ready()) { if (!data.getContentGate().ready()) {
return List.of(); return List.of();
} }
boolean dimensionExcluded = loadEverything(data, dimensionFiles); boolean dimensionExcluded = loadEverything(packFolder, data, dimensionFiles);
List<CompatFinding> findings = data.getCompatReport().findings(); List<CompatFinding> findings = data.getCompatReport().findings();
applyCompatFindings(minecraftVersion, dimensionExcluded, findings, blockingErrors, warnings); applyCompatFindings(minecraftVersion, dimensionExcluded, findings, blockingErrors, warnings);
return findings; return findings;
@@ -219,11 +221,12 @@ public final class PackValidator {
* *
* @return true when any dimension in the pack cannot generate on this server * @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; boolean dimensionExcluded = false;
File dimensionsFolder = new File(packFolder, DIMENSIONS_FOLDER);
for (File dimensionFile : dimensionFiles) { for (File dimensionFile : dimensionFiles) {
IrisDimension dimension = data.getDimensionLoader().load( IrisDimension dimension = data.getDimensionLoader().load(
PackValidationIo.stripExtension(dimensionFile.getName()), false); PackValidationIo.deriveKey(dimensionsFolder, dimensionFile), false);
if (dimension == null) { if (dimension == null) {
continue; 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.IrisBlockData;
import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisEntity; 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.IrisGenerator;
import art.arcane.iris.engine.object.IrisLootTable; import art.arcane.iris.engine.object.IrisLootTable;
import art.arcane.iris.core.pack.PackExportClosure; 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()); throw new IllegalStateException("Failed to create structure package staging folder " + folder.getAbsolutePath());
} }
IrisLogging.info("Packaging Dimension " + dimension.getName() + " " + (obfuscate ? "(Obfuscated)" : "")); 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<IrisRegion> regions = new KSet<>();
KSet<IrisBiome> biomes = new KSet<>(); KSet<IrisBiome> biomes = new KSet<>();
KSet<IrisEntity> entities = new KSet<>(); KSet<IrisEntity> entities = new KSet<>();
KSet<IrisSpawner> spawners = new KSet<>(); KSet<IrisSpawner> spawners = new KSet<>();
KSet<IrisGenerator> generators = new KSet<>(); KSet<IrisGenerator> generators = new KSet<>();
KSet<IrisExpression> expressions = new KSet<>();
KSet<IrisLootTable> loot = new KSet<>(); KSet<IrisLootTable> loot = new KSet<>();
KSet<IrisBlockData> blocks = new KSet<>(); KSet<IrisBlockData> blocks = new KSet<>();
@@ -119,22 +128,30 @@ public class IrisPackageCompiler {
for (String i : dm.getBlockLoader().getPossibleKeys()) { for (String i : dm.getBlockLoader().getPossibleKeys()) {
addIfPresent(blocks, dm.getBlockLoader().load(i)); 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)); for (IrisDimension exportedDimension : dimensions) {
dimension.getLoot().getTables().forEach((i) -> addIfPresent(loot, dm.getLootLoader().load(i))); 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))); 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.getLoot().getTables().forEach((i) -> addIfPresent(loot, dm.getLootLoader().load(i))));
regions.forEach((r) -> r.getEntitySpawners().forEach((sp) -> addIfPresent(spawners, dm.getSpawnerLoader().load(sp)))); 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((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.getLoot().getTables().forEach((i) -> addIfPresent(loot, dm.getLootLoader().load(i))));
biomes.forEach((r) -> r.getEntitySpawners().forEach((sp) -> addIfPresent(spawners, dm.getSpawnerLoader().load(sp)))); biomes.forEach((r) -> r.getEntitySpawners().forEach((sp) -> addIfPresent(spawners, dm.getSpawnerLoader().load(sp))));
KList<IrisObjectPlacement> allPlacements = new KList<>(); KList<IrisObjectPlacement> allPlacements = new KList<>();
regions.forEach((r) -> allPlacements.addAll(r.getObjects())); regions.forEach((r) -> allPlacements.addAll(r.getObjects()));
biomes.forEach((i) -> allPlacements.addAll(i.getObjects())); biomes.forEach((i) -> allPlacements.addAll(i.getObjects()));
for (IrisStaticObject placement : dimension.getStaticObjects()) { for (IrisDimension exportedDimension : dimensions) {
allPlacements.add(placement.toPlacement()); for (IrisStaticObject placement : exportedDimension.getStaticObjects()) {
allPlacements.add(placement.toPlacement());
}
} }
KSet<IrisMarker> markers = new KSet<>(); KSet<IrisMarker> markers = new KSet<>();
for (String markerKey : PackExportClosure.collectMarkerKeys(allPlacements)) { for (String markerKey : PackExportClosure.collectMarkerKeys(allPlacements)) {
@@ -148,7 +165,9 @@ public class IrisPackageCompiler {
collectSpawnerEntityKeys(spawners).forEach((i) -> addIfPresent(entities, dm.getEntityLoader().load(i))); collectSpawnerEntityKeys(spawners).forEach((i) -> addIfPresent(entities, dm.getEntityLoader().load(i)));
entities.forEach((e) -> e.getLoot().getTables().forEach((i) -> addIfPresent(loot, dm.getLootLoader().load(i)))); entities.forEach((e) -> e.getLoot().getTables().forEach((i) -> addIfPresent(loot, dm.getLootLoader().load(i))));
Set<String> structureKeys = new LinkedHashSet<>(); Set<String> structureKeys = new LinkedHashSet<>();
collectStructureKeys(structureKeys, dimension.getStructures()); for (IrisDimension exportedDimension : dimensions) {
collectStructureKeys(structureKeys, exportedDimension.getStructures());
}
regions.forEach((region) -> collectStructureKeys(structureKeys, region.getStructures())); regions.forEach((region) -> collectStructureKeys(structureKeys, region.getStructures()));
biomes.forEach((biome) -> collectStructureKeys(structureKeys, biome.getStructures())); biomes.forEach((biome) -> collectStructureKeys(structureKeys, biome.getStructures()));
KMap<String, String> renameObjects = new KMap<>(); KMap<String, String> renameObjects = new KMap<>();
@@ -176,8 +195,10 @@ public class IrisPackageCompiler {
j.setPlace(newNames); j.setPlace(newNames);
} }
for (IrisStaticObject placement : dimension.getStaticObjects()) { for (IrisDimension exportedDimension : dimensions) {
placement.setObject(renameObjects.get(placement.getObject())); for (IrisStaticObject placement : exportedDimension.getStaticObjects()) {
placement.setObject(renameObjects.get(placement.getObject()));
}
} }
KMap<String, KList<String>> lookupObjects = renameObjects.flip(); KMap<String, KList<String>> lookupObjects = renameObjects.flip();
@@ -230,9 +251,12 @@ public class IrisPackageCompiler {
} }
b.append(structureClosure.writeTo(folder, minify)); b.append(structureClosure.writeTo(folder, minify));
b.append(ImageMapPackageClosure.writeAll(dm, folder, minify)); b.append(ImageMapPackageClosure.writeAll(dm, folder, minify));
a = new JSONObject(new Gson().toJson(dimension)).toString(minify ? 0 : 4); for (IrisDimension exportedDimension : dimensions.stream()
IO.writeAll(new File(folder, "dimensions/" + dimension.getLoadKey() + ".json"), a); .sorted(Comparator.comparing(IrisDimension::getLoadKey)).toList()) {
b.append(IO.hash(a)); 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) { for (IrisGenerator i : generators) {
a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4); a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4);
@@ -240,6 +264,13 @@ public class IrisPackageCompiler {
b.append(IO.hash(a)); 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())); c.append(IO.hash(b.toString()));
b = new StringBuilder(); b = new StringBuilder();
@@ -21,6 +21,7 @@ package art.arcane.iris.core.project;
import art.arcane.iris.core.pack.PackDirectoryResolver; import art.arcane.iris.core.pack.PackDirectoryResolver;
import art.arcane.volmlib.util.format.Form; import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.io.IO; import art.arcane.volmlib.util.io.IO;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject; import art.arcane.volmlib.util.json.JSONObject;
import java.io.File; import java.io.File;
@@ -179,19 +180,58 @@ public final class IrisProjectCopier {
} }
private static void transformDimension(Path stage, String sourceKey, String targetKey) throws IOException { private static void transformDimension(Path stage, String sourceKey, String targetKey) throws IOException {
Path oldDimension = stage.resolve("dimensions").resolve(sourceKey + ".json"); Path dimensionsRoot = stage.resolve("dimensions");
Path newDimension = stage.resolve("dimensions").resolve(targetKey + ".json"); Path oldDimension = dimensionsRoot.resolve(sourceKey + ".json");
Path newDimension = dimensionsRoot.resolve(targetKey + ".json");
if (!oldDimension.equals(newDimension)) { if (!oldDimension.equals(newDimension)) {
Files.move(oldDimension, newDimension); Files.move(oldDimension, newDimension);
} }
JSONObject json = new JSONObject(IO.readAll(newDimension.toFile())); try (Stream<Path> files = Files.walk(dimensionsRoot)) {
if (json.has("name")) { for (Path dimensionFile : files.filter(path -> Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS))
json.put("name", Form.capitalizeWords(targetKey.replace('-', ' '))); .filter(path -> path.getFileName().toString().endsWith(".json"))
IO.writeAll(newDimension.toFile(), json.toString(4)); .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 { private static void publish(Path stage, Path target) throws IOException {
try { try {
Files.move(stage, target, StandardCopyOption.ATOMIC_MOVE); Files.move(stage, target, StandardCopyOption.ATOMIC_MOVE);
@@ -515,7 +515,7 @@ public class BoardSVC implements IrisService, BoardProvider {
} }
lines.add("&7&m "); 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_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_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)))); 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 // Add more or find any in the region
if (isUseAll || placements.isEmpty()) { 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())); 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; IrisComplex nextComplex = null;
try { try {
engine.sealForTransition("complex hotload", false); engine.sealForTransition("complex hotload", false);
engine.prepareRuntimeHotload();
RuntimeAssembly assembly = new RuntimeAssembly(RuntimeAssembly.nextRuntimeId(), previous.target()); RuntimeAssembly assembly = new RuntimeAssembly(RuntimeAssembly.nextRuntimeId(), previous.target());
engine.runtimeAssembly.set(assembly); engine.runtimeAssembly.set(assembly);
EngineRuntime next; EngineRuntime next;
try (IrisContext.Scope ignored = IrisContext.open(engine, engine.getGenerationSessions().currentSessionId(), null)) { try (IrisContext.Scope ignored = IrisContext.open(engine, engine.getGenerationSessions().currentSessionId(), null)) {
assembly.complex = new IrisComplex(engine); assembly.complex = new IrisComplex(engine);
nextComplex = assembly.complex; nextComplex = assembly.complex;
assembly.dimensionStackContext = engine.runtimeBuilder.buildDimensionStackContext();
assembly.upperContext = engine.runtimeBuilder.buildUpperContext(); assembly.upperContext = engine.runtimeBuilder.buildUpperContext();
BiomeMaxes biomeMaxes = engine.runtimeBuilder.computeBiomeMaxes(); 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 { } finally {
engine.runtimeAssembly.remove(); engine.runtimeAssembly.remove();
} }
@@ -110,6 +117,7 @@ final class EngineHotloader {
boolean published = false; boolean published = false;
try { try {
engine.sealForTransition("hotload", false); engine.sealForTransition("hotload", false);
engine.prepareRuntimeHotload();
replacementData = IrisData.openRuntime(previousData.getDataFolder()); replacementData = IrisData.openRuntime(previousData.getDataFolder());
IrisDimension replacement = replacementData.getDimensionLoader().load(previousDimension.getLoadKey()); IrisDimension replacement = replacementData.getDimensionLoader().load(previousDimension.getLoadKey());
if (replacement == null) { if (replacement == null) {
@@ -34,6 +34,7 @@ record EngineRuntime(
EngineTarget target, EngineTarget target,
IrisComplex complex, IrisComplex complex,
UpperDimensionContext upperContext, UpperDimensionContext upperContext,
DimensionStackContext dimensionStackContext,
EngineEffects effects, EngineEffects effects,
EngineMode mode, EngineMode mode,
EngineWorldManager worldManager, EngineWorldManager worldManager,
@@ -44,6 +45,7 @@ record EngineRuntime(
int nextCacheId, int nextCacheId,
IrisComplex nextComplex, IrisComplex nextComplex,
UpperDimensionContext nextUpperContext, UpperDimensionContext nextUpperContext,
DimensionStackContext nextDimensionStackContext,
BiomeMaxes nextBiomeMaxes BiomeMaxes nextBiomeMaxes
) { ) {
return new EngineRuntime( return new EngineRuntime(
@@ -51,6 +53,7 @@ record EngineRuntime(
target, target,
nextComplex, nextComplex,
nextUpperContext, nextUpperContext,
nextDimensionStackContext,
effects, effects,
mode, mode,
worldManager, worldManager,
@@ -22,12 +22,14 @@ import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.ResourceLoader; import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.engine.EngineRuntime.BiomeMaxes; import art.arcane.iris.engine.EngineRuntime.BiomeMaxes;
import art.arcane.iris.engine.IrisEngine.LifecycleState; 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.EngineEffects;
import art.arcane.iris.engine.framework.EngineEffectsProvider; import art.arcane.iris.engine.framework.EngineEffectsProvider;
import art.arcane.iris.engine.framework.EngineMode; import art.arcane.iris.engine.framework.EngineMode;
import art.arcane.iris.engine.framework.EngineTarget; import art.arcane.iris.engine.framework.EngineTarget;
import art.arcane.iris.engine.framework.EngineWorldManager; import art.arcane.iris.engine.framework.EngineWorldManager;
import art.arcane.iris.engine.framework.EngineWorldManagerProvider; 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.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomePaletteLayer; import art.arcane.iris.engine.object.IrisBiomePaletteLayer;
import art.arcane.iris.engine.object.IrisDecorator; import art.arcane.iris.engine.object.IrisDecorator;
@@ -74,6 +76,9 @@ final class EngineRuntimeBuilder {
assembly.complex = new IrisComplex(engine); assembly.complex = new IrisComplex(engine);
IrisLogging.debug("[IrisEngine timing] complex=" + (M.ms() - started) + "ms"); IrisLogging.debug("[IrisEngine timing] complex=" + (M.ms() - started) + "ms");
started = M.ms(); started = M.ms();
assembly.dimensionStackContext = buildDimensionStackContext();
IrisLogging.debug("[IrisEngine timing] buildDimensionStackContext=" + (M.ms() - started) + "ms");
started = M.ms();
assembly.upperContext = buildUpperContext(); assembly.upperContext = buildUpperContext();
IrisLogging.debug("[IrisEngine timing] buildUpperContext=" + (M.ms() - started) + "ms"); IrisLogging.debug("[IrisEngine timing] buildUpperContext=" + (M.ms() - started) + "ms");
started = M.ms(); started = M.ms();
@@ -91,6 +96,14 @@ final class EngineRuntimeBuilder {
IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData()); IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData());
assembly.mode.registerStage((x, z, blocks, biomes, multicore, context) -> assembly.mode.registerStage((x, z, blocks, biomes, multicore, context) ->
staticObjects.apply(engine, x, z, blocks)); 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"); IrisLogging.debug("[IrisEngine timing] setupMode=" + (M.ms() - started) + "ms");
started = M.ms(); started = M.ms();
assembly.worldManager = IrisServices.get(EngineWorldManagerProvider.class).create(engine); assembly.worldManager = IrisServices.get(EngineWorldManagerProvider.class).create(engine);
@@ -205,6 +218,21 @@ final class EngineRuntimeBuilder {
return null; 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() { private EngineMode createMode() {
Throwable configuredFailure = null; Throwable configuredFailure = null;
try { try {
@@ -300,6 +328,7 @@ final class EngineRuntimeBuilder {
} }
IrisComplex complex; IrisComplex complex;
UpperDimensionContext upperContext; UpperDimensionContext upperContext;
DimensionStackContext dimensionStackContext;
EngineEffects effects; EngineEffects effects;
EngineMode mode; EngineMode mode;
EngineWorldManager worldManager; EngineWorldManager worldManager;
@@ -319,6 +348,7 @@ final class EngineRuntimeBuilder {
target, target,
complex, complex,
upperContext, upperContext,
dimensionStackContext,
effects, effects,
mode, mode,
worldManager, worldManager,
@@ -501,10 +501,14 @@ public class IrisEngine implements Engine {
} }
public void hotloadSilently() { public void hotloadSilently() {
nativeStructureVolumeMemo.clear();
hotloader.hotloadSilently(); hotloader.hotloadSilently();
} }
void prepareRuntimeHotload() {
nativeStructureVolumeMemo.clear();
platformHooks.prepareRuntimeHotload(this);
}
@Override @Override
public KList<NativeStructureVolume> getNativeStructureVolumes(int minX, int minZ, int maxX, int maxZ) { public KList<NativeStructureVolume> getNativeStructureVolumes(int minX, int minZ, int maxX, int maxZ) {
if (!nativeStructureVolumeQueriesEnabled.get()) { if (!nativeStructureVolumeQueriesEnabled.get()) {
@@ -632,6 +636,16 @@ public class IrisEngine implements Engine {
return current == null ? null : current.upperContext(); 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 @Override
public CompletableFuture<Long> getHash32() { public CompletableFuture<Long> getHash32() {
RuntimeAssembly assembly = runtimeAssembly.get(); 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; package art.arcane.iris.engine;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.image.IrisImageMapRuntime; 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.IrisBiome;
import art.arcane.iris.engine.object.IrisDimension; 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.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.spi.PlatformBlockState;
import art.arcane.iris.util.project.stream.ProceduralStream; import art.arcane.iris.util.common.data.DataProvider;
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;
public class UpperDimensionContext implements DataProvider { public class UpperDimensionContext implements DataProvider {
private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D); private final DimensionTerrainContext terrainContext;
private final IrisDimension dimension;
private final IrisData data;
private final int chunkHeight; 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) { private UpperDimensionContext(DimensionTerrainContext terrainContext, int chunkHeight) {
this.dimension = state.dimension(); this.terrainContext = terrainContext;
this.data = state.data(); this.chunkHeight = chunkHeight;
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();
} }
public static UpperDimensionContext create(Engine engine, IrisDimension upperDim) { public static UpperDimensionContext create(Engine engine, IrisDimension upperDimension) {
boolean selfRef = upperDim.getLoadKey().equals(engine.getDimension().getLoadKey()); return new UpperDimensionContext(
int chunkHeight = engine.getHeight(); DimensionTerrainContext.forUpper(engine, upperDimension),
if (selfRef) { engine.getHeight()
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()
); );
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( static IrisRegion mappedRegion(
@@ -314,8 +49,8 @@ public class UpperDimensionContext implements DataProvider {
double worldX, double worldX,
double worldZ double worldZ
) { ) {
IrisRegion mappedRegion = imageMapRuntime.sampleRegion(worldX, worldZ); return DimensionTerrainContext.mappedRegion(
return mappedRegion == null ? proceduralRegion : mappedRegion; imageMapRuntime, proceduralRegion, worldX, worldZ);
} }
static double mappedTerrainHeight( static double mappedTerrainHeight(
@@ -324,7 +59,8 @@ public class UpperDimensionContext implements DataProvider {
double worldX, double worldX,
double worldZ double worldZ
) { ) {
return imageMapRuntime.sampleTerrainHeight(worldX, worldZ, proceduralHeight); return DimensionTerrainContext.mappedTerrainHeight(
imageMapRuntime, proceduralHeight, worldX, worldZ);
} }
static IrisBiome mappedBiome( static IrisBiome mappedBiome(
@@ -333,8 +69,8 @@ public class UpperDimensionContext implements DataProvider {
double worldX, double worldX,
double worldZ double worldZ
) { ) {
IrisBiome mappedBiome = imageMapRuntime.sampleBiome(worldX, worldZ); return DimensionTerrainContext.mappedBiome(
return mappedBiome == null ? proceduralBiome : mappedBiome; imageMapRuntime, proceduralBiome, worldX, worldZ);
} }
static PlatformBlockState mappedSurfaceBlock( static PlatformBlockState mappedSurfaceBlock(
@@ -343,108 +79,40 @@ public class UpperDimensionContext implements DataProvider {
double worldX, double worldX,
double worldZ double worldZ
) { ) {
PlatformBlockState mappedBlock = imageMapRuntime.sampleSurfaceBlock(worldX, worldZ); return DimensionTerrainContext.mappedSurfaceBlock(
return mappedBlock == null ? proceduralBlock : mappedBlock; imageMapRuntime, proceduralBlock, worldX, worldZ);
}
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);
} }
public int getUpperSurfaceY(int x, int z) { public int getUpperSurfaceY(int x, int z) {
double rawHeight = heightStream.get((double) x, (double) z); return chunkHeight - 1 - (int) Math.round(terrainContext.getNormalTerrainHeight(x, z));
return chunkHeight - 1 - (int) Math.round(rawHeight);
} }
public IrisBiome getUpperBiome(int x, int 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) { 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) { 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) { public PlatformBlockState getSurfaceBlock(int x, int z) {
return imageMapRuntime.sampleSurfaceBlock(x, z); return terrainContext.getSurfaceBlock(x, z);
} }
public IrisDimension getDimension() { public IrisDimension getDimension() {
return dimension; return terrainContext.getDimension();
} }
@Override @Override
public IrisData getData() { public IrisData getData() {
return data; return terrainContext.getData();
} }
public boolean isSelfReferencing() { public boolean isSelfReferencing() {
return selfReferencing; return terrainContext.isSelfReferencing();
}
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
) {
} }
} }
@@ -18,10 +18,13 @@
package art.arcane.iris.engine.actuator; 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.Engine;
import art.arcane.iris.engine.framework.EngineAssignedActuator; import art.arcane.iris.engine.framework.EngineAssignedActuator;
import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom; 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.ChunkContext;
import art.arcane.iris.util.project.context.ChunkedDataCache; import art.arcane.iris.util.project.context.ChunkedDataCache;
import art.arcane.volmlib.util.collection.KMap; 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.IrisPlatforms;
import art.arcane.iris.spi.PlatformBiome; import art.arcane.iris.spi.PlatformBiome;
import java.util.List;
public class IrisBiomeActuator extends EngineAssignedActuator<PlatformBiome> { public class IrisBiomeActuator extends EngineAssignedActuator<PlatformBiome> {
private final RNG rng; private final RNG rng;
private final ChronoLatch cl = new ChronoLatch(5000); private final ChronoLatch cl = new ChronoLatch(5000);
@@ -60,33 +65,167 @@ public class IrisBiomeActuator extends EngineAssignedActuator<PlatformBiome> {
Engine engine = getEngine(); Engine engine = getEngine();
Mantle<Matter> mantle = engine.getMantle().getMantle(); Mantle<Matter> mantle = engine.getMantle().getMantle();
ChunkedDataCache<IrisBiome> biomeCache = context.getBiome(); ChunkedDataCache<IrisBiome> biomeCache = context.getBiome();
DimensionStackContext dimensionStackContext = engine.getDimensionStackContext();
for (int xf = 0; xf < width; xf++) { for (int xf = 0; xf < width; xf++) {
IrisBiome ib;
for (int zf = 0; zf < depth; zf++) { for (int zf = 0; zf < depth; zf++) {
ib = biomeCache.get(xf, zf); int worldX = x + xf;
String key; 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()) { if (platformBiome != null) {
IrisBiomeCustom custom = ib.getCustomBiome(rng, engine, x + xf, 0, z + zf); h.set(xf, 0, zf, xf, height - 1, zf, platformBiome);
key = getDimension().getLoadKey() + ":" + custom.getId();
} else {
key = ib.getSkyBiomeKey(rng, engine, x + xf, 0, z + zf);
} }
ResolvedBiome resolved = resolve(key); mantle.set(worldX, 0, worldZ, resolved.matter());
PlatformBiome biome = resolved.biome(); if (dimensionStackContext != null) {
PlatformBiome bottomBiome = platformBiome == null
if (biome != null) { ? h.getRaw(xf, 0, zf)
h.set(xf, 0, zf, xf, height - 1, zf, biome); : 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()); 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 * 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 * 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.core.nms.container.Pair;
import art.arcane.iris.engine.IrisComplex; import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.UpperDimensionContext; 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.data.chunk.TerrainChunk;
import art.arcane.iris.engine.mantle.EngineMantle; import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisBiome;
@@ -90,6 +93,10 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
return null; return null;
} }
default @Nullable DimensionStackContext getDimensionStackContext() {
return null;
}
EngineMode getMode(); EngineMode getMode();
EnginePlatformHooks getPlatformHooks(); EnginePlatformHooks getPlatformHooks();
@@ -255,6 +262,25 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
@BlockCoordinates @BlockCoordinates
default IrisRegion getRegion(int x, int z) { 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); return getComplex().getRegionStream().get(x, z);
} }
@@ -301,7 +327,45 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
@BlockCoordinates @BlockCoordinates
default IrisBiome getCaveBiome(int x, int y, int z, IrisDimensionCarvingResolver.State state) { 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(); int worldY = y + getWorld().minHeight();
IrisDimensionCarvingEntry rootCarvingEntry = IrisDimensionCarvingResolver.resolveRootEntry(this, worldY, state); IrisDimensionCarvingEntry rootCarvingEntry = IrisDimensionCarvingResolver.resolveRootEntry(this, worldY, state);
if (rootCarvingEntry != null) { if (rootCarvingEntry != null) {
@@ -311,13 +375,21 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
return resolvedCarvingBiome; return resolvedCarvingBiome;
} }
} }
return null;
}
private IrisBiome resolveDepthCaveBiome(
int x,
int y,
int z,
IrisBiome surfaceBiome,
int surfaceY
) {
IrisBiome caveBiome = getCaveBiome(x, z); IrisBiome caveBiome = getCaveBiome(x, z);
if (caveBiome == null) { if (caveBiome == null) {
return surfaceBiome; return surfaceBiome;
} }
int surfaceY = getComplex().getHeightStream().get(x, z).intValue();
int depthBelowSurface = surfaceY - y; int depthBelowSurface = surfaceY - y;
if (depthBelowSurface <= 0) { if (depthBelowSurface <= 0) {
return surfaceBiome; return surfaceBiome;
@@ -333,6 +405,18 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
@BlockCoordinates @BlockCoordinates
default IrisBiome getSurfaceBiome(int x, int z) { 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)) { if (answersFromNaturalTerrain(x, z)) {
return getComplex().naturalSurfaceBiome(x, z); return getComplex().naturalSurfaceBiome(x, z);
} }
@@ -364,6 +448,12 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
@BlockCoordinates @BlockCoordinates
default int getHeight(int x, int z, boolean ignoreFluid) { 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)) { if (answersFromNaturalTerrain(x, z)) {
int natural = getComplex().naturalTrueHeight(x, z); int natural = getComplex().naturalTrueHeight(x, z);
return ignoreFluid ? natural : Math.max(natural, getMantle().getFluidHeight()); 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); 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 @BlockCoordinates
@Override @Override
default void catchBlockUpdates(int x, int y, int z, PlatformBlockState data) { 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(); IrisDimension dim = getDimension();
dim.getReachableBiomes(this).forEach((i) -> v.put(i.getLoadKey(), i)); 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(); return v.v();
} }
@@ -454,6 +571,22 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
boolean isStudio(); boolean isStudio();
default IrisBiome getBiome(int x, int y, int z) { 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) { if (y <= getHeight(x, z) - 2) {
return getCaveBiome(x, y, z); 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) { 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) { if (y <= getHeight(x, z) - 2) {
return getCaveOrMantleBiome(x, y, z); return getCaveOrMantleBiome(x, y, z);
} }
@@ -469,6 +618,12 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
return getSurfaceBiome(x, z); 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) { default String getObjectPlacementKey(int x, int y, int z) {
PlacedObject o = getObjectPlacement(x, y, 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()) { for (IrisObjectPlacement i : region.getObjects()) {
if (i.compatPlace(getData()).contains(object)) { 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()) { for (IrisObjectPlacement i : biome.getObjects()) {
if (i.compatPlace(getData()).contains(object)) { 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) { default void generate(int x, int z, Hunk<PlatformBlockState> blocks, Hunk<PlatformBiome> biomes, boolean multicore, long generationSessionId) {
boolean cacheContext = !getEngine().getPlatformHooks().shouldDisableChunkContextCache(getEngine()); boolean cacheContext = !getEngine().getPlatformHooks().shouldDisableChunkContextCache(getEngine());
ChunkContext.PrefillPlan prefillPlan = cacheContext ? ChunkContext.PrefillPlan.NO_CAVE : ChunkContext.PrefillPlan.NONE; 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]); EngineStage[] stages = getStages().toArray(new EngineStage[0]);
try (IrisContext.Scope chunkScope = IrisContext.open(getEngine(), generationSessionId, ctx)) { 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 validateDimensionHotload(Engine engine, IrisDimension replacement) {
} }
default void prepareRuntimeHotload(Engine engine) {
}
default void applyWorldBoundary(Engine engine) { default void applyWorldBoundary(Engine engine) {
} }
@@ -19,6 +19,8 @@
package art.arcane.iris.engine.framework; package art.arcane.iris.engine.framework;
import art.arcane.iris.core.IrisSettings; 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.engine.IrisComplex;
import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.object.IrisBiome; 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) { if (engine.getFocus() != null || engine.getFocusRegion() != null) {
return SearchPlan.unpruned(); 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(); IrisComplex complex = engine.getComplex();
BiomeSource source = new BiomeSource(engine); BiomeSource source = new BiomeSource(engine);
@@ -380,6 +389,17 @@ public final class HintedLocator<T> implements Locator<T> {
return SearchPlan.unpruned(); 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)) { if (!engine.getDimension().getRegions().contains(regionKey)) {
return SearchPlan.impossible(); return SearchPlan.impossible();
} }
@@ -655,7 +655,7 @@ public final class IrisStructureLocator {
return null; return null;
} }
if (placement.resolvedAnchor() == IrisStructureAnchorMode.SURFACE) { 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; return surfaceY < placement.getMinHeight() || surfaceY > placement.getMaxHeight() ? null : surfaceY;
} }
int bandMin = Math.max(worldMin, Math.min(placement.getMinHeight(), placement.getMaxHeight())); 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) { if (surfaceHeights.size() >= MAX_BURIAL_COLUMNS) {
return Integer.MIN_VALUE; 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); surfaceHeights.put(columnKey, surfaceY);
} }
int allowedTopY = surfaceY - UNDERGROUND_SURFACE_CLEARANCE; int allowedTopY = surfaceY - UNDERGROUND_SURFACE_CLEARANCE;
@@ -142,7 +142,7 @@ public final class NativeStructurePlacementPlanner {
return null; return null;
} }
if (!placement.isUnderground()) { 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()) { if (surfaceY < placement.getMinHeight() || surfaceY > placement.getMaxHeight()) {
return null; return null;
} }
@@ -160,7 +160,7 @@ public final class NativeStructurePlacementPlanner {
int localFluidHeight = engine.getComplex() == null int localFluidHeight = engine.getComplex() == null
? engine.getDimension().getFluidHeight() ? engine.getDimension().getFluidHeight()
: (int) Math.round(engine.getComplex().getRiverWaterSurfaceStream().get(blockX, blockZ)); : (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) { 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.IrisSettings;
import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.IrisComplex; 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.UpperDimensionContext;
import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineTarget; 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.BlockCoordinates;
import art.arcane.volmlib.util.documentation.ChunkCoordinates; import art.arcane.volmlib.util.documentation.ChunkCoordinates;
import art.arcane.iris.util.project.hunk.Hunk; 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.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk; import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.mantle.flag.MantleFlag; import art.arcane.volmlib.util.mantle.flag.MantleFlag;
@@ -196,17 +199,37 @@ public interface EngineMantle extends MatterGenerator {
} }
@ChunkCoordinates @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()) { if (!getEngine().getDimension().isUseMantle()) {
return; return;
} }
UpperDimensionContext upperCtx = getEngine().getUpperContext(); UpperDimensionContext upperCtx = getEngine().getUpperContext();
boolean protectUpper = upperCtx != null; boolean protectUpper = upperCtx != null;
DimensionStackContext stackContext = getEngine().getDimensionStackContext();
MantleChunk<Matter> chunk = getMantle().getChunk(x, z).use(); MantleChunk<Matter> chunk = getMantle().getChunk(x, z).use();
try { 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 chunkBlockX = x << 4;
int chunkBlockZ = z << 4; int chunkBlockZ = z << 4;
int gap = getEngine().getDimension().getUpperDimensionGap(); int gap = getEngine().getDimension().getUpperDimensionGap();
@@ -227,7 +227,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
if (cached != CACHE_MISS) { if (cached != CACHE_MISS) {
return cached; return cached;
} }
int surfaceY = engine.getHeight(x, z, true); int surfaceY = Engine.hostHeight(engine, x, z, true);
surfaceHeights.put(key, surfaceY); surfaceHeights.put(key, surfaceY);
return surfaceY; return surfaceY;
} }
@@ -286,7 +286,7 @@ public class IrisCaveCarver3D {
if (precomputedSurfaceHeights != null && precomputedSurfaceHeights.length > index) { if (precomputedSurfaceHeights != null && precomputedSurfaceHeights.length > index) {
columnSurfaceY = precomputedSurfaceHeights[index]; columnSurfaceY = precomputedSurfaceHeights[index];
} else { } else {
columnSurfaceY = engine.getHeight(x, z); columnSurfaceY = Engine.hostHeight(engine, x, z, true);
} }
int unclampedClearanceTopY = columnSurfaceY - surfaceClearance; int unclampedClearanceTopY = columnSurfaceY - surfaceClearance;
int clearanceTopY = Math.min(maxY, Math.max(minY, unclampedClearanceTopY)); int clearanceTopY = Math.min(maxY, Math.max(minY, unclampedClearanceTopY));
@@ -189,7 +189,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
int zzz = 8 + (z << 4); int zzz = 8 + (z << 4);
IrisRegion region = complex.getRegionStream().get(xxx, zzz); IrisRegion region = complex.getRegionStream().get(xxx, zzz);
IrisBiome surfaceBiome = complex.getTrueBiomeStream().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); IrisBiome caveBiome = resolveCaveObjectBiome(xxx, zzz, surfaceY, surfaceBiome);
if (IrisSettings.get().getGeneral().isDebug() && (x & 31) == 0 && (z & 31) == 0) { if (IrisSettings.get().getGeneral().isDebug() && (x & 31) == 0 && (z & 31) == 0) {
int carvedBlocks = 0; int carvedBlocks = 0;
@@ -948,7 +948,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
} }
Engine engine = getEngineMantle().getEngine(); Engine engine = getEngineMantle().getEngine();
IrisBiome at = engine.getCaveBiome(x, y, z); 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); return caveAnchorBiomeConflicts(at, surface, expectedCaveBiomeKey);
} }
@@ -1631,8 +1631,10 @@ public class MantleObjectComponent extends IrisMantleComponent {
public Set<String> guess(int x, int z) { public Set<String> guess(int x, int z) {
// todo The guess doesnt bring into account that the placer may return -1 // todo The guess doesnt bring into account that the placer may return -1
RNG rng = applyNoise(x, z, Cache.key(x, z) + seed()); RNG rng = applyNoise(x, z, Cache.key(x, z) + seed());
IrisBiome biome = getEngineMantle().getEngine().getSurfaceBiome((x << 4) + 8, (z << 4) + 8); int blockX = (x << 4) + 8;
IrisRegion region = getEngineMantle().getEngine().getRegion((x << 4) + 8, (z << 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<>(); Set<String> v = new KSet<>();
for (IrisObjectPlacement i : biome.getSurfaceObjects()) { for (IrisObjectPlacement i : biome.getSurfaceObjects()) {
if (rng.chance(i.getChance() + rng.d(-0.005, 0.005)) && !i.isCompatExcluded(getData())) { 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; return surfaceY;
} }
surfaceY = getEngineMantle().getEngine().getHeight(x, z); surfaceY = getEngineMantle().trueHeight(x, z);
surfaceHeights.put(columnKey, surfaceY); surfaceHeights.put(columnKey, surfaceY);
return surfaceY; return surfaceY;
} }
@@ -27,8 +27,8 @@ import art.arcane.iris.engine.framework.IrisEngineMode;
public class ModeEnclosure extends IrisEngineMode implements EngineMode { public class ModeEnclosure extends IrisEngineMode implements EngineMode {
public ModeEnclosure(Engine engine) { public ModeEnclosure(Engine engine) {
super(engine); super(engine);
var terrain = new IrisTerrainNormalActuator(getEngine()); IrisTerrainNormalActuator terrain = new IrisTerrainNormalActuator(getEngine());
var biome = new IrisBiomeActuator(getEngine()); IrisBiomeActuator biome = new IrisBiomeActuator(getEngine());
registerStage(burst( registerStage(burst(
(x, z, k, p, m, c) -> terrain.actuate(x, z, k, m, c), (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 class ModeIslands extends IrisEngineMode implements EngineMode {
public ModeIslands(Engine engine) { public ModeIslands(Engine engine) {
super(engine); super(engine);
var terrain = new IrisTerrainNormalActuator(getEngine()); IrisTerrainNormalActuator terrain = new IrisTerrainNormalActuator(getEngine());
var biome = new IrisBiomeActuator(getEngine()); IrisBiomeActuator biome = new IrisBiomeActuator(getEngine());
registerStage(burst( registerStage(burst(
(x, z, k, p, m, c) -> terrain.actuate(x, z, k, m, c), (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()) { if (shouldBypassMantleStages()) {
return; 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 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); 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(sDecorant);
registerStage(sFloatingDecorate); registerStage(sFloatingDecorate);
registerStage(sPerfection); registerStage(sPerfection);
registerStage(sCustom); if (getEngine().getDimensionStackContext() == null) {
registerStage(sCustom);
}
} }
private boolean shouldBypassMantleStages() { private boolean shouldBypassMantleStages() {
@@ -27,8 +27,8 @@ import art.arcane.iris.engine.framework.IrisEngineMode;
public class ModeSuperFlat extends IrisEngineMode implements EngineMode { public class ModeSuperFlat extends IrisEngineMode implements EngineMode {
public ModeSuperFlat(Engine engine) { public ModeSuperFlat(Engine engine) {
super(engine); super(engine);
var terrain = new IrisTerrainNormalActuator(getEngine()); IrisTerrainNormalActuator terrain = new IrisTerrainNormalActuator(getEngine());
var biome = new IrisBiomeActuator(getEngine()); IrisBiomeActuator biome = new IrisBiomeActuator(getEngine());
registerStage(burst( registerStage(burst(
(x, z, k, p, m, c) -> terrain.actuate(x, z, k, m, c), (x, z, k, p, m, c) -> terrain.actuate(x, z, k, m, c),
@@ -19,6 +19,8 @@
package art.arcane.iris.engine.modifier; package art.arcane.iris.engine.modifier;
import art.arcane.iris.core.loader.IrisData; 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.UpperDimensionContext;
import art.arcane.iris.engine.actuator.IrisDecorantActuator; import art.arcane.iris.engine.actuator.IrisDecorantActuator;
import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.Engine;
@@ -91,6 +93,8 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
UpperDimensionContext upperCtx = getEngine().getUpperContext(); UpperDimensionContext upperCtx = getEngine().getUpperContext();
boolean protectUpper = upperCtx != null && !getEngine().getDimension().isUpperDimensionCarving(); boolean protectUpper = upperCtx != null && !getEngine().getDimension().isUpperDimensionCarving();
int[] upperSurfaceHeights = protectUpper ? scratch.getOrCreateUpperSurfaceHeights() : null; int[] upperSurfaceHeights = protectUpper ? scratch.getOrCreateUpperSurfaceHeights() : null;
DimensionStackContext stackContext = getEngine().getDimensionStackContext();
DimensionStackLayout[] stackLayouts = stackContext == null ? null : new DimensionStackLayout[256];
int chunkBlockX = PowerOfTwoCoordinates.chunkToBlock(x); int chunkBlockX = PowerOfTwoCoordinates.chunkToBlock(x);
int chunkBlockZ = PowerOfTwoCoordinates.chunkToBlock(z); int chunkBlockZ = PowerOfTwoCoordinates.chunkToBlock(z);
for (int columnIndex = 0; columnIndex < 256; columnIndex++) { for (int columnIndex = 0; columnIndex < 256; columnIndex++) {
@@ -104,6 +108,9 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
int gap = getEngine().getDimension().getUpperDimensionGap(); int gap = getEngine().getDimension().getUpperDimensionGap();
upperSurfaceHeights[columnIndex] = Math.max(rawUpper, surfaceHeights[columnIndex] + gap); 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(); MantleChunk<Matter> mantleChunk = mantle.getChunk(x, z).use();
@@ -117,6 +124,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
scratch, scratch,
columnMasks, columnMasks,
upperSurfaceHeights, upperSurfaceHeights,
stackLayouts,
worldHeightSpan, worldHeightSpan,
caveLavaHeight, caveLavaHeight,
chunkBlockX, chunkBlockX,
@@ -348,6 +356,10 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
if (context.upperSurfaceHeights() != null && y >= context.upperSurfaceHeights()[columnIndex]) { if (context.upperSurfaceHeights() != null && y >= context.upperSurfaceHeights()[columnIndex]) {
return; return;
} }
if (context.stackLayouts() != null
&& context.stackLayouts()[columnIndex].isHostFeatureProtectedY(y)) {
return;
}
PlatformBlockState current = context.output().getRaw(localX, y, localZ); PlatformBlockState current = context.output().getRaw(localX, y, localZ);
if (hydrology != null && hydrology.action() == HydrologyCaveAction.SEAL_GUARD) { if (hydrology != null && hydrology.action() == HydrologyCaveAction.SEAL_GUARD) {
@@ -405,6 +417,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
IrisCarveScratch scratch, IrisCarveScratch scratch,
CarveColumnMask[] columnMasks, CarveColumnMask[] columnMasks,
int[] upperSurfaceHeights, int[] upperSurfaceHeights,
DimensionStackLayout[] stackLayouts,
int worldHeightSpan, int worldHeightSpan,
int caveLavaHeight, int caveLavaHeight,
int chunkBlockX, 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.Engine;
import art.arcane.iris.engine.framework.EngineAssignedModifier; 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.IrisBiome;
import art.arcane.iris.engine.object.IrisDepositGenerator; import art.arcane.iris.engine.object.IrisDepositGenerator;
import art.arcane.iris.engine.object.IrisDepositHeightDistribution; import art.arcane.iris.engine.object.IrisDepositHeightDistribution;
@@ -186,6 +187,11 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
continue; continue;
} }
DimensionStackLayout stackLayout = context.getDimensionStackLayout(nx, nz);
if (stackLayout != null && stackLayout.isHostFeatureProtectedY(ny)) {
continue;
}
PlatformBlockState current = data.get(nx, ny, nz); PlatformBlockState current = data.get(nx, ny, nz);
if (!canReplaceDepositTarget(current)) { if (!canReplaceDepositTarget(current)) {
continue; continue;
@@ -20,6 +20,7 @@ package art.arcane.iris.engine.modifier;
import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.DimensionStackLayout;
import art.arcane.iris.engine.IrisComplex; import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.decorator.FloatingDecorator; import art.arcane.iris.engine.decorator.FloatingDecorator;
import art.arcane.iris.engine.decorator.IrisSeaSurfaceDecorator; 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 { try {
IdentityHashMap<IrisFloatingChildBiomes, MatterBiomeInject> matterByEntry = new IdentityHashMap<>(); IdentityHashMap<IrisFloatingChildBiomes, MatterBiomeInject> matterByEntry = new IdentityHashMap<>();
for (int k = 0; k <= sample.topIdx; k++) { for (int k = 0; k <= sample.topIdx; k++) {
@@ -378,6 +395,9 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Platf
if (y < 0 || y >= chunkHeight) { if (y < 0 || y >= chunkHeight) {
continue; continue;
} }
if (!shouldWriteHostBiomeMarker(stackLayout, y)) {
continue;
}
IrisFloatingChildBiomes entry = sample.entryAt(k); IrisFloatingChildBiomes entry = sample.entryAt(k);
MatterBiomeInject matter = matterByEntry.get(entry); MatterBiomeInject matter = matterByEntry.get(entry);
if (matter == null) { 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) { private MatterBiomeInject createSkyBiomeMatter(IrisBiome target, int wx, int wz) {
if (target.isCustom()) { if (target.isCustom()) {
IrisBiomeCustom custom = target.getCustomBiome(rng, getEngine(), wx, 0, wz); 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))); 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.volmlib.util.math.RNG;
import art.arcane.iris.util.project.noise.CNG; import art.arcane.iris.util.project.noise.CNG;
import art.arcane.iris.util.project.context.IrisContext; import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.iris.util.project.stream.ProceduralStream;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; 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); 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) { 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); 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.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.B; import art.arcane.iris.util.common.data.B;
import art.arcane.iris.util.project.noise.CNG; 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.collection.KList;
import art.arcane.volmlib.util.math.RNG; 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) { 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()) { 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<>(); KList<PlatformBlockState> data = new KList<>();
@@ -70,7 +79,7 @@ final class IrisBiomeLayerGenerator {
IrisSlopeClip sc = layer.getSlopeCondition(); IrisSlopeClip sc = layer.getSlopeCondition();
if (!sc.isDefault()) { if (!sc.isDefault()) {
if (!sc.isValid(complex.getSlopeStream().get(wx, wz))) { if (!sc.isValid(resolveSlopeStream(complex, slopeStream).getDouble(wx, wz))) {
d = 0; 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) { 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> data = new KList<>();
KList<PlatformBlockState> real = new KList<>(); KList<PlatformBlockState> real = new KList<>();
int maxDepth = Math.min(maxDepthf, biome.getLockLayersMax()); int maxDepth = Math.min(maxDepthf, biome.getLockLayersMax());
@@ -190,7 +203,7 @@ final class IrisBiomeLayerGenerator {
IrisSlopeClip sc = layer.getSlopeCondition(); IrisSlopeClip sc = layer.getSlopeCondition();
if (!sc.isDefault()) { if (!sc.isDefault()) {
if (!sc.isValid(complex.getSlopeStream().get(wx, wz))) { if (!sc.isValid(resolveSlopeStream(complex, slopeStream).getDouble(wx, wz))) {
d = 0; d = 0;
} }
} }
@@ -222,6 +235,13 @@ final class IrisBiomeLayerGenerator {
return real; 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) { static KList<PlatformBlockState> generateSeaLayers(IrisBiome biome, double wx, double wz, RNG random, int maxDepth, IrisData rdata) {
KList<PlatformBlockState> data = new KList<>(); KList<PlatformBlockState> data = new KList<>();
@@ -222,6 +222,8 @@ public class IrisDimension extends IrisRegistrant {
private boolean upperDimensionObjects = false; 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.") @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; 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) @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.") @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 = ""; 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(); return regions.v();
} }
@@ -564,6 +572,7 @@ public class IrisDimension extends IrisRegistrant {
} }
Deque<String> pending = new ArrayDeque<>(); Deque<String> pending = new ArrayDeque<>();
addReachableBiomeKey(pending, getFocus());
IrisHydrology configuredHydrology = getHydrology(); IrisHydrology configuredHydrology = getHydrology();
IrisRiverHydrology configuredRivers = configuredHydrology == null ? null : configuredHydrology.getRivers(); IrisRiverHydrology configuredRivers = configuredHydrology == null ? null : configuredHydrology.getRivers();
boolean hydrologyPoliciesActive = configuredRivers != null && configuredRivers.isEnabled(); 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 { public void installBiomes(IDataFixer fixer, DataProvider data, KList<File> datapackRoots, KSet<String> biomes) throws IOException {
String namespace = getLoadKey().toLowerCase(Locale.ROOT); String keyPrefix = getCustomBiomeKeyPrefix();
installBiomes(fixer, data, datapackRoots, namespace, "", biomes); 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, public void installBiomes(IDataFixer fixer, DataProvider data, KList<File> datapackRoots,
@@ -747,18 +762,19 @@ public class IrisDimension extends IrisRegistrant {
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) { for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
String customBiomeId = customBiome.getId(); String customBiomeId = customBiome.getId();
String customBiomePath = sanitizeRegistryPath(customBiomeId, "biome");
String json = customBiome.generateJson(fixer, contentGate); String json = customBiome.generateJson(fixer, contentGate);
synchronized (biomes) { synchronized (biomes) {
if (!biomes.add(customBiomeId)) { if (!biomes.add(customBiomePath)) {
IrisLogging.debug("Duplicate Data Pack Biome: " + getLoadKey() + "/" + customBiomeId); IrisLogging.debug("Duplicate Data Pack Biome: " + getLoadKey() + "/" + customBiomeId);
continue; continue;
} }
} }
String biomePath = pathPrefix.isBlank() String biomePath = pathPrefix.isBlank()
? customBiomeId ? customBiomePath
: pathPrefix + "/" + customBiomeId; : pathPrefix + "/" + customBiomePath;
for (File datapackRoot : datapackRoots) { for (File datapackRoot : datapackRoots) {
File output = new File(datapackRoot, "data/" + namespace + "/worldgen/biome/" + biomePath + ".json"); File output = new File(datapackRoot, "data/" + namespace + "/worldgen/biome/" + biomePath + ".json");
@@ -900,6 +916,28 @@ public class IrisDimension extends IrisRegistrant {
return sanitizeDimensionTypeKeyValue(getLoadKey()); 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) { public static String sanitizeDimensionTypeKeyValue(String value) {
if (value == null || value.isBlank()) { if (value == null || value.isBlank()) {
return "dimension"; return "dimension";
@@ -918,6 +956,26 @@ public class IrisDimension extends IrisRegistrant {
return sanitized.isBlank() ? "dimension" : sanitized; 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() { public IrisDimensionType getDimensionType() {
IrisDimensionTypeOptions options = getDimensionOptions(); IrisDimensionTypeOptions options = getDimensionOptions();
if (fullbright) { if (fullbright) {
@@ -930,6 +988,10 @@ public class IrisDimension extends IrisRegistrant {
return upperDimension != null && !upperDimension.isEmpty() && !upperDimension.equalsIgnoreCase("none"); return upperDimension != null && !upperDimension.isEmpty() && !upperDimension.equalsIgnoreCase("none");
} }
public boolean hasDimensionStack() {
return dimensionStack != null;
}
public void installDimensionType(IDataFixer fixer, KList<File> datapackRoots) throws IOException { public void installDimensionType(IDataFixer fixer, KList<File> datapackRoots) throws IOException {
IrisDimensionType type = getDimensionType(); IrisDimensionType type = getDimensionType();
String json = type.toJson(fixer); 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) { 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) { 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) { public static IrisBiome getSurfaceBiome(Engine engine, Chunk c) {
@@ -1,5 +1,7 @@
package art.arcane.iris.util.project.context; 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.spi.IrisPlatforms;
import art.arcane.iris.engine.IrisComplex; import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.EngineMetrics; import art.arcane.iris.engine.framework.EngineMetrics;
@@ -27,6 +29,8 @@ public class ChunkContext {
private final ChunkedDataCache<PlatformBlockState> fluid; private final ChunkedDataCache<PlatformBlockState> fluid;
private final ChunkedDataCache<IrisRegion> region; private final ChunkedDataCache<IrisRegion> region;
private final FloatingIslandBoundarySampler floatingIslandBoundarySampler; private final FloatingIslandBoundarySampler floatingIslandBoundarySampler;
private final DimensionStackContext dimensionStackContext;
private final DimensionStackLayout[] dimensionStackLayouts;
public ChunkContext(int x, int z, IrisComplex complex) { public ChunkContext(int x, int z, IrisComplex complex) {
this(x, z, complex, 0L, true, PrefillPlan.NO_CAVE, null); 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) { 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.x = x;
this.z = z; this.z = z;
this.complex = complex; this.complex = complex;
@@ -57,6 +74,8 @@ public class ChunkContext {
this.fluid = new ChunkedDataCache<>(complex.getFluidStream(), x, z, cache); this.fluid = new ChunkedDataCache<>(complex.getFluidStream(), x, z, cache);
this.region = new ChunkedDataCache<>(complex.getRegionStream(), x, z, cache); this.region = new ChunkedDataCache<>(complex.getRegionStream(), x, z, cache);
this.floatingIslandBoundarySampler = new FloatingIslandBoundarySampler((wx, wz) -> complex.getTrueBiomeStream().get(wx, wz)); this.floatingIslandBoundarySampler = new FloatingIslandBoundarySampler((wx, wz) -> complex.getTrueBiomeStream().get(wx, wz));
this.dimensionStackContext = dimensionStackContext;
this.dimensionStackLayouts = dimensionStackContext == null ? null : new DimensionStackLayout[256];
if (cache) { if (cache) {
PrefillPlan resolvedPlan = prefillPlan == null ? PrefillPlan.NO_CAVE : prefillPlan; PrefillPlan resolvedPlan = prefillPlan == null ? PrefillPlan.NO_CAVE : prefillPlan;
@@ -165,6 +184,19 @@ public class ChunkContext {
return region; 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 { public enum PrefillPlan {
ALL(true, true, true, true, true, true), ALL(true, true, true, true, true, true),
NO_CAVE(true, true, false, true, true, true), NO_CAVE(true, true, false, true, true, true),
@@ -1,8 +1,10 @@
package art.arcane.iris.core; package art.arcane.iris.core;
import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration.IrisGeneratorBinding; 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.DataVersion;
import art.arcane.iris.core.nms.datapack.v1217.DataFixerV1217; 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.collection.KList;
import art.arcane.volmlib.util.json.JSONObject; import art.arcane.volmlib.util.json.JSONObject;
import org.junit.Rule; import org.junit.Rule;
@@ -88,6 +90,26 @@ public class IrisDatapackCompilerTest {
assertTrue(Files.isRegularFile(datapackRoot.resolve("pack.mcmeta"))); 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 @Test
public void collectsInstalledPacksThroughSymbolicLinkWorkspace() throws Exception { public void collectsInstalledPacksThroughSymbolicLinkWorkspace() throws Exception {
Path dataDirectory = temporaryFolder.newFolder("linked-data").toPath(); Path dataDirectory = temporaryFolder.newFolder("linked-data").toPath();
@@ -252,6 +274,57 @@ public class IrisDatapackCompilerTest {
assertFalse(ServerConfigurator.loadedRegistrySatisfies(loaded, changedBiomeTags)); 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 @Test
public void rejectsBindingToMissingDimension() throws Exception { public void rejectsBindingToMissingDimension() throws Exception {
Path packRoot = temporaryFolder.newFolder("missing-binding-pack").toPath(); Path packRoot = temporaryFolder.newFolder("missing-binding-pack").toPath();
@@ -325,6 +398,7 @@ public class IrisDatapackCompilerTest {
) throws Exception { ) throws Exception {
Files.createDirectories(root.resolve("dimensions")); Files.createDirectories(root.resolve("dimensions"));
Files.createDirectories(root.resolve("biomes")); Files.createDirectories(root.resolve("biomes"));
Files.createDirectories(root.resolve("dimensions").resolve(dimensionKey).getParent());
Files.writeString( Files.writeString(
root.resolve("dimensions").resolve(dimensionKey + ".json"), root.resolve("dimensions").resolve(dimensionKey + ".json"),
""" """
@@ -80,6 +80,7 @@ public class ServerConfiguratorRegistryVerificationTest {
IrisDimension dimension = mock(IrisDimension.class); IrisDimension dimension = mock(IrisDimension.class);
when(dimension.getAllBiomes(any())).thenReturn(new KList<>(biome)); when(dimension.getAllBiomes(any())).thenReturn(new KList<>(biome));
when(dimension.getLoadKey()).thenReturn("overworld"); when(dimension.getLoadKey()).thenReturn("overworld");
when(dimension.getCustomBiomeKey("missing")).thenReturn("overworld:missing");
when(dimension.getLoadFile()).thenReturn( when(dimension.getLoadFile()).thenReturn(
new File("packs/overworld/dimensions/overworld.json")); new File("packs/overworld/dimensions/overworld.json"));
when(dimension.getDimensionTypeKey()).thenReturn("iris:overworld"); when(dimension.getDimensionTypeKey()).thenReturn("iris:overworld");
@@ -1,5 +1,7 @@
package art.arcane.iris.core.pack; 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.IrisObjectMarker;
import art.arcane.iris.engine.object.IrisObjectPlacement; import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisStaticObject; import art.arcane.iris.engine.object.IrisStaticObject;
@@ -9,6 +11,7 @@ import org.junit.Test;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.List; import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
@@ -63,6 +66,38 @@ public class PackExportClosureTest {
PackExportClosure.collectStaticObjectKeys(placements).stream().sorted().toList()); 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 * Source guard: both packagers must export the ambient-spawning graph. Spawner and marker
* folders were silently omitted from exports, leaving dangling entitySpawners references. * 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 write markers/", bukkit.contains("\"markers/\""));
assertTrue("Bukkit packager must export region objects alongside biome objects", assertTrue("Bukkit packager must export region objects alongside biome objects",
bukkit.contains("regions.forEach((r) -> allPlacements.addAll(r.getObjects()))")); 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", assertTrue("Bukkit obfuscation must rewrite static object references",
bukkit.contains("placement.setObject(renameObjects.get(placement.getObject()))")); bukkit.contains("placement.setObject(renameObjects.get(placement.getObject()))"));
assertTrue("Bukkit packager must export entity loot tables", bukkit.contains("getLoot().getTables()")); 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())"); int bukkitValidation = bukkit.indexOf("PackValidator.validateForPackaging(project.getPath())");
assertTrue("Bukkit packager must validate before opening or mutating package state", assertTrue("Bukkit packager must validate before opening or mutating package state",
bukkitValidation >= 0 bukkitValidation >= 0
@@ -92,7 +133,14 @@ public class PackExportClosureTest {
assertTrue("modded packager must include initial spawns", modded.contains("getInitialSpawns")); 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 region objects", modded.contains("region.getObjects()"));
assertTrue("modded packager must export static objects", 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)"); int moddedValidation = modded.indexOf("PackValidator.validateForPackaging(packFolder)");
assertTrue("modded packager must validate before mutating package state", assertTrue("modded packager must validate before mutating package state",
moddedValidation >= 0 && moddedValidation < modded.indexOf("IO.delete(folder)")); 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")); "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 @Test
public void acceptsUpperDimensionWithoutStandaloneBoundaryWhenParentIsCovered() throws Exception { public void acceptsUpperDimensionWithoutStandaloneBoundaryWhenParentIsCovered() throws Exception {
File pack = createPack("upper-parent-only-boundary", """ 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"))); 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 @Test
public void existingTargetRemainsUnchanged() throws Exception { public void existingTargetRemainsUnchanged() throws Exception {
Path workspace = temporaryFolder.newFolder("packs").toPath(); 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)"); 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) { private static void assertBefore(String source, String first, String second) {
int firstIndex = source.indexOf(first); int firstIndex = source.indexOf(first);
int secondIndex = source.indexOf(second); 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.mantle.runtime.Mantle;
import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.matter.Matter; import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterBiomeInject;
import org.junit.After; import org.junit.After;
import org.junit.Test; 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.anyString;
import static org.mockito.ArgumentMatchers.same; import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
public class IrisBiomeActuatorCoordinateTest { public class IrisBiomeActuatorCoordinateTest {
@@ -69,6 +72,19 @@ public class IrisBiomeActuatorCoordinateTest {
assertEquals(List.of("100,-200", "100,-199", "101,-200", "101,-199"), samples); 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") @SuppressWarnings("unchecked")
private Engine engine() { private Engine engine() {
Mantle<Matter> mantle = mock(Mantle.class); 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.IrisData;
import art.arcane.iris.core.loader.ResourceLoader; 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.IrisComplex;
import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.object.IrisDimension;
@@ -22,6 +24,7 @@ import org.junit.Test;
import java.io.File; import java.io.File;
import java.nio.file.Files; import java.nio.file.Files;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Future; import java.util.concurrent.Future;
@@ -301,6 +304,20 @@ public class HintedLocatorTest {
assertEquals(1, plan.getStrideChunks()); 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 @Test
public void regionPlanMatchesOnlyTargetRegionCells() { public void regionPlanMatchesOnlyTargetRegionCells() {
IrisRegion regionA = region("reg_a"); IrisRegion regionA = region("reg_a");
@@ -326,6 +343,24 @@ public class HintedLocatorTest {
assertFalse(plan.isPossible()); 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 @Test
public void objectPlanPrunesToHostBiomes() { public void objectPlanPrunesToHostBiomes() {
IrisRegion regionA = region("reg_a"); 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; import static org.mockito.Mockito.when;
public class IrisDimensionReachableBiomesTest { 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 @Test
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public void includesExactRecursiveGenerationClosure() { public void includesExactRecursiveGenerationClosure() {
@@ -20,4 +20,20 @@ public class IrisDimensionTypeKeyTest {
assertEquals("worlds_my_pack", dimension.getDimensionTypeKey()); 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);
}
}
Loaded 100 of 119 files, more files were not shown because too many files have changed in this diff. Show more