mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-11-01 17:00:26 +00:00
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:
1 parent
366033a535
commit
6549b40797
119 files changed
+6874
-743
No files matched your search
+491
-49
@@ -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) {
|
||||
|
||||
+67
-11
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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);
|
||||
}
|
||||
|
||||
+1
-2
@@ -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);
|
||||
|
||||
+1
-1
@@ -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) {
|
||||
|
||||
+5
-11
@@ -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 '"
|
||||
|
||||
+109
-10
@@ -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<>();
|
||||
|
||||
+48
-9
@@ -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) {
|
||||
|
||||
+6
-2
@@ -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();
|
||||
|
||||
+75
-34
@@ -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;
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -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;
|
||||
|
||||
+52
-14
@@ -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) {
|
||||
|
||||
+2
-1
@@ -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()),
|
||||
|
||||
+20
-1
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user