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

@@ -18,16 +18,22 @@
package art.arcane.iris.modded;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.DimensionStackContext;
import art.arcane.iris.engine.DimensionStackLayout;
import art.arcane.iris.engine.DimensionTerrainContext;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.GenerationSessionException;
import art.arcane.iris.engine.framework.GenerationSessionLease;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.volmlib.util.math.RNG;
import com.mojang.datafixers.util.Pair;
import com.mojang.serialization.MapCodec;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.QuartPos;
@@ -37,12 +43,15 @@ import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.util.RandomSource;
import net.minecraft.util.Mth;
import net.minecraft.world.level.LevelReader;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeSource;
import net.minecraft.world.level.biome.Climate;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureSet;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
@@ -196,11 +205,16 @@ final class IrisModdedBiomeSource extends BiomeSource {
private List<Holder<Biome>> collectPossibleBiomeHolders() {
BiomeKeySets keys = biomeKeySets();
Set<String> generatedBiomeKeys = requireConfiguredStructureBiomeKeys(keys.required());
Set<String> visibleBiomeKeys = keys.visibleOnly();
LinkedHashSet<String> generatedBiomeKeys = new LinkedHashSet<>(
requireConfiguredStructureBiomeKeys(keys.structureRequired()));
generatedBiomeKeys.addAll(keys.visibleRequired());
LinkedHashSet<String> visibleBiomeKeys = new LinkedHashSet<>(keys.visibleOnly());
Registry<Biome> registry = biomeRegistry();
LinkedHashSet<Holder<Biome>> possible = new LinkedHashSet<>();
if (registry == null) {
if (keys.visibleFallbackPossible()) {
visibleBiomeKeys.add("minecraft:plains");
}
for (Holder<Biome> biome : serializedSource.possibleBiomes()) {
String key = holderKey(biome);
if (isGeneratedBiomeKey(key, generatedBiomeKeys)
@@ -215,6 +229,9 @@ final class IrisModdedBiomeSource extends BiomeSource {
throw new IllegalStateException("Iris structure biomes are not registered: "
+ missingBiomeKeys);
}
if (needsVisibleFallback(keys, registry)) {
visibleBiomeKeys.add("minecraft:plains");
}
// Registry-ordered, never a hash-ordered walk: see possibleBiomes().
registry.listElements().forEach((Holder.Reference<Biome> reference) -> {
String key = holderKey(reference);
@@ -223,7 +240,7 @@ final class IrisModdedBiomeSource extends BiomeSource {
possible.add(reference);
}
});
warnUnregisteredVisibleBiomes(registry, visibleBiomeKeys);
warnUnregisteredVisibleBiomes(registry, keys.visibleOnly());
}
if (possible.isEmpty()) {
String phase = registry == null ? "serialized biome bootstrap" : "biome registry";
@@ -233,6 +250,36 @@ final class IrisModdedBiomeSource extends BiomeSource {
return List.copyOf(possible);
}
private List<Holder<Biome>> collectVisibleBiomeHolders() {
BiomeKeySets keys = biomeKeySets();
Set<String> visibleBiomeKeys = keys.visibleEmittable();
Registry<Biome> registry = biomeRegistry();
boolean includeFallback = registry == null
? keys.visibleFallbackPossible()
: needsVisibleFallback(keys, registry);
ArrayList<Holder<Biome>> visible = new ArrayList<>();
for (Holder<Biome> biome : resolvePossibleBiomes().ordered()) {
String key = holderKey(biome);
if (isGeneratedBiomeKey(key, visibleBiomeKeys)
|| includeFallback && "minecraft:plains".equals(key)) {
visible.add(biome);
}
}
return List.copyOf(visible);
}
private static boolean needsVisibleFallback(BiomeKeySets keys, Registry<Biome> registry) {
if (keys.visibleFallbackPossible()) {
return true;
}
for (String key : keys.visibleEmittable()) {
if (resolveHolder(registry, key) == null) {
return true;
}
}
return false;
}
/**
* Scatter and derivative keys are advisory: a typo there must not stop a world from loading the way a
* missing structure biome does, so they are reported once and skipped.
@@ -273,19 +320,91 @@ final class IrisModdedBiomeSource extends BiomeSource {
}
}
@Override
public Pair<BlockPos, Holder<Biome>> findClosestBiome3d(
BlockPos origin,
int searchRadius,
int sampleResolutionHorizontal,
int sampleResolutionVertical,
Predicate<Holder<Biome>> allowed,
Climate.Sampler sampler,
LevelReader level
) {
Engine engine = engineOrNull();
if (engine == null) {
return serializedSource.findClosestBiome3d(
origin, searchRadius, sampleResolutionHorizontal, sampleResolutionVertical,
allowed, sampler, level);
}
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_locate_visible_biome");
if (lease == null) {
throw new IllegalStateException("Iris visible biome search was rejected during an engine transition");
}
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
if (!isReady(engine)) {
throw new IllegalStateException("Iris visible biome search has no active engine runtime");
}
DimensionStackContext stackContext = engine.getDimensionStackContext();
if (stackContext == null) {
return super.findClosestBiome3d(
origin, searchRadius, sampleResolutionHorizontal, sampleResolutionVertical,
allowed, sampler, level);
}
Set<Holder<Biome>> candidates = new HashSet<>();
for (Holder<Biome> biome : collectVisibleBiomeHolders()) {
if (allowed.test(biome)) {
candidates.add(biome);
}
}
if (candidates.isEmpty()) {
return null;
}
int sampleRadius = Math.floorDiv(searchRadius, sampleResolutionHorizontal);
int[] sampleYs = Mth.outFromOrigin(
origin.getY(), level.getMinY() + 1, level.getMaxY() + 1, sampleResolutionVertical).toArray();
for (BlockPos.MutableBlockPos sampleColumn : BlockPos.spiralAround(
BlockPos.ZERO, sampleRadius, Direction.EAST, Direction.SOUTH)) {
int blockX = origin.getX() + sampleColumn.getX() * sampleResolutionHorizontal;
int blockZ = origin.getZ() + sampleColumn.getZ() * sampleResolutionHorizontal;
int quartX = QuartPos.fromBlock(blockX);
int quartZ = QuartPos.fromBlock(blockZ);
DimensionStackLayout stackLayout = stackContext.sample(
QuartPos.toBlock(quartX), QuartPos.toBlock(quartZ));
for (int blockY : sampleYs) {
int quartY = QuartPos.fromBlock(blockY);
Holder<Biome> visibleBiome = resolveVisibleBiome(
engine, quartX, quartY, quartZ, sampler, stackLayout);
if (candidates.contains(visibleBiome)) {
return Pair.of(new BlockPos(blockX, blockY, blockZ), visibleBiome);
}
}
}
return null;
}
}
private Holder<Biome> getNoiseBiome(Engine engine, int quartX, int quartY, int quartZ,
Climate.Sampler sampler) {
if (isGuaranteedSurfaceBiome(quartY, engine.getMinHeight())) {
return getSurfaceStructureBiome(engine, quartX, quartZ, sampler);
}
boolean cacheable = isBiomeCacheable(
engine,
QuartPos.toBlock(quartX),
QuartPos.toBlock(quartZ)
);
long key = packNoiseKey(quartX, quartY, quartZ);
BiomeHolderTable cache = structureBiomeCache;
Holder<Biome> cached = cache.get(key);
if (cached != null) {
return cached;
if (cacheable) {
Holder<Biome> cached = cache.get(key);
if (cached != null) {
return cached;
}
}
Holder<Biome> resolved = resolveStructureBiome(engine, quartX, quartY, quartZ, sampler);
cache.put(key, resolved);
if (cacheable) {
cache.put(key, resolved);
}
return resolved;
}
@@ -302,14 +421,81 @@ final class IrisModdedBiomeSource extends BiomeSource {
if (!isReady(engine)) {
throw new IllegalStateException("Iris visible biome lookup has no active engine runtime");
}
long key = packNoiseKey(quartX, quartY, quartZ);
BiomeHolderTable cache = visibleBiomeCache;
return getVisibleNoiseBiomeWithActiveGenerationLease(
engine, quartX, quartY, quartZ, sampler);
}
}
private Holder<Biome> getVisibleNoiseBiomeWithActiveGenerationLease(
Engine engine,
int quartX,
int quartY,
int quartZ,
Climate.Sampler sampler
) {
boolean cacheable = isBiomeCacheable(
engine,
QuartPos.toBlock(quartX),
QuartPos.toBlock(quartZ)
);
long key = packNoiseKey(quartX, quartY, quartZ);
BiomeHolderTable cache = visibleBiomeCache;
if (cacheable) {
Holder<Biome> cached = cache.get(key);
if (cached != null) {
return cached;
}
Holder<Biome> resolved = resolveVisibleBiome(engine, quartX, quartY, quartZ, sampler);
}
Holder<Biome> resolved = resolveVisibleBiome(engine, quartX, quartY, quartZ, sampler);
if (cacheable) {
cache.put(key, resolved);
}
return resolved;
}
Holder<Biome> getVisibleSurfaceBiome(int blockX, int blockZ) {
Engine engine = engineOrNull();
if (engine == null) {
return null;
}
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_surface_spawn_biome");
if (lease == null) {
return null;
}
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
if (!isReady(engine)) {
return null;
}
Registry<Biome> registry = biomeRegistry();
if (registry == null) {
return null;
}
int quartX = QuartPos.fromBlock(blockX);
int quartZ = QuartPos.fromBlock(blockZ);
int sampleX = QuartPos.toBlock(quartX);
int sampleZ = QuartPos.toBlock(quartZ);
boolean cacheable = isBiomeCacheable(engine, sampleX, sampleZ);
DimensionStackContext stackContext = engine.getDimensionStackContext();
DimensionStackLayout.Layer layer = stackContext == null
? null
: stackContext.getLayout(sampleX, sampleZ).surfaceLayer();
int internalY = layer == null
? Engine.hostHeight(engine, sampleX, sampleZ, true)
: layer.clippedSurfaceY();
int quartY = QuartPos.fromBlock(internalY + engine.getMinHeight());
long key = packNoiseKey(quartX, quartY, quartZ);
BiomeHolderTable cache = visibleBiomeCache;
if (cacheable) {
Holder<Biome> cached = cache.get(key);
if (cached != null) {
return cached;
}
}
Holder<Biome> resolved = resolveVisibleBiome(
engine, quartX, quartY, quartZ, null);
if (cacheable) {
cache.put(key, resolved);
}
return resolved;
}
}
@@ -395,7 +581,9 @@ final class IrisModdedBiomeSource extends BiomeSource {
if (registry == null) {
throw new IllegalStateException("Iris structure biome lookup has no biome registry");
}
IrisBiome irisBiome = engine.getComplex().getTrueBiomeStream().get(quartX << 2, quartZ << 2);
int blockX = quartX << 2;
int blockZ = quartZ << 2;
IrisBiome irisBiome = resolveSurfaceStructureBiome(engine, blockX, blockZ);
if (irisBiome == null) {
throw new IllegalStateException("Iris returned no surface structure biome at quart "
+ quartX + "," + quartZ);
@@ -412,7 +600,8 @@ final class IrisModdedBiomeSource extends BiomeSource {
private Holder<Biome> resolveStructureBiome(Engine engine, int quartX, int quartY, int quartZ,
Climate.Sampler sampler) {
Registry<Biome> registry = biomeRegistry();
BiomeResolution resolution = resolveBiomeResolution(engine, quartX, quartY, quartZ);
BiomeResolution resolution = resolveStructureBiomeResolution(
engine, quartX, quartY, quartZ);
if (resolution == null) {
throw new IllegalStateException("Iris returned no structure biome at quart "
+ quartX + "," + quartY + "," + quartZ);
@@ -431,11 +620,36 @@ final class IrisModdedBiomeSource extends BiomeSource {
private Holder<Biome> resolveVisibleBiome(Engine engine, int quartX, int quartY, int quartZ,
Climate.Sampler sampler) {
return resolveVisibleBiome(engine, quartX, quartY, quartZ, sampler, null);
}
private Holder<Biome> resolveVisibleBiome(
Engine engine,
int quartX,
int quartY,
int quartZ,
Climate.Sampler sampler,
DimensionStackLayout stackLayout
) {
Registry<Biome> registry = biomeRegistry();
BiomeResolution resolution = resolveBiomeResolution(engine, quartX, quartY, quartZ);
BiomeResolution resolution = resolveBiomeResolution(
engine, quartX, quartY, quartZ, true, stackLayout);
if (resolution == null || registry == null) {
return serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler);
}
return resolveVisibleBiome(
engine, registry, resolution, quartX, quartY, quartZ, sampler);
}
private Holder<Biome> resolveVisibleBiome(
Engine engine,
Registry<Biome> registry,
BiomeResolution resolution,
int quartX,
int quartY,
int quartZ,
Climate.Sampler sampler
) {
String biomeKey;
if (resolution.irisBiome().isCustom()) {
IrisBiomeCustom customBiome = resolution.irisBiome().getCustomBiome(
@@ -444,7 +658,8 @@ final class IrisModdedBiomeSource extends BiomeSource {
return fallbackBiome(registry, "custom derivative of '"
+ resolution.irisBiome().getLoadKey() + "'", quartX, quartY, quartZ, sampler);
}
biomeKey = ModdedWorldgenIds.biomeRef(engine, customBiome.getId());
biomeKey = customBiomeRef(
resolution.packName(), resolution.dimension(), customBiome.getId());
} else if (resolution.underground()) {
biomeKey = resolution.irisBiome().getGroundBiomeKey(
resolution.rng(), engine, resolution.blockX(), resolution.blockY(), resolution.blockZ());
@@ -459,6 +674,37 @@ final class IrisModdedBiomeSource extends BiomeSource {
}
private BiomeResolution resolveBiomeResolution(Engine engine, int quartX, int quartY, int quartZ) {
return resolveBiomeResolution(engine, quartX, quartY, quartZ, true);
}
private BiomeResolution resolveStructureBiomeResolution(
Engine engine,
int quartX,
int quartY,
int quartZ
) {
return resolveBiomeResolution(engine, quartX, quartY, quartZ, false);
}
private BiomeResolution resolveBiomeResolution(
Engine engine,
int quartX,
int quartY,
int quartZ,
boolean includeDimensionStack
) {
return resolveBiomeResolution(
engine, quartX, quartY, quartZ, includeDimensionStack, null);
}
private BiomeResolution resolveBiomeResolution(
Engine engine,
int quartX,
int quartY,
int quartZ,
boolean includeDimensionStack,
DimensionStackLayout stackLayout
) {
if (!isReady(engine)) {
return null;
}
@@ -467,27 +713,75 @@ final class IrisModdedBiomeSource extends BiomeSource {
int blockZ = quartZ << 2;
int internalY = blockY - engine.getMinHeight();
int caveSwitchY = Math.max(-8 - engine.getMinHeight(), 40);
DimensionStackLayout.Layer stackLayer = includeDimensionStack
? stackLayout == null
? resolveDimensionStackLayer(engine, blockX, internalY, blockZ)
: stackLayout.layerAt(internalY)
: null;
IrisDimension owningDimension = stackLayer == null
? engine.getDimension()
: stackLayer.terrainContext().getDimension();
IrisData owningData = stackLayer == null
? engine.getData()
: stackLayer.terrainContext().getData();
if (stackLayer != null && !stackLayer.terrainContext().isSelfReferencing()) {
IrisBiome stackedBiome = stackLayer.biome();
if (stackedBiome == null) {
return null;
}
return createBiomeResolution(
engine,
stackedBiome,
false,
owningDimension,
owningData,
blockX,
blockY,
blockZ
);
}
boolean underground = false;
IrisBiome irisBiome;
if (internalY <= caveSwitchY) {
int surfaceY = engine.getComplex().getHeightStream().get(blockX, blockZ).intValue();
if (stackLayer != null) {
int surfaceY = stackLayer.surfaceY();
underground = isUnderground(internalY, surfaceY);
irisBiome = underground
? engine.getCaveBiome(
blockX,
internalY,
blockZ,
null,
stackLayer.biome(),
surfaceY
)
: stackLayer.biome();
} else if (internalY <= caveSwitchY) {
int surfaceY = Engine.hostHeight(engine, blockX, blockZ, true);
underground = isUnderground(internalY, surfaceY);
irisBiome = underground
? engine.getCaveBiome(blockX, internalY, blockZ)
: engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
: engine.getHostSurfaceBiome(blockX, blockZ);
} else {
irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
irisBiome = engine.getHostSurfaceBiome(blockX, blockZ);
}
if (irisBiome == null && underground) {
irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
irisBiome = stackLayer == null
? engine.getHostSurfaceBiome(blockX, blockZ)
: stackLayer.biome();
}
if (irisBiome == null) {
return null;
}
IrisModdedChunkGenerator current = generator;
long worldSeed = current == null ? engine.getWorld().getRawWorldSeed() : current.visibleBiomeSeed();
long seed = biomeResolutionSeed(worldSeed, blockX, blockY, blockZ);
return new BiomeResolution(irisBiome, underground, blockX, blockY, blockZ, new RNG(seed));
return createBiomeResolution(
engine,
irisBiome,
underground,
owningDimension,
owningData,
blockX,
blockY,
blockZ
);
}
private Holder<Biome> resolveRequiredStructureBiome(int quartX, int quartY, int quartZ) {
@@ -510,9 +804,10 @@ final class IrisModdedBiomeSource extends BiomeSource {
}
IrisBiome irisBiome;
if (isGuaranteedSurfaceBiome(quartY, engine.getMinHeight())) {
irisBiome = engine.getComplex().getTrueBiomeStream().get(quartX << 2, quartZ << 2);
irisBiome = resolveSurfaceStructureBiome(engine, quartX << 2, quartZ << 2);
} else {
BiomeResolution resolution = resolveBiomeResolution(engine, quartX, quartY, quartZ);
BiomeResolution resolution = resolveStructureBiomeResolution(
engine, quartX, quartY, quartZ);
irisBiome = resolution == null ? null : resolution.irisBiome();
}
if (irisBiome == null) {
@@ -557,6 +852,10 @@ final class IrisModdedBiomeSource extends BiomeSource {
return internalY <= surfaceY - 8;
}
static boolean isBiomeCacheable(Engine engine, int blockX, int blockZ) {
return !engine.answersFromNaturalTerrain(blockX, blockZ);
}
static boolean isMonumentSurfaceBiomeQuery(int blockY, int radius, int minHeight, int fluidHeight) {
return radius == 29 && blockY == minHeight + fluidHeight;
}
@@ -612,7 +911,7 @@ final class IrisModdedBiomeSource extends BiomeSource {
}
private Set<String> exactStructureBiomeKeys() {
return biomeKeySets().required();
return biomeKeySets().structureRequired();
}
/**
@@ -636,11 +935,12 @@ final class IrisModdedBiomeSource extends BiomeSource {
private BiomeKeySets collectBiomeKeySets(long generation) {
IrisModdedChunkGenerator current = generator;
if (current == null) {
return new BiomeKeySets(generation, Set.of(), Set.of());
return new BiomeKeySets(generation, Set.of(), Set.of(), Set.of(), Set.of(), false);
}
Engine engine = current.structureEngineOrNull();
if (engine == null) {
return new BiomeKeySets(generation, current.configuredStructureBiomeKeys(), Set.of());
return new BiomeKeySets(
generation, current.configuredStructureBiomeKeys(), Set.of(), Set.of(), Set.of(), false);
}
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_structure_biome_keys");
if (lease == null) {
@@ -650,36 +950,154 @@ final class IrisModdedBiomeSource extends BiomeSource {
if (!isReady(engine)) {
throw new IllegalStateException("Iris structure biome key lookup has no active engine runtime");
}
LinkedHashSet<String> required = new LinkedHashSet<>();
LinkedHashSet<String> visible = new LinkedHashSet<>();
for (IrisBiome irisBiome : engine.getAllBiomes()) {
String derivative = normalizeKey(irisBiome.getStructureDerivativeKey());
if (derivative != null) {
required.add(derivative);
}
Set<String> structureRequired = collectStructureBiomeKeys(
engine.getDimension(), engine.getData());
LinkedHashSet<String> visibleRequired = new LinkedHashSet<>();
LinkedHashSet<String> visibleEmittable = new LinkedHashSet<>();
boolean visibleFallbackPossible = false;
for (OwnedBiome ownedBiome : getOwnedBiomes(engine)) {
IrisBiome irisBiome = ownedBiome.biome();
if (irisBiome.isCustom()) {
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
required.add(ModdedWorldgenIds.biomeRef(engine, customBiome.getId()));
String customKey = customBiomeRef(
ownedBiome.data().getDataFolder().getName(),
ownedBiome.dimension(),
customBiome.getId()
);
visibleRequired.add(customKey);
visibleEmittable.add(customKey);
}
continue;
}
addVisibleBiomeKey(visible, irisBiome.getDerivativeKey());
visibleFallbackPossible |= !addVisibleBiomeKey(
visibleEmittable, irisBiome.getDerivativeKey());
for (String scatter : irisBiome.getBiomeScatter()) {
addVisibleBiomeKey(visible, scatter);
visibleFallbackPossible |= !addVisibleBiomeKey(visibleEmittable, scatter);
}
for (String scatter : irisBiome.getBiomeSkyScatter()) {
addVisibleBiomeKey(visible, scatter);
visibleFallbackPossible |= !addVisibleBiomeKey(visibleEmittable, scatter);
}
}
visible.removeAll(required);
return new BiomeKeySets(generation, Set.copyOf(required), Set.copyOf(visible));
LinkedHashSet<String> visible = new LinkedHashSet<>(visibleEmittable);
visible.removeAll(structureRequired);
visible.removeAll(visibleRequired);
return new BiomeKeySets(
generation,
Set.copyOf(structureRequired),
Set.copyOf(visibleRequired),
Set.copyOf(visible),
Set.copyOf(visibleEmittable),
visibleFallbackPossible
);
}
}
private static void addVisibleBiomeKey(Set<String> target, String key) {
String normalized = normalizeKey(key);
if (normalized != null) {
target.add(normalized);
static Set<String> collectStructureBiomeKeys(IrisDimension dimension, IrisData data) {
LinkedHashSet<String> keys = new LinkedHashSet<>();
for (IrisBiome irisBiome : dimension.getReachableBiomes(() -> data)) {
String derivative = normalizeKey(irisBiome.getStructureDerivativeKey());
if (derivative != null) {
keys.add(derivative);
}
}
return Set.copyOf(keys);
}
private static boolean addVisibleBiomeKey(Set<String> target, String key) {
String normalized = normalizeKey(key);
if (normalized == null) {
return false;
}
target.add(normalized);
return true;
}
static String customBiomeRef(String packName, IrisDimension dimension, String customBiomeId) {
return ModdedWorldgenIds.biomeRef(packName, dimension.getLoadKey(), customBiomeId);
}
private BiomeResolution createBiomeResolution(
Engine engine,
IrisBiome irisBiome,
boolean underground,
IrisDimension dimension,
IrisData data,
int blockX,
int blockY,
int blockZ
) {
IrisModdedChunkGenerator current = generator;
long worldSeed = current == null
? engine.getWorld().getRawWorldSeed()
: current.visibleBiomeSeed();
long seed = biomeResolutionSeed(worldSeed, blockX, blockY, blockZ);
return new BiomeResolution(
irisBiome,
underground,
dimension,
data.getDataFolder().getName(),
blockX,
blockY,
blockZ,
new RNG(seed)
);
}
private static List<OwnedBiome> getOwnedBiomes(Engine engine) {
ArrayList<OwnedBiome> biomes = new ArrayList<>();
IrisDimension hostDimension = engine.getDimension();
IrisData hostData = engine.getData();
addOwnedBiomes(
biomes,
hostDimension,
hostData,
hostDimension.getReachableBiomes(engine)
);
DimensionStackContext stackContext = engine.getDimensionStackContext();
if (stackContext == null) {
return biomes;
}
for (DimensionTerrainContext terrainContext : stackContext.getLayersBottomToTop()) {
if (terrainContext.isSelfReferencing()) {
continue;
}
IrisDimension dimension = terrainContext.getDimension();
IrisData data = terrainContext.getData();
addOwnedBiomes(
biomes,
dimension,
data,
dimension.getReachableBiomes(terrainContext)
);
}
return biomes;
}
private static void addOwnedBiomes(
List<OwnedBiome> target,
IrisDimension dimension,
IrisData data,
Iterable<IrisBiome> biomes
) {
for (IrisBiome biome : biomes) {
if (biome != null) {
target.add(new OwnedBiome(dimension, data, biome));
}
}
}
private static DimensionStackLayout.Layer resolveDimensionStackLayer(
Engine engine,
int blockX,
int internalY,
int blockZ
) {
DimensionStackContext stackContext = engine.getDimensionStackContext();
return stackContext == null ? null : stackContext.getLayerAt(blockX, internalY, blockZ);
}
private static IrisBiome resolveSurfaceStructureBiome(Engine engine, int blockX, int blockZ) {
return engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
}
private static Set<String> registeredBiomeKeys(Registry<Biome> registry) {
@@ -726,7 +1144,13 @@ final class IrisModdedBiomeSource extends BiomeSource {
Holder<Biome> plains = resolveHolder(registry, "minecraft:plains");
warnUnresolvedBiome(unresolvedKey, plains == null ? "the serialized biome source" : "minecraft:plains",
quartX, quartY, quartZ);
return plains == null ? serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler) : plains;
if (plains != null) {
return plains;
}
if (sampler == null) {
throw new IllegalStateException("Iris cannot resolve a registered fallback biome");
}
return serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler);
}
private void warnUnresolvedBiome(String unresolvedKey, String fallback,
@@ -753,11 +1177,29 @@ final class IrisModdedBiomeSource extends BiomeSource {
return ((long) x << 32) ^ ((long) z & 4294967295L);
}
private record BiomeResolution(IrisBiome irisBiome, boolean underground, int blockX, int blockY,
int blockZ, RNG rng) {
private record BiomeResolution(
IrisBiome irisBiome,
boolean underground,
IrisDimension dimension,
String packName,
int blockX,
int blockY,
int blockZ,
RNG rng
) {
}
private record BiomeKeySets(long generation, Set<String> required, Set<String> visibleOnly) {
private record OwnedBiome(IrisDimension dimension, IrisData data, IrisBiome biome) {
}
private record BiomeKeySets(
long generation,
Set<String> structureRequired,
Set<String> visibleRequired,
Set<String> visibleOnly,
Set<String> visibleEmittable,
boolean visibleFallbackPossible
) {
}
private record PossibleBiomes(long generation, List<Holder<Biome>> ordered, Set<Holder<Biome>> set) {
@@ -30,6 +30,7 @@ import art.arcane.iris.engine.object.IrisImportedStructureControl;
import art.arcane.iris.nativegen.NativeStructureStartInjector;
import art.arcane.iris.nativegen.NativeStructureReferenceRepair;
import art.arcane.iris.nativegen.NativeStructureVanillaLocator;
import art.arcane.iris.nativegen.NativeStructureVolumeIndex;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState;
@@ -89,7 +90,9 @@ import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -181,11 +184,18 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
}
requireGlobalStructureGeneration(
level.getServer().getWorldGenSettings().options().generateStructures(), dimensionKey);
Engine activeEngine = engineIfBound();
Engine replacement = ModdedWorldEngines.prepareReplacement(level, pack, packDimensionKey, seed);
try {
if (activeEngine != null) {
requireStructureBiomeUniverseCompatible(
activeEngine.getDimension(), replacement.getDimension());
}
nativeStructures.installVolumeIndex(level, replacement);
ModdedWorldEngines.installReplacement(level, replacement);
replacement.getPlatformHooks().applyWorldBoundary(replacement);
} catch (Throwable error) {
NativeStructureVolumeIndex.uninstall(replacement);
try {
ModdedWorldEngines.closeUnregistered(replacement);
} catch (Throwable cleanupError) {
@@ -203,17 +213,14 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
this.activePack = pack;
this.activeDimensionKey = packDimensionKey;
this.seedOverride = seed;
this.engine = replacement;
this.configuredStructureBiomeKeys = null;
this.configuredPack = null;
this.heightMetadata = engineHeights(replacement);
this.engineBinding.reset();
this.engineBinding.complete(replacement);
this.announced.set(false);
this.structureBiomeSource.clearCaches();
this.importedFeatures.invalidate();
this.nativeStructures.clearWorldCheckStructureShifts();
this.spawnTables.resetVanillaSpawnBiomes();
resetRuntimeCaches();
this.engine = replacement;
this.engineBinding.complete(replacement);
// Bind time: a feature-order cycle in the new pack is reported here, once, and degrades to features-off.
// Never waits on a build owned by another thread: this method owns the generator monitor and the build
// path can need it.
@@ -751,7 +758,38 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
return lastChunkGenAt;
}
public void onHotload() {
public void prepareRuntimeHotload(ServerLevel level, Engine current) {
if (this.engine != current || level.getChunkSource().getGenerator() != this) {
throw new IllegalStateException("Iris generator '" + dimensionKey
+ "' cannot prepare caches for an unrelated engine hotload");
}
resetRuntimeCaches();
nativeStructures.installVolumeIndex(level, current);
}
public static void requireStructureBiomeUniverseCompatible(
IrisDimension active,
IrisDimension replacement
) {
IrisData activeData = Objects.requireNonNull(active.getLoader(),
"Active Iris dimension has no pack loader");
IrisData replacementData = Objects.requireNonNull(replacement.getLoader(),
"Replacement Iris dimension has no pack loader");
requireStructureBiomeUniverseCompatible(
IrisModdedBiomeSource.collectStructureBiomeKeys(active, activeData),
IrisModdedBiomeSource.collectStructureBiomeKeys(replacement, replacementData));
}
static void requireStructureBiomeUniverseCompatible(Set<String> active, Set<String> replacement) {
if (active.equals(replacement)) {
return;
}
throw new IllegalArgumentException("Iris cannot hotload a changed host structure-biome derivative set "
+ "from " + new TreeSet<>(active) + " to " + new TreeSet<>(replacement)
+ ". Restart the server so Minecraft can rebuild its immutable structure state.");
}
private void resetRuntimeCaches() {
configuredStructureBiomeKeys = null;
structureBiomeSource.clearCaches();
importedFeatures.invalidate();
@@ -986,7 +1024,17 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
Registry<Biome> registry = region.registryAccess().lookupOrThrow(Registries.BIOME);
spawnTables.initializeVanillaSpawnBiomes(registry);
ChunkPos center = region.getCenter();
Holder<Biome> visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY()));
Holder<Biome> visibleBiome;
if (engine == null || engine.getDimensionStackContext() == null) {
visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY()));
} else {
visibleBiome = structureBiomeSource.getVisibleSurfaceBiome(
center.getMinBlockX() + 8,
center.getMinBlockZ() + 8);
if (visibleBiome == null) {
visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY()));
}
}
Holder<Biome> vanillaBiome = spawnTables.vanillaSpawnBiome(visibleBiome.value());
WorldgenRandom random = new WorldgenRandom(new LegacyRandomSource(RandomSupport.generateUniqueSeed()));
random.setDecorationSeed(region.getSeed(), center.getMinBlockX(), center.getMinBlockZ());
@@ -1029,7 +1077,10 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
boolean ignoreFluid = !type.isOpaque().test(Blocks.WATER.defaultBlockState());
try (GenerationSessionLease lease = current.acquireGenerationLease("modded_base_height");
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
return heightAccessor.getMinY() + current.getHeight(x, z, ignoreFluid) + 1;
int height = current.getDimensionStackContext() == null
? current.getHeight(x, z, ignoreFluid)
: Engine.hostHeight(current, x, z, ignoreFluid);
return heightAccessor.getMinY() + height + 1;
} catch (GenerationSessionException e) {
throw new IllegalStateException("Iris base height query could not acquire its engine runtime.", e);
}
@@ -1043,8 +1094,13 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
try (GenerationSessionLease lease = current.acquireGenerationLease("modded_base_column");
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
BlockState airState = Blocks.AIR.defaultBlockState();
int surface = current.getHeight(x, z, true);
int fluid = current.getHeight(x, z, false);
boolean dimensionStack = current.getDimensionStackContext() != null;
int surface = dimensionStack
? Engine.hostHeight(current, x, z, true)
: current.getHeight(x, z, true);
int fluid = dimensionStack
? Engine.hostHeight(current, x, z, false)
: current.getHeight(x, z, false);
BlockState stone = Blocks.STONE.defaultBlockState();
BlockState water = Blocks.WATER.defaultBlockState();
int solidEnd = Math.max(0, Math.min(states.length, surface + 1));
@@ -21,6 +21,7 @@ package art.arcane.iris.modded;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBiomeWriter;
import art.arcane.iris.util.project.context.IrisContext;
@@ -97,7 +98,7 @@ public final class ModdedBiomeWriter implements PlatformBiomeWriter {
return key;
}
Engine engine = context.getEngine();
String prefix = engine.getDimension().getLoadKey() + ":";
String prefix = engine.getDimension().getCustomBiomeKeyPrefix();
if (!key.regionMatches(true, 0, prefix, 0, prefix.length())) {
return key;
}
@@ -138,13 +139,7 @@ public final class ModdedBiomeWriter implements PlatformBiomeWriter {
}
private int idForDerivative(Registry<Biome> registry, String key) {
int colon = key.indexOf(':');
if (colon <= 0 || colon >= key.length() - 1) {
return -1;
}
String dimensionLoadKey = key.substring(0, colon);
String customBiomeId = key.substring(colon + 1);
IrisBiome owner = findCustomBiomeOwner(dimensionLoadKey, customBiomeId);
IrisBiome owner = findCustomBiomeOwner(key);
if (owner == null) {
return -1;
}
@@ -155,20 +150,18 @@ public final class ModdedBiomeWriter implements PlatformBiomeWriter {
return idForKey(registry, derivativeKey);
}
private IrisBiome findCustomBiomeOwner(String dimensionLoadKey, String customBiomeId) {
private IrisBiome findCustomBiomeOwner(String key) {
for (Engine engine : ModdedWorldEngines.activeEngines()) {
if (engine == null || engine.isClosed()) {
continue;
}
if (!dimensionLoadKey.equalsIgnoreCase(engine.getDimension().getLoadKey())) {
continue;
}
for (IrisBiome biome : engine.getDimension().getAllBiomes(engine)) {
IrisDimension dimension = engine.getDimension();
for (IrisBiome biome : dimension.getAllBiomes(engine)) {
if (!biome.isCustom()) {
continue;
}
for (IrisBiomeCustom custom : biome.getCustomDerivitives()) {
if (customBiomeId.equals(custom.getId())) {
if (key.equalsIgnoreCase(dimension.getCustomBiomeKey(custom.getId()))) {
return biome;
}
}
@@ -258,7 +258,7 @@ public final class ModdedBlockBreakHandler {
if (skipsParents(providers)) {
return providers;
}
IrisRegion region = engine.getRegion(position.getX(), position.getZ());
IrisRegion region = engine.getRegion(position.getX(), relativeY, position.getZ());
if (region != null) {
addMatching(providers, region.getBlockDrops(), brokenState, data);
}
@@ -62,7 +62,6 @@ final class ModdedDimensionMetadata {
static Set<String> collectConfiguredBiomeKeys(Iterable<IrisBiome> biomes, String dimensionLoadKey) {
LinkedHashSet<String> keys = new LinkedHashSet<>();
String namespace = dimensionLoadKey.toLowerCase(Locale.ROOT);
for (IrisBiome irisBiome : biomes) {
if (irisBiome == null) {
continue;
@@ -75,7 +74,7 @@ final class ModdedDimensionMetadata {
continue;
}
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
keys.add(namespace + ":" + customBiome.getId().toLowerCase(Locale.ROOT));
keys.add(IrisDimension.customBiomeKey(dimensionLoadKey, customBiome.getId()));
}
}
return Set.copyOf(keys);
@@ -226,7 +226,7 @@ public final class ModdedEngineEffects extends EngineAssignedComponent implement
int blockY = floor(player.getY()) - getEngine().getWorld().minHeight();
int blockZ = floor(player.getZ());
state.biome(getEngine().getBiome(blockX, blockY, blockZ));
state.region(getEngine().getRegion(blockX, blockZ));
state.region(getEngine().getRegion(blockX, blockY, blockZ));
}
private void applyEffects(ServerLevel level, ServerPlayer player, Iterable<IrisEffect> effects) {
@@ -52,7 +52,6 @@ import java.nio.file.attribute.BasicFileAttributes;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HexFormat;
import java.util.LinkedHashMap;
@@ -514,18 +513,13 @@ public final class ModdedForcedDatapack {
if (!dimensionsDirectory.isDirectory()) {
return false;
}
File[] dimensionFiles = dimensionsDirectory.listFiles(
(File file) -> file.isFile() && file.getName().endsWith(".json"));
if (dimensionFiles == null) {
throw new IOException("Iris could not read dimensions for pack '" + packName + "'");
}
if (dimensionFiles.length == 0) {
IrisData data = IrisData.get(packFolder);
String[] dimensionKeys = data.getDimensionLoader().getPossibleKeys();
if (dimensionKeys == null || dimensionKeys.length == 0) {
return false;
}
Arrays.sort(dimensionFiles, Comparator.comparing(File::getName));
IrisData data = IrisData.get(packFolder);
for (File dimensionFile : dimensionFiles) {
String dimensionKey = dimensionFile.getName().substring(0, dimensionFile.getName().length() - ".json".length());
List<String> sortedDimensionKeys = Stream.of(dimensionKeys).sorted().toList();
for (String dimensionKey : sortedDimensionKeys) {
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey);
if (dimension == null) {
throw new IllegalStateException("Iris pack '" + packName + "' dimension '"
@@ -18,6 +18,8 @@
package art.arcane.iris.modded;
import art.arcane.iris.engine.DimensionStackContext;
import art.arcane.iris.engine.DimensionStackLayout;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.GenerationSessionLease;
import art.arcane.iris.engine.framework.NativeFeatureGenerationPolicy;
@@ -57,7 +59,10 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.IntBinaryOperator;
import java.util.function.Predicate;
/**
* Native placed-feature passthrough for one Iris dimension, gated on {@code importedFeatures.enabled}.
@@ -219,7 +224,14 @@ final class ModdedImportedFeatureStage {
private FeatureTable buildTable(Engine engine, IrisImportedFeatureControl control, long generation) {
// Registry-ordered biome list. FeatureSorter's cycle detection walks it, so an unordered list makes
// detection depend on JVM hash order and turns a real cycle into an intermittent one.
List<Holder<Biome>> biomes = biomeSource.orderedPossibleBiomes();
Set<String> hostBiomeKeys = hostBiomeKeys(engine);
List<Holder<Biome>> biomes = new ArrayList<>();
for (Holder<Biome> biome : biomeSource.orderedPossibleBiomes()) {
String key = holderKey(biome);
if (key != null && hostBiomeKeys.contains(key)) {
biomes.add(biome);
}
}
if (biomes.isEmpty()) {
ModdedIrisLog.error("Iris importedFeatures is on but {} exposes no biomes; features off",
dimensionKey(engine));
@@ -262,7 +274,7 @@ final class ModdedImportedFeatureStage {
*/
private Map<String, Holder<Biome>> customBiomeDerivatives(Engine engine, Map<String, Holder<Biome>> byKey) {
Map<String, Holder<Biome>> derivatives = new HashMap<>();
for (IrisBiome irisBiome : engine.getAllBiomes()) {
for (IrisBiome irisBiome : engine.getDimension().getReachableBiomes(engine)) {
if (!irisBiome.isCustom()) {
continue;
}
@@ -322,14 +334,24 @@ final class ModdedImportedFeatureStage {
List<FeatureSorter.StepFeatureData> steps = table.steps();
WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed()));
long decorationSeed = random.setDecorationSeed(level.getSeed(), origin.getX(), origin.getZ());
Set<Holder<Biome>> chunkBiomes = chunkBiomes(level, sectionPos, table);
IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData());
int staticMinY = engine.getMinHeight();
WorldGenLevel placementLevel = staticObjects.isEmpty() ? level : ModdedNativeStructureWorldgenAccess.create(
DimensionStackContext stackContext = engine.getDimensionStackContext();
Set<Holder<Biome>> chunkBiomes = chunkBiomes(level, sectionPos, table, stackContext);
IntBinaryOperator surfaceFirstFreeY = stackContext == null
? (x, z) -> level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z)
: (x, z) -> Engine.hostHeight(engine, x, z, false) + staticMinY + 1;
IntBinaryOperator floorFirstFreeY = stackContext == null
? (x, z) -> level.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, x, z)
: (x, z) -> Engine.hostHeight(engine, x, z, true) + staticMinY + 1;
WorldGenLevel placementLevel = staticObjects.isEmpty() && stackContext == null
? level
: ModdedNativeStructureWorldgenAccess.create(
level, centerPos,
(x, z) -> level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z),
(x, z) -> level.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, x, z),
position -> staticObjects.contains(position.getX(), position.getY() - staticMinY, position.getZ()));
surfaceFirstFreeY,
floorFirstFreeY,
stackContext != null,
importedFeatureProtection(staticObjects, stackContext, staticMinY));
try {
for (int stepIndex = 0; stepIndex < steps.size(); stepIndex++) {
@@ -349,6 +371,58 @@ final class ModdedImportedFeatureStage {
WorldCheckFeaturePlacement.recordPlacementPass();
}
private static Set<String> hostBiomeKeys(Engine engine) {
Set<String> keys = new LinkedHashSet<>();
for (IrisBiome irisBiome : engine.getDimension().getReachableBiomes(engine)) {
addKey(keys, irisBiome.getStructureDerivativeKey());
addKey(keys, irisBiome.getDerivativeKey());
for (String scatter : irisBiome.getBiomeScatter()) {
addKey(keys, scatter);
}
for (String scatter : irisBiome.getBiomeSkyScatter()) {
addKey(keys, scatter);
}
if (!irisBiome.isCustom()) {
continue;
}
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
addKey(keys, ModdedWorldgenIds.biomeRef(engine, customBiome.getId()));
}
}
return keys;
}
private static void addKey(Set<String> keys, String key) {
String normalized = normalizeKey(key);
if (normalized != null) {
keys.add(normalized);
}
}
private static Predicate<BlockPos> importedFeatureProtection(
IrisStaticObjectLayer staticObjects,
DimensionStackContext stackContext,
int minimumY
) {
Map<Long, DimensionStackLayout> layouts = new ConcurrentHashMap<>();
return position -> {
if (!staticObjects.isEmpty() && staticObjects.contains(
position.getX(), position.getY() - minimumY, position.getZ())) {
return true;
}
if (stackContext == null) {
return false;
}
long columnKey = ((long) position.getX() << 32)
^ (position.getZ() & 0xFFFFFFFFL);
DimensionStackLayout layout = layouts.computeIfAbsent(
columnKey,
ignored -> stackContext.sample(position.getX(), position.getZ())
);
return layout.isHostFeatureProtectedY(position.getY() - minimumY);
};
}
private void placeStep(WorldGenLevel level, FeatureTable table, FeatureSorter.StepFeatureData stepData,
Registry<PlacedFeature> featureRegistry, Set<Holder<Biome>> chunkBiomes,
IrisModdedChunkGenerator owner, WorldgenRandom random, long decorationSeed,
@@ -394,12 +468,37 @@ final class ModdedImportedFeatureStage {
* shape as vanilla, which drops any section biome its biome source does not claim. Holders are canonicalised
* back onto the table's own holders so the index mapping cannot be handed a holder it never saw.
*/
private Set<Holder<Biome>> chunkBiomes(WorldGenLevel level, SectionPos sectionPos, FeatureTable table) {
private Set<Holder<Biome>> chunkBiomes(
WorldGenLevel level,
SectionPos sectionPos,
FeatureTable table,
DimensionStackContext stackContext
) {
List<Holder<Biome>> collected = new ArrayList<>();
ChunkPos.rangeClosed(sectionPos.chunk(), 1).forEach((ChunkPos chunkPos) -> {
ChunkAccess neighbour = level.getChunk(chunkPos.x(), chunkPos.z());
for (LevelChunkSection section : neighbour.getSections()) {
section.getBiomes().getAll(collected::add);
LevelChunkSection[] sections = neighbour.getSections();
for (int sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) {
LevelChunkSection section = sections[sectionIndex];
if (stackContext == null) {
section.getBiomes().getAll(collected::add);
continue;
}
int sectionMinimumY = neighbour.getSectionYFromSectionIndex(sectionIndex) << 4;
for (int quartY = 0; quartY < 4; quartY++) {
int internalY = sectionMinimumY + (quartY << 2) - level.getMinY();
for (int quartX = 0; quartX < 4; quartX++) {
int blockX = chunkPos.getMinBlockX() + (quartX << 2);
for (int quartZ = 0; quartZ < 4; quartZ++) {
int blockZ = chunkPos.getMinBlockZ() + (quartZ << 2);
DimensionStackLayout.Layer layer = stackContext.getLayerAt(
blockX, internalY, blockZ);
if (layer.terrainContext().isSelfReferencing()) {
collected.add(section.getBiomes().get(quartX, quartY, quartZ));
}
}
}
}
}
});
Set<Holder<Biome>> present = new LinkedHashSet<>();
@@ -18,6 +18,8 @@
package art.arcane.iris.modded;
import art.arcane.iris.engine.DimensionStackContext;
import art.arcane.iris.engine.DimensionStackLayout;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
@@ -78,6 +80,7 @@ import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiConsumer;
import java.util.function.IntBinaryOperator;
import java.util.function.Predicate;
/**
* Native (vanilla registry) structure stage for {@link IrisModdedChunkGenerator}. The generator keeps the
@@ -266,7 +269,7 @@ final class ModdedNativeStructureStage {
undergroundStep,
decision.preserveSourceY(),
decision.yBand(),
(x, z) -> current.getHeight(x, z, true) + current.getMinHeight());
(x, z) -> Engine.hostHeight(current, x, z, true) + current.getMinHeight());
StructureStart wrapped = NativeStructureReferenceEnvelope.wrapForPublication(
start, structure, start.getReferences(),
NativeStructureTerrainIntegrator.resolveNativeTerrain(start, decision.terrain()),
@@ -367,10 +370,14 @@ final class ModdedNativeStructureStage {
chunkPos.x(), chunkPos.z(), error);
}
IrisStaticObjectLayer staticObjects = current.getDimension().getStaticObjectLayer(current.getData());
int staticMinY = current.getMinHeight();
WorldGenLevel boundedWorld = staticObjects.isEmpty() ? world : ModdedNativeStructureWorldgenAccess.create(
Predicate<BlockPos> protectedPosition = nativeStructureProtection(current, staticObjects);
WorldGenLevel boundedWorld = staticObjects.isEmpty()
&& current.getDimensionStackContext() == null
? world
: ModdedNativeStructureWorldgenAccess.create(
world, chunkPos, worldgenSurfaceHeight(current, world.getMinY()), worldgenFloorHeight(current, world.getMinY()),
position -> staticObjects.contains(position.getX(), position.getY() - staticMinY, position.getZ()));
current.getDimensionStackContext() != null,
protectedPosition);
try {
NativeStructureVegetationClearer.clearIntersectingVegetation(
boundedWorld, chunk, area, vegetationTargets);
@@ -383,7 +390,7 @@ final class ModdedNativeStructureStage {
try {
vacuumFoundationPlan = NativeStructureSurfaceFitter.prepareSurfaceStructures(
boundedWorld, area, terrainTargets,
(x, z) -> current.getHeight(x, z, true) + current.getMinHeight());
(x, z) -> Engine.hostHeight(current, x, z, true) + current.getMinHeight());
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"terrain integration", nativeStructureBatchContext(placementGroups),
@@ -443,18 +450,50 @@ final class ModdedNativeStructureStage {
IrisNativeStructureDecision decision) {
Engine current = generator.engine();
WorldGenLevel boundedWorld = world instanceof ModdedNativeStructureWorldgenAccess ? world : ModdedNativeStructureWorldgenAccess.create(
world, chunkPos, worldgenSurfaceHeight(current, world.getMinY()), worldgenFloorHeight(current, world.getMinY()), position -> false);
world,
chunkPos,
worldgenSurfaceHeight(current, world.getMinY()),
worldgenFloorHeight(current, world.getMinY()),
current.getDimensionStackContext() != null,
nativeStructureProtection(
current,
current.getDimension().getStaticObjectLayer(current.getData())));
world.setCurrentlyGenerating(() -> "Iris native structure " + structureId);
try {
NativeStructurePostProcessor.place(
boundedWorld, structureManager, generator, random, area, chunkPos,
structureId, start, decision, this::resolvePaletteBlock,
(x, z) -> current.getHeight(x, z, true) + current.getMinHeight());
(x, z) -> Engine.hostHeight(current, x, z, true) + current.getMinHeight());
} finally {
world.setCurrentlyGenerating(null);
}
}
private static Predicate<BlockPos> nativeStructureProtection(
Engine engine,
IrisStaticObjectLayer staticObjects
) {
DimensionStackContext stackContext = engine.getDimensionStackContext();
int minimumY = engine.getMinHeight();
Map<Long, DimensionStackLayout> layouts = new ConcurrentHashMap<>();
return position -> {
if (!staticObjects.isEmpty() && staticObjects.contains(
position.getX(), position.getY() - minimumY, position.getZ())) {
return true;
}
if (stackContext == null) {
return false;
}
long columnKey = ((long) position.getX() << 32)
^ (position.getZ() & 0xFFFFFFFFL);
DimensionStackLayout layout = layouts.computeIfAbsent(
columnKey,
ignored -> stackContext.sample(position.getX(), position.getZ())
);
return layout.isHostFeatureProtectedY(position.getY() - minimumY);
};
}
private List<List<Structure>> structuresByStep(Registry<Structure> registry) {
StructureStepCache cached = structureStepCache;
if (cached != null && cached.registry() == registry) {
@@ -523,11 +562,11 @@ final class ModdedNativeStructureStage {
}
private IntBinaryOperator worldgenSurfaceHeight(Engine generationEngine, int runtimeMinY) {
return (x, z) -> generationEngine.getHeight(x, z, false) + runtimeMinY + 1;
return (x, z) -> Engine.hostHeight(generationEngine, x, z, false) + runtimeMinY + 1;
}
private IntBinaryOperator worldgenFloorHeight(Engine generationEngine, int runtimeMinY) {
return (x, z) -> generationEngine.getHeight(x, z, true) + runtimeMinY + 1;
return (x, z) -> Engine.hostHeight(generationEngine, x, z, true) + runtimeMinY + 1;
}
private record NativeStructureStartKey(String structureId, long chunkPosition) {
@@ -63,6 +63,7 @@ final class ModdedNativeStructureWorldgenAccess implements WorldGenLevel {
private final ChunkPos generationCenter;
private final IntBinaryOperator surfaceFirstFreeY;
private final IntBinaryOperator floorFirstFreeY;
private final boolean virtualizeHeights;
private final Predicate<BlockPos> protectedPosition;
private final Holder<Biome> fallbackBiome;
private final BiomeManager biomeManager;
@@ -80,6 +81,7 @@ final class ModdedNativeStructureWorldgenAccess implements WorldGenLevel {
boundary.surfaceFirstFreeY(), "Native structure world access requires a surface resolver");
this.floorFirstFreeY = Objects.requireNonNull(
boundary.floorFirstFreeY(), "Native structure world access requires an ocean-floor resolver");
this.virtualizeHeights = boundary.virtualizeHeights();
this.protectedPosition = Objects.requireNonNull(
boundary.protectedPosition(), "World access requires a protected-position predicate");
BlockPos biomeSample = generationCenter.getMiddleBlockPosition(delegate.getSeaLevel());
@@ -102,9 +104,10 @@ final class ModdedNativeStructureWorldgenAccess implements WorldGenLevel {
static ModdedNativeStructureWorldgenAccess create(WorldGenLevel delegate, ChunkPos generationCenter,
IntBinaryOperator surfaceFirstFreeY,
IntBinaryOperator floorFirstFreeY,
boolean virtualizeHeights,
Predicate<BlockPos> protectedPosition) {
return new ModdedNativeStructureWorldgenAccess(delegate, new Boundary(
generationCenter, surfaceFirstFreeY, floorFirstFreeY, protectedPosition));
generationCenter, surfaceFirstFreeY, floorFirstFreeY, virtualizeHeights, protectedPosition));
}
@Override
@@ -241,7 +244,7 @@ final class ModdedNativeStructureWorldgenAccess implements WorldGenLevel {
@Override
public int getHeight(Heightmap.Types type, int x, int z) {
if (isInsideGenerationRegion(x >> 4, z >> 4)) {
if (!virtualizeHeights && isInsideGenerationRegion(x >> 4, z >> 4)) {
return delegate.getHeight(type, x, z);
}
return height(type, x, z);
@@ -526,6 +529,7 @@ final class ModdedNativeStructureWorldgenAccess implements WorldGenLevel {
private record Boundary(ChunkPos generationCenter,
IntBinaryOperator surfaceFirstFreeY,
IntBinaryOperator floorFirstFreeY,
boolean virtualizeHeights,
Predicate<BlockPos> protectedPosition) {
}
@@ -108,12 +108,17 @@ public final class ModdedScheduler implements PlatformScheduler {
scheduler.drain();
}
public static boolean isMainThread() {
Thread running = mainThread;
return running != null && Thread.currentThread() == running;
}
@Override
public void global(Runnable task) {
if (task == null) {
return;
}
if (onMainThread()) {
if (isMainThread()) {
runGuarded(task);
return;
}
@@ -250,11 +255,6 @@ public final class ModdedScheduler implements PlatformScheduler {
}
}
private boolean onMainThread() {
Thread main = mainThread;
return main != null && Thread.currentThread() == main;
}
private void runGuarded(Runnable task) {
try {
task.run();
@@ -18,10 +18,14 @@
package art.arcane.iris.modded;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.DimensionStackContext;
import art.arcane.iris.engine.DimensionTerrainContext;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.GenerationSessionLease;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.util.project.context.IrisContext;
import net.minecraft.core.Holder;
import net.minecraft.core.Registry;
@@ -31,32 +35,24 @@ import net.minecraft.world.entity.MobCategory;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.MobSpawnSettings;
import java.util.Locale;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
/**
* Vanilla-derivative spawn table state for {@link IrisModdedChunkGenerator}. Every mutator locks the
* generator monitor because the repoint/bind/reset paths already hold it while resetting this state.
*/
final class ModdedSpawnTableMerger {
private final IrisModdedChunkGenerator generator;
private final ConcurrentHashMap<Biome, Holder<Biome>> vanillaSpawnBiomes = new ConcurrentHashMap<>();
private final ConcurrentHashMap<SpawnTableKey, WeightedList<MobSpawnSettings.SpawnerData>> mergedSpawnTables = new ConcurrentHashMap<>();
private volatile boolean vanillaSpawnBiomesInitialized;
private volatile SpawnTableState state = new SpawnTableState();
ModdedSpawnTableMerger(IrisModdedChunkGenerator generator) {
this.generator = generator;
}
void initializeVanillaSpawnBiomes(Registry<Biome> registry) {
// Volatile fast path BEFORE the monitor: this runs per mob category per spawn attempt
// on the server thread, and the generator monitor is contended by binds/hotloads.
if (vanillaSpawnBiomesInitialized) {
SpawnTableState active = state;
if (active.initialized) {
return;
}
synchronized (generator) {
if (vanillaSpawnBiomesInitialized) {
synchronized (active) {
if (active != state || active.initialized) {
return;
}
Engine current = generator.engineOrNull();
@@ -66,29 +62,70 @@ final class ModdedSpawnTableMerger {
try (GenerationSessionLease lease = generator.requireGenerationLease(current, "modded_spawn_biomes");
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
String namespace = current.getDimension().getLoadKey().toLowerCase(Locale.ROOT);
for (IrisBiome irisBiome : current.getDimension().getReachableBiomes(current)) {
if (irisBiome == null || !irisBiome.isCustom()) {
continue;
}
Holder<Biome> vanillaHolder = resolveBiomeHolder(registry, irisBiome.getVanillaDerivativeKey());
if (vanillaHolder == null) {
continue;
}
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
Holder<Biome> customHolder = resolveBiomeHolder(registry, namespace + ":" + customBiome.getId());
if (customHolder != null) {
vanillaSpawnBiomes.putIfAbsent(customHolder.value(), vanillaHolder);
IrisDimension hostDimension = current.getDimension();
addVanillaSpawnBiomes(
registry,
hostDimension,
current.getData(),
hostDimension.getReachableBiomes(current),
active
);
DimensionStackContext stackContext = current.getDimensionStackContext();
if (stackContext != null) {
for (DimensionTerrainContext terrainContext : stackContext.getLayersBottomToTop()) {
if (terrainContext.isSelfReferencing()) {
continue;
}
IrisDimension dimension = terrainContext.getDimension();
addVanillaSpawnBiomes(
registry,
dimension,
terrainContext.getData(),
dimension.getReachableBiomes(terrainContext),
active
);
}
}
vanillaSpawnBiomesInitialized = true;
active.initialized = true;
}
}
}
private void addVanillaSpawnBiomes(
Registry<Biome> registry,
IrisDimension dimension,
IrisData data,
Iterable<IrisBiome> biomes,
SpawnTableState state
) {
for (IrisBiome irisBiome : biomes) {
if (irisBiome == null || !irisBiome.isCustom()) {
continue;
}
Holder<Biome> vanillaHolder = resolveBiomeHolder(
registry, irisBiome.getVanillaDerivativeKey());
if (vanillaHolder == null) {
continue;
}
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
Holder<Biome> customHolder = resolveBiomeHolder(
registry,
ModdedWorldgenIds.biomeRef(
data.getDataFolder().getName(),
dimension.getLoadKey(),
customBiome.getId()
)
);
if (customHolder == null) {
continue;
}
state.vanillaSpawnBiomes.putIfAbsent(customHolder.value(), vanillaHolder);
}
}
}
Holder<Biome> vanillaSpawnBiome(Biome biome) {
return vanillaSpawnBiomes.get(biome);
return state.vanillaSpawnBiomes.get(biome);
}
WeightedList<MobSpawnSettings.SpawnerData> mergedSpawnTable(
@@ -96,7 +133,8 @@ final class ModdedSpawnTableMerger {
WeightedList<MobSpawnSettings.SpawnerData> vanillaSpawns,
WeightedList<MobSpawnSettings.SpawnerData> explicitSpawns) {
SpawnTableKey key = new SpawnTableKey(biome, category);
return mergedSpawnTables.computeIfAbsent(key, ignored -> NativeSpawnTableMerger.merge(vanillaSpawns, explicitSpawns));
return state.mergedSpawnTables.computeIfAbsent(
key, ignored -> NativeSpawnTableMerger.merge(vanillaSpawns, explicitSpawns));
}
private Holder<Biome> resolveBiomeHolder(Registry<Biome> registry, String key) {
@@ -112,13 +150,16 @@ final class ModdedSpawnTableMerger {
}
void resetVanillaSpawnBiomes() {
synchronized (generator) {
vanillaSpawnBiomes.clear();
mergedSpawnTables.clear();
vanillaSpawnBiomesInitialized = false;
}
state = new SpawnTableState();
}
private record SpawnTableKey(Biome biome, MobCategory category) {
}
private static final class SpawnTableState {
private final ConcurrentHashMap<Biome, Holder<Biome>> vanillaSpawnBiomes = new ConcurrentHashMap<>();
private final ConcurrentHashMap<SpawnTableKey, WeightedList<MobSpawnSettings.SpawnerData>> mergedSpawnTables =
new ConcurrentHashMap<>();
private volatile boolean initialized;
}
}
@@ -84,7 +84,8 @@ final class ModdedEditCommands {
}
BlockPos pos = player.blockPosition();
try {
region = engine.getRegion(pos.getX(), pos.getZ());
region = engine.getRegion(
pos.getX(), pos.getY() - engine.getMinHeight(), pos.getZ());
} catch (Throwable e) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_REGION_LOOKUP_FAILED, MessageArgument.untrusted("value", e.getClass().getSimpleName())));
return 0;
@@ -73,6 +73,8 @@ import org.zeroturnaround.zip.ZipUtil;
import java.awt.Desktop;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
@@ -85,6 +87,7 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.ModdedCommandMessages;
@@ -891,21 +894,30 @@ public final class ModdedStudioCommands {
LinkedHashSet<String> objectKeys = new LinkedHashSet<>();
LinkedHashSet<String> markerKeys = new LinkedHashSet<>();
LinkedHashSet<String> structureKeys = new LinkedHashSet<>();
List<IrisDimension> dimensions = new ArrayList<>();
dimension.getAllRegions(() -> dm).forEach((IrisRegion region) -> {
if (region != null && region.getLoadKey() != null) {
regionKeys.add(region.getLoadKey());
for (String dimensionKey : PackExportClosure.collectDimensionKeys(dimension).stream().sorted().toList()) {
IrisDimension exportedDimension = dm.getDimensionLoader().load(dimensionKey);
if (exportedDimension != null) {
dimensions.add(exportedDimension);
}
});
dimension.getReachableBiomes(() -> dm).forEach((IrisBiome biome) -> {
if (biome != null && biome.getLoadKey() != null) {
biomeKeys.add(biome.getLoadKey());
}
});
lootKeys.addAll(dimension.getLoot().getTables());
spawnerKeys.addAll(dimension.getEntitySpawners());
collectStructureKeys(structureKeys, dimension.getStructures());
objectKeys.addAll(PackExportClosure.collectStaticObjectKeys(dimension.getStaticObjects()));
}
for (IrisDimension exportedDimension : dimensions) {
exportedDimension.getAllRegions(() -> dm).forEach((IrisRegion region) -> {
if (region != null && region.getLoadKey() != null) {
regionKeys.add(region.getLoadKey());
}
});
exportedDimension.getReachableBiomes(() -> dm).forEach((IrisBiome biome) -> {
if (biome != null && biome.getLoadKey() != null) {
biomeKeys.add(biome.getLoadKey());
}
});
lootKeys.addAll(exportedDimension.getLoot().getTables());
spawnerKeys.addAll(exportedDimension.getEntitySpawners());
collectStructureKeys(structureKeys, exportedDimension.getStructures());
objectKeys.addAll(PackExportClosure.collectStaticObjectKeys(exportedDimension.getStaticObjects()));
}
for (String regionKey : regionKeys) {
IrisRegion region = dm.getRegionLoader().load(regionKey);
@@ -975,10 +987,17 @@ public final class ModdedStudioCommands {
}
}
hashes.append(copyJson(folder, "dimensions", dimension.getLoadKey(), dm.getDimensionLoader().findFile(dimension.getLoadKey())));
for (IrisDimension exportedDimension : dimensions) {
String key = exportedDimension.getLoadKey();
hashes.append(copyJson(folder, "dimensions", key, dm.getDimensionLoader().findFile(key)));
}
for (String key : generatorKeys) {
hashes.append(copyJson(folder, "generators", key, dm.getGeneratorLoader().findFile(key)));
}
for (String key : Stream.of(dm.getExpressionLoader().getPossibleKeys()).sorted().toList()) {
hashes.append(copyJson(folder, "expressions", key, dm.getExpressionLoader().findFile(key)));
}
hashes.append(copyStyleSnippets(packFolder, folder));
for (String key : regionKeys) {
hashes.append(copyJson(folder, "regions", key, dm.getRegionLoader().findFile(key)));
}
@@ -1041,6 +1060,25 @@ public final class ModdedStudioCommands {
}
}
private static String copyStyleSnippets(File packFolder, File folder) throws IOException {
Path sourceRoot = new File(packFolder, "snippet/style").toPath();
if (!Files.isDirectory(sourceRoot)) {
return "";
}
StringBuilder hashes = new StringBuilder();
try (Stream<Path> files = Files.walk(sourceRoot)) {
for (Path source : files.filter(Files::isRegularFile)
.filter(path -> path.getFileName().toString().endsWith(".json"))
.sorted()
.toList()) {
String relative = sourceRoot.relativize(source).toString().replace(File.separatorChar, '/');
String key = relative.substring(0, relative.length() - ".json".length());
hashes.append(copyJson(folder, "snippet/style", key, source.toFile()));
}
}
return hashes.toString();
}
private static int regions(CommandSourceStack source, int radius) {
ServerPlayer player = source.getPlayer();
if (player == null) {
@@ -216,7 +216,8 @@ public final class ModdedWhatCommands {
int centerX = (pos.getX() & ~15) + 8;
int centerZ = (pos.getZ() & ~15) + 8;
try {
IrisRegion region = engine.getRegion(centerX, centerZ);
IrisRegion region = engine.getRegion(
centerX, pos.getY() - engine.getMinHeight(), centerZ);
IrisModdedCommands.ok(source, IrisLanguage.plain(
RuntimeUiMessages.WHAT_IRIS_REGION,
MessageArgument.untrusted("region", region.getLoadKey()),
@@ -32,6 +32,7 @@ import art.arcane.iris.engine.object.IrisWorldBoundary;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.modded.ModdedDimensionManager;
import art.arcane.iris.modded.ModdedForcedDatapack;
import art.arcane.iris.modded.ModdedScheduler;
import art.arcane.iris.modded.ModdedWorkspaceGenerator;
import art.arcane.iris.nativegen.NativeStructureVolumeIndex;
import art.arcane.iris.modded.command.ModdedPregenJob;
@@ -100,6 +101,11 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
return NativeStructureVolumeIndex.volumes(engine, minX, minZ, maxX, maxZ);
}
@Override
public boolean isMainThread() {
return ModdedScheduler.isMainThread();
}
@Override
public void refreshDatapackWorkspace(Engine engine) {
IrisData data = engine.getData();
@@ -121,6 +127,20 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
engine.getDimension(),
replacement,
"irisworldgen");
IrisModdedChunkGenerator.requireStructureBiomeUniverseCompatible(
engine.getDimension(), replacement);
}
@Override
public void prepareRuntimeHotload(Engine engine) {
IrisWorld world = engine.getWorld();
if (world == null || !world.hasPlatformWorld()
|| !(world.platformWorld().nativeHandle() instanceof ServerLevel level)
|| !(level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator generator)) {
NativeStructureVolumeIndex.invalidate(engine);
return;
}
generator.prepareRuntimeHotload(level, engine);
}
@Override
@@ -285,7 +305,6 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
long start = System.currentTimeMillis();
try {
engine.hotloadSilently();
generator.onHotload();
ModdedIrisLog.info("Iris studio hotload {} pack={} {}ms", dimensionId, engine.getDimension().getLoadKey(), System.currentTimeMillis() - start);
} catch (Throwable e) {
ModdedIrisLog.error("Iris studio hotload failed for {}", dimensionId, e);