This commit is contained in:
Brian Neumann-Fopiano
2026-07-13 21:33:20 -04:00
parent 4dea984d77
commit 45230c0689
234 changed files with 13818 additions and 2979 deletions
@@ -1,14 +1,17 @@
package art.arcane.iris.core.nms.v26_2_R1;
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import com.mojang.serialization.MapCodec;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.engine.data.cache.AtomicCache;
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.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.math.RNG;
import net.minecraft.core.Holder;
import net.minecraft.core.QuartPos;
import net.minecraft.core.Registry;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.Registries;
@@ -26,12 +29,14 @@ import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;
public class CustomBiomeSource extends BiomeSource {
@@ -42,11 +47,13 @@ public class CustomBiomeSource extends BiomeSource {
private final Registry<Biome> biomeCustomRegistry;
private final Registry<Biome> biomeRegistry;
private final AtomicCache<RegistryAccess> registryAccess = new AtomicCache<>();
private final KMap<String, Holder<Biome>> customBiomes;
private final Map<Biome, Holder<Biome>> vanillaSpawnBiomes;
private final Holder<Biome> fallbackBiome;
private final ConcurrentHashMap<Long, Holder<Biome>> noiseBiomeCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Long, Holder<Biome>> structureBiomeCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Long, Holder<Biome>> surfaceStructureBiomeCache = new ConcurrentHashMap<>();
private volatile KMap<String, Holder<Biome>> customBiomes;
private volatile Map<Biome, Holder<Biome>> vanillaSpawnBiomes;
private volatile IrisDimension cacheDimension;
public CustomBiomeSource(long seed, Engine engine, World world) {
this.engine = engine;
@@ -56,20 +63,19 @@ public class CustomBiomeSource extends BiomeSource {
this.fallbackBiome = resolveFallbackBiome(this.biomeRegistry, this.biomeCustomRegistry);
this.customBiomes = fillCustomBiomes(this.biomeCustomRegistry, engine, this.fallbackBiome);
this.vanillaSpawnBiomes = fillVanillaSpawnBiomes(this.biomeCustomRegistry, this.biomeRegistry, engine);
this.cacheDimension = engine.getDimension();
}
private static List<Holder<Biome>> getAllBiomes(Registry<Biome> customRegistry, Registry<Biome> registry, Engine engine, Holder<Biome> fallback) {
LinkedHashSet<Holder<Biome>> biomes = new LinkedHashSet<>();
if (fallback != null) {
biomes.add(fallback);
}
boolean resolutionFailed = false;
for (IrisBiome i : engine.getAllBiomes()) {
Holder<Biome> vanillaHolder = NMSBinding.biomeToBiomeBase(registry, i.getVanillaDerivative());
if (vanillaHolder != null) {
biomes.add(vanillaHolder);
} else if (!i.isCustom() && fallback != null) {
biomes.add(fallback);
} else if (!i.isCustom()) {
resolutionFailed = true;
}
if (i.isCustom()) {
@@ -77,13 +83,17 @@ public class CustomBiomeSource extends BiomeSource {
Holder<Biome> customHolder = resolveCustomBiomeHolder(customRegistry, engine, j.getId());
if (customHolder != null) {
biomes.add(customHolder);
} else if (fallback != null) {
biomes.add(fallback);
} else {
resolutionFailed = true;
}
}
}
}
if ((resolutionFailed || biomes.isEmpty()) && fallback != null) {
biomes.add(fallback);
}
return new ArrayList<>(biomes);
}
@@ -135,12 +145,21 @@ public class CustomBiomeSource extends BiomeSource {
@Override
protected Stream<Holder<Biome>> collectPossibleBiomes() {
return getAllBiomes(
((RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer()))
.lookup(Registries.BIOME).orElse(null),
((CraftWorld) engine.getWorld().realWorld()).getHandle().registryAccess().lookup(Registries.BIOME).orElse(null),
engine,
fallbackBiome).stream();
return possibleStructureBiomes().stream();
}
Set<Holder<Biome>> possibleStructureBiomes() {
ensureCachesCurrent();
World world = BukkitWorldBinding.world(engine.getWorld());
if (world == null) {
throw new IllegalStateException("Iris biome source has no bound Bukkit world");
}
Registry<Biome> customRegistry = ((RegistryAccess) getFor(
RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer()))
.lookup(Registries.BIOME).orElse(null);
Registry<Biome> worldRegistry = ((CraftWorld) world).getHandle().registryAccess()
.lookup(Registries.BIOME).orElse(null);
return Set.copyOf(getAllBiomes(customRegistry, worldRegistry, engine, fallbackBiome));
}
private KMap<String, Holder<Biome>> fillCustomBiomes(Registry<Biome> customRegistry, Engine engine, Holder<Biome> fallback) {
@@ -194,6 +213,7 @@ public class CustomBiomeSource extends BiomeSource {
}
Holder<Biome> getVanillaSpawnBiome(Holder<Biome> biome) {
ensureCachesCurrent();
if (biome == null) {
return null;
}
@@ -211,6 +231,11 @@ public class CustomBiomeSource extends BiomeSource {
@Override
public Holder<Biome> getNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
ensureCachesCurrent();
if (isGuaranteedSurfaceBiome(y)) {
return getSurfaceStructureBiomeHolder(x, z);
}
long cacheKey = packNoiseKey(x, y, z);
Holder<Biome> cachedHolder = structureBiomeCache.get(cacheKey);
if (cachedHolder != null) {
@@ -230,7 +255,69 @@ public class CustomBiomeSource extends BiomeSource {
return resolvedHolder;
}
@Override
public Set<Holder<Biome>> getBiomesWithin(int x, int y, int z, int radius, Climate.Sampler sampler) {
ensureCachesCurrent();
int minQuartY = QuartPos.fromBlock(y - radius);
boolean monumentQuery = radius == 29
&& y == engine.getMinHeight() + engine.getDimension().getFluidHeight();
if (!monumentQuery && !isGuaranteedSurfaceBiome(minQuartY)) {
return super.getBiomesWithin(x, y, z, radius, sampler);
}
int minQuartX = QuartPos.fromBlock(x - radius);
int maxQuartX = QuartPos.fromBlock(x + radius);
int minQuartZ = QuartPos.fromBlock(z - radius);
int maxQuartZ = QuartPos.fromBlock(z + radius);
int columns = (maxQuartX - minQuartX + 1) * (maxQuartZ - minQuartZ + 1);
Set<Holder<Biome>> biomes = new HashSet<>(columns);
for (int quartZ = minQuartZ; quartZ <= maxQuartZ; quartZ++) {
for (int quartX = minQuartX; quartX <= maxQuartX; quartX++) {
biomes.add(getSurfaceStructureBiomeHolder(quartX, quartZ));
}
}
return biomes;
}
private Holder<Biome> getSurfaceStructureBiomeHolder(int x, int z) {
long columnKey = packColumnKey(x, z);
Holder<Biome> surfaceHolder = surfaceStructureBiomeCache.get(columnKey);
if (surfaceHolder != null) {
return surfaceHolder;
}
Holder<Biome> resolvedSurfaceHolder = resolveSurfaceStructureBiomeHolder(x, z);
Holder<Biome> existingSurfaceHolder = surfaceStructureBiomeCache.putIfAbsent(columnKey, resolvedSurfaceHolder);
if (existingSurfaceHolder != null) {
return existingSurfaceHolder;
}
if (surfaceStructureBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
surfaceStructureBiomeCache.clear();
}
return resolvedSurfaceHolder;
}
private boolean isGuaranteedSurfaceBiome(int quartY) {
if (engine == null || engine.isClosed() || engine.getComplex() == null) {
return false;
}
int worldMinHeight = engine.getWorld().minHeight();
int internalY = (quartY << 2) - worldMinHeight;
int caveSwitchInternalY = Math.max(-8 - worldMinHeight, 40);
return internalY > caveSwitchInternalY;
}
private Holder<Biome> resolveSurfaceStructureBiomeHolder(int x, int z) {
int blockX = x << 2;
int blockZ = z << 2;
IrisBiome irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
if (irisBiome == null) {
return getFallbackBiome();
}
Holder<Biome> holder = NMSBinding.biomeToBiomeBase(biomeRegistry, irisBiome.getVanillaDerivative());
return holder == null ? getFallbackBiome() : holder;
}
public Holder<Biome> getVisibleNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
ensureCachesCurrent();
long cacheKey = packNoiseKey(x, y, z);
Holder<Biome> cachedHolder = noiseBiomeCache.get(cacheKey);
if (cachedHolder != null) {
@@ -250,6 +337,28 @@ public class CustomBiomeSource extends BiomeSource {
return resolvedHolder;
}
private void ensureCachesCurrent() {
IrisDimension dimension = engine.getDimension();
if (cacheDimension == dimension) {
return;
}
synchronized (this) {
if (cacheDimension == dimension) {
return;
}
KMap<String, Holder<Biome>> refreshedCustomBiomes = fillCustomBiomes(
biomeCustomRegistry, engine, fallbackBiome);
Map<Biome, Holder<Biome>> refreshedSpawnBiomes = fillVanillaSpawnBiomes(
biomeCustomRegistry, biomeRegistry, engine);
noiseBiomeCache.clear();
structureBiomeCache.clear();
surfaceStructureBiomeCache.clear();
customBiomes = refreshedCustomBiomes;
vanillaSpawnBiomes = refreshedSpawnBiomes;
cacheDimension = dimension;
}
}
private Holder<Biome> resolveStructureBiomeHolder(int x, int y, int z) {
BiomeResolution resolution = resolveBiomeResolution(x, y, z);
if (resolution == null) {
@@ -315,14 +424,19 @@ public class CustomBiomeSource extends BiomeSource {
int blockY = y << 2;
int worldMinHeight = engine.getWorld().minHeight();
int internalY = blockY - worldMinHeight;
int surfaceInternalY = engine.getComplex().getHeightStream().get(blockX, blockZ).intValue();
int caveSwitchInternalY = Math.max(-8 - worldMinHeight, 40);
boolean deepUnderground = internalY <= caveSwitchInternalY;
boolean belowSurface = internalY <= surfaceInternalY - 8;
boolean underground = deepUnderground && belowSurface;
IrisBiome irisBiome = underground
? engine.getCaveBiome(blockX, internalY, blockZ)
: engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
boolean underground = false;
IrisBiome irisBiome;
if (deepUnderground) {
int surfaceInternalY = engine.getComplex().getHeightStream().get(blockX, blockZ).intValue();
underground = internalY <= surfaceInternalY - 8;
irisBiome = underground
? engine.getCaveBiome(blockX, internalY, blockZ)
: engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
} else {
irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
}
if (irisBiome == null && underground) {
irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
}
@@ -357,6 +471,10 @@ public class CustomBiomeSource extends BiomeSource {
| ((long) y & 4095L);
}
private static long packColumnKey(int x, int z) {
return ((long) x << 32) ^ ((long) z & 4294967295L);
}
private static Holder<Biome> resolveCustomBiomeHolder(Registry<Biome> customRegistry, Engine engine, String customBiomeId) {
if (customRegistry == null || engine == null || customBiomeId == null || customBiomeId.isBlank()) {
return null;
@@ -1,5 +1,20 @@
package art.arcane.iris.core.nms.v26_2_R1;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisImportedStructureControl;
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
import art.arcane.iris.engine.object.IrisVanillaStructureStiltSettings;
import art.arcane.iris.nativegen.NativeStructurePostProcessor;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.IrisCustomData;
import art.arcane.iris.util.common.reflect.WrappedField;
import art.arcane.iris.util.common.reflect.WrappedReturningMethod;
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.Holder;
import net.minecraft.core.HolderLookup;
@@ -7,6 +22,13 @@ import net.minecraft.core.HolderSet;
import net.minecraft.core.Registry;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.SectionPos;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.WorldGenRegion;
import net.minecraft.util.random.Weighted;
import net.minecraft.util.random.WeightedList;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.LevelHeightAccessor;
@@ -23,26 +45,6 @@ import net.minecraft.world.level.levelgen.RandomState;
import net.minecraft.world.level.levelgen.RandomSupport;
import net.minecraft.world.level.levelgen.WorldgenRandom;
import net.minecraft.world.level.levelgen.XoroshiroRandomSource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import com.mojang.datafixers.util.Pair;
import com.mojang.serialization.MapCodec;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisImportedStructureControl;
import art.arcane.iris.util.common.reflect.WrappedField;
import art.arcane.iris.util.common.reflect.WrappedReturningMethod;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.WorldGenRegion;
import net.minecraft.util.random.Weighted;
import net.minecraft.util.random.WeightedList;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.MobCategory;
import net.minecraft.world.level.block.Blocks;
@@ -52,35 +54,44 @@ import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.levelgen.blending.Blender;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureSet;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import net.minecraft.core.registries.Registries;
import java.util.stream.Collectors;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import org.bukkit.World;
import org.bukkit.craftbukkit.CraftWorld;
import org.bukkit.craftbukkit.block.data.CraftBlockData;
import org.bukkit.craftbukkit.generator.CustomChunkGenerator;
import org.bukkit.block.data.BlockData;
import org.spigotmc.SpigotWorldConfig;
import javax.annotation.Nullable;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
public class IrisChunkGenerator extends CustomChunkGenerator {
private static final String NATIVE_MONUMENT_KEY = "minecraft:monument";
private static final WrappedField<ChunkGenerator, BiomeSource> BIOME_SOURCE;
private static final WrappedReturningMethod<Heightmap, Object> SET_HEIGHT;
private final ChunkGenerator delegate;
private final Engine engine;
private final CustomBiomeSource customBiomeSource;
private final int runtimeMinY;
private final int runtimeHeight;
private final ConcurrentHashMap<SpawnTableKey, WeightedList<MobSpawnSettings.SpawnerData>> mergedSpawnTables = new ConcurrentHashMap<>();
private volatile Set<String> reachableStructureKeysCache;
private volatile ReachableStructureCache reachableStructureCache;
private volatile StructureStepCache structureStepCache;
public IrisChunkGenerator(ChunkGenerator delegate, long seed, Engine engine, World world) {
this(delegate, engine, world, new CustomBiomeSource(seed, engine, world));
@@ -91,6 +102,9 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
this.delegate = delegate;
this.engine = engine;
this.customBiomeSource = customBiomeSource;
ServerLevel level = ((CraftWorld) world).getHandle();
this.runtimeMinY = level.getMinY();
this.runtimeHeight = level.getHeight();
}
@Override
@@ -105,16 +119,20 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
if (id == null) {
continue;
}
int[] at = IrisStructureLocator.locate(engine, id.toString(), pos.getX(), pos.getZ(), Math.max(1, radius));
if (at == null) {
IrisStructureLocator.LocateResult result = IrisStructureLocator.locate(
engine, id.toString(), pos.getX(), pos.getZ(), Math.max(1, radius));
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
continue;
}
long dx = (long) at[0] - pos.getX();
long dz = (long) at[2] - pos.getZ();
if (!result.found()) {
continue;
}
long dx = (long) result.originX() - pos.getX();
long dz = (long) result.originZ() - pos.getZ();
long d = dx * dx + dz * dz;
if (d < bestDist) {
bestDist = d;
best = new BlockPos(at[0], at[1], at[2]);
best = new BlockPos(result.originX(), result.baseY(), result.originZ());
bestHolder = holder;
}
}
@@ -122,7 +140,8 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
return Pair.of(best, bestHolder);
}
} catch (Throwable e) {
IrisLogging.reportError(e);
IrisLogging.reportError("Iris-placed structure lookup failed near "
+ pos.getX() + ", " + pos.getZ() + ".", e);
}
if (!importedControl().active()) {
return null;
@@ -134,28 +153,30 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
try {
return delegate.findNearestMapStructure(level, reachable, pos, radius, findUnexplored);
} catch (Throwable e) {
IrisLogging.error("Vanilla structure locate failed near " + pos.getX() + ", " + pos.getZ() + ": " + e);
IrisLogging.reportError(e);
IrisLogging.reportError("Vanilla structure locate failed near "
+ pos.getX() + ", " + pos.getZ() + ".", e);
return null;
}
}
private HolderSet<Structure> filterReachableStructures(ServerLevel level, HolderSet<Structure> holders) {
Set<String> reachable = reachableStructureKeysCache;
if (reachable == null) {
reachable = VanillaStructureBiomes.reachableStructureKeys(level, delegate.getBiomeSource());
reachableStructureKeysCache = reachable;
}
if (reachable.isEmpty()) {
return holders;
}
Set<String> reachable = reachableStructureKeys(level);
IrisImportedStructureControl control = importedControl();
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
List<Holder<Structure>> kept = new ArrayList<>();
List<Holder<Structure>> kept = new ArrayList<>(holders.size());
for (Holder<Structure> holder : holders) {
Object id = registry.getKey(holder.value());
if (id != null && reachable.contains(id.toString())) {
kept.add(holder);
if (id == null) {
continue;
}
String key = id.toString();
IrisNativeStructureDecision decision = control.resolve(
key, NativeStructurePostProcessor.isUndergroundStep(holder.value().step()));
if (NATIVE_MONUMENT_KEY.equals(key) || !decision.generate()
|| IrisStructureLocator.suppressesVanilla(engine, key) || !reachable.contains(key)) {
continue;
}
kept.add(holder);
}
if (kept.size() == holders.size()) {
return holders;
@@ -163,6 +184,30 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
return HolderSet.direct(kept);
}
private Set<String> reachableStructureKeys(ServerLevel level) {
IrisDimension dimension = engine.getDimension();
ReachableStructureCache cached = reachableStructureCache;
if (cached != null && cached.dimension() == dimension) {
return cached.keys();
}
synchronized (this) {
cached = reachableStructureCache;
if (cached != null && cached.dimension() == dimension) {
return cached.keys();
}
Set<String> reachable;
try {
reachable = Set.copyOf(VanillaStructureBiomes.reachableStructureKeys(level, customBiomeSource));
} catch (Throwable error) {
IrisLogging.reportError("Iris could not resolve native structure biome reachability; "
+ "native locate is disabled until the next hotload.", error);
reachable = Set.of();
}
reachableStructureCache = new ReachableStructureCache(dimension, reachable);
return reachable;
}
}
@Override
protected MapCodec<? extends ChunkGenerator> codec() {
return MapCodec.unit(null);
@@ -177,12 +222,12 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
@Override
public int getMinY() {
return delegate.getMinY();
return runtimeMinY;
}
@Override
public int getSeaLevel() {
return delegate.getSeaLevel();
return runtimeMinY + engine.getDimension().getFluidHeight();
}
@Override
@@ -190,7 +235,42 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
if (!importedControl().active()) {
return;
}
Map<Structure, StructureStart> previousStarts = new HashMap<>(access.getAllStarts());
super.createStructures(registryAccess, structureState, structureManager, access, templateManager, levelKey);
adjustGeneratedStructures(registryAccess, access, previousStarts);
}
private void adjustGeneratedStructures(RegistryAccess registryAccess, ChunkAccess access, Map<Structure, StructureStart> previousStarts) {
Registry<Structure> registry = registryAccess.lookupOrThrow(Registries.STRUCTURE);
IrisImportedStructureControl control = importedControl();
for (Map.Entry<Structure, StructureStart> entry : access.getAllStarts().entrySet()) {
Structure structure = entry.getKey();
StructureStart start = entry.getValue();
if (!start.isValid() || previousStarts.get(structure) == start) {
continue;
}
Identifier id = registry.getKey(structure);
String structureId = id == null ? null : id.toString();
IrisNativeStructureDecision decision = control.resolve(
structureId, NativeStructurePostProcessor.isUndergroundStep(structure.step()));
if (!decision.generate() || IrisStructureLocator.suppressesVanilla(engine, structureId)) {
access.setStartForStructure(structure, StructureStart.INVALID_START);
continue;
}
try {
NativeStructurePostProcessor.applyVerticalShift(
start,
decision.yShift(),
access.getMinY(),
access.getMinY() + access.getHeight());
} catch (RuntimeException error) {
access.setStartForStructure(structure, StructureStart.INVALID_START);
IrisLogging.reportError("Iris rejected native structure " + structureId + " in chunk "
+ access.getPos().x() + "," + access.getPos().z()
+ " because its vertical bounds are invalid.", error);
continue;
}
}
}
private IrisImportedStructureControl importedControl() {
@@ -281,68 +361,119 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
SectionPos sectionPos = SectionPos.of(chunkPos, world.getMinSectionY());
BlockPos origin = sectionPos.origin();
Registry<Structure> registry = world.registryAccess().lookupOrThrow(Registries.STRUCTURE);
Map<Integer, List<Structure>> byStep = registry.stream().collect(Collectors.groupingBy(s -> s.step().ordinal()));
List<List<Structure>> byStep = structuresByStep(registry);
WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed()));
long decoSeed = random.setDecorationSeed(world.getSeed(), origin.getX(), origin.getZ());
BoundingBox area = writableArea(chunk);
int steps = GenerationStep.Decoration.values().length;
IrisImportedStructureControl control = importedControl();
List<NativePlacementGroup> placementGroups = new ArrayList<>();
List<NativeStructurePostProcessor.VegetationTarget> vegetationTargets = new ArrayList<>();
for (int step = 0; step < steps; step++) {
int index = 0;
for (Structure structure : byStep.getOrDefault(step, List.of())) {
for (Structure structure : byStep.get(step)) {
Object id = registry.getKey(structure);
String structureId = id == null ? null : id.toString();
if (control.shouldGenerate(structureId) && !IrisStructureLocator.suppressesVanilla(engine, structureId)) {
random.setFeatureSeed(decoSeed, index, step);
int[] offset = control.resolveOffset(structureId, isUndergroundStep(structure.step()));
boolean shifted = offset[0] != 0 || offset[1] != 0 || offset[2] != 0;
WorldGenLevel target = shifted ? shiftedLevel(world, offset[0], offset[1], offset[2]) : world;
BoundingBox placeArea = shifted
? new BoundingBox(area.minX() - offset[0], area.minY(), area.minZ() - offset[2], area.maxX() - offset[0], area.maxY(), area.maxZ() - offset[2])
: area;
IrisNativeStructureDecision decision = control.resolve(
structureId, NativeStructurePostProcessor.isUndergroundStep(structure.step()));
if (decision.generate() && !IrisStructureLocator.suppressesVanilla(engine, structureId)) {
try {
structureManager.startsForStructure(sectionPos, structure)
.forEach(start -> start.placeInChunk(target, structureManager, this, random, placeArea, chunkPos));
List<StructureStart> starts = structureManager.startsForStructure(sectionPos, structure);
if (!starts.isEmpty()) {
List<StructureStart> resolvedStarts = List.copyOf(starts);
placementGroups.add(new NativePlacementGroup(
structureId, decision, index, step, resolvedStarts));
for (StructureStart start : resolvedStarts) {
vegetationTargets.add(new NativeStructurePostProcessor.VegetationTarget(
start, decision.clearVegetation()));
}
}
} catch (Throwable e) {
IrisLogging.reportError(e);
IrisLogging.reportError("Iris failed to resolve native structure " + structureId
+ " in chunk " + chunkPos.x() + "," + chunkPos.z() + ".", e);
}
}
index++;
}
}
try {
NativeStructurePostProcessor.clearIntersectingVegetation(
world, chunk, area, vegetationTargets);
} catch (Throwable e) {
IrisLogging.reportError("Iris failed to clear vegetation from native structures in chunk "
+ chunkPos.x() + "," + chunkPos.z() + ".", e);
}
for (NativePlacementGroup group : placementGroups) {
random.setFeatureSeed(decoSeed, group.featureIndex(), group.step());
try {
for (StructureStart start : group.starts()) {
placeVanillaStructure(world, structureManager, random, area, chunkPos,
group.structureId(), start, group.decision());
}
} catch (Throwable e) {
IrisLogging.reportError("Iris failed to place native structure " + group.structureId()
+ " in chunk " + chunkPos.x() + "," + chunkPos.z() + ".", e);
}
}
}
private static boolean isUndergroundStep(GenerationStep.Decoration step) {
return step == GenerationStep.Decoration.UNDERGROUND_STRUCTURES
|| step == GenerationStep.Decoration.STRONGHOLDS;
private void placeVanillaStructure(WorldGenLevel world, StructureManager structureManager, WorldgenRandom random,
BoundingBox area, ChunkPos chunkPos, String structureId, StructureStart start,
IrisNativeStructureDecision decision) {
NativeStructurePostProcessor.place(world, structureManager, this, random, area, chunkPos,
structureId, start, decision, this::resolveStiltBlock);
}
private WorldGenLevel shiftedLevel(WorldGenLevel world, int dx, int dy, int dz) {
return (WorldGenLevel) Proxy.newProxyInstance(
WorldGenLevel.class.getClassLoader(),
new Class<?>[]{WorldGenLevel.class},
(proxy, method, args) -> {
if (args != null) {
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof BlockPos bp) {
args[i] = new BlockPos(bp.getX() + dx, bp.getY() + dy, bp.getZ() + dz);
}
}
}
try {
return method.invoke(world, args);
} catch (InvocationTargetException e) {
throw e.getCause();
}
});
private List<List<Structure>> structuresByStep(Registry<Structure> registry) {
StructureStepCache cached = structureStepCache;
if (cached != null && cached.registry() == registry) {
return cached.structures();
}
synchronized (this) {
cached = structureStepCache;
if (cached != null && cached.registry() == registry) {
return cached.structures();
}
int steps = GenerationStep.Decoration.values().length;
List<List<Structure>> grouped = new ArrayList<>(steps);
for (int step = 0; step < steps; step++) {
grouped.add(new ArrayList<>());
}
for (Structure structure : registry) {
grouped.get(structure.step().ordinal()).add(structure);
}
for (int step = 0; step < steps; step++) {
grouped.set(step, List.copyOf(grouped.get(step)));
}
List<List<Structure>> resolved = List.copyOf(grouped);
structureStepCache = new StructureStepCache(registry, resolved);
return resolved;
}
}
private BlockState resolveStiltBlock(IrisVanillaStructureStiltSettings settings, RNG rng, int x, int y, int z) {
if (settings.getPalette() == null) {
return Blocks.COBBLESTONE.defaultBlockState();
}
PlatformBlockState platformState = settings.getPalette().get(rng, x, y, z, engine.getData());
if (platformState == null || !(platformState.nativeHandle() instanceof BlockData blockData)) {
return Blocks.COBBLESTONE.defaultBlockState();
}
if (blockData instanceof IrisCustomData customData) {
blockData = customData.getBase();
}
if (blockData instanceof CraftBlockData craftBlockData) {
return craftBlockData.getState();
}
return Blocks.COBBLESTONE.defaultBlockState();
}
private BoundingBox writableArea(ChunkAccess chunk) {
ChunkPos cp = chunk.getPos();
int i = cp.getMinBlockX();
int j = cp.getMinBlockZ();
int minY = getMinY() + 1;
int maxY = getMinY() + engine.getHeight() - 1;
int minY = chunk.getMinY();
int maxY = minY + chunk.getHeight() - 1;
return new BoundingBox(i, minY, j, i + 15, maxY, j + 15);
}
@@ -403,7 +534,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
@Override
public int getGenDepth() {
return delegate.getGenDepth();
return runtimeHeight;
}
@Override
@@ -447,7 +578,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
Method setHeight = null;
for (Method method : Heightmap.class.getDeclaredMethods()) {
var types = method.getParameterTypes();
Class<?>[] types = method.getParameterTypes();
if (types.length != 3 || !Arrays.equals(types, new Class<?>[]{int.class, int.class, int.class})
|| !method.getReturnType().equals(void.class))
continue;
@@ -475,4 +606,14 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
private record SpawnTableKey(Biome biome, MobCategory category) {
}
private record ReachableStructureCache(IrisDimension dimension, Set<String> keys) {
}
private record StructureStepCache(Registry<Structure> registry, List<List<Structure>> structures) {
}
private record NativePlacementGroup(String structureId, IrisNativeStructureDecision decision,
int featureIndex, int step, List<StructureStart> starts) {
}
}
@@ -13,6 +13,7 @@ import art.arcane.iris.core.nms.datapack.DataVersion;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.data.chunk.TerrainChunk;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisDimensionRuntimeContract;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.util.project.agent.Agent;
import art.arcane.volmlib.util.collection.KList;
@@ -71,6 +72,7 @@ import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.commands.data.BlockDataAccessor;
import net.minecraft.server.level.ChunkMap;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ThreadedLevelLightEngine;
import net.minecraft.tags.TagKey;
@@ -93,6 +95,7 @@ import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.chunk.status.WorldGenContext;
import net.minecraft.world.level.dimension.LevelStem;
import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.levelgen.FlatLevelSource;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.WorldgenRandom;
@@ -1019,30 +1022,39 @@ public class NMSBinding implements INMSBinding {
}
public void inject(long seed, Engine engine, World world) throws NoSuchFieldException, IllegalAccessException {
var chunkMap = ((CraftWorld)world).getHandle().getChunkSource().chunkMap;
var worldGenContextField = getField(chunkMap.getClass(), WorldGenContext.class);
ServerLevel level = ((CraftWorld) world).getHandle();
validateDimensionContract(engine, world, level);
ChunkMap chunkMap = level.getChunkSource().chunkMap;
Field worldGenContextField = getField(chunkMap.getClass(), WorldGenContext.class);
worldGenContextField.setAccessible(true);
var worldGenContext = (WorldGenContext) worldGenContextField.get(chunkMap);
var dimensionType = chunkMap.level.dimensionTypeRegistration().unwrapKey().orElse(null);
String expectedDimensionType = "iris:" + engine.getDimension().getDimensionTypeKey();
if (dimensionType != null) {
String actualDimensionType = dimensionType.identifier().toString();
if (!dimensionType.identifier().getNamespace().equals("iris")) {
IrisLogging.error("Loaded world %s with invalid dimension type! expected=%s actual=%s", world.getName(), expectedDimensionType, actualDimensionType);
} else {
IrisLogging.debug("Loaded world " + world.getName() + " with Iris dimension type " + actualDimensionType);
}
} else {
IrisLogging.error("Loaded world %s with unknown dimension type! expected=%s", world.getName(), expectedDimensionType);
}
WorldGenContext worldGenContext = (WorldGenContext) worldGenContextField.get(chunkMap);
IrisChunkGenerator irisGenerator = new IrisChunkGenerator(worldGenContext.generator(), seed, engine, world);
var newContext = new WorldGenContext(
WorldGenContext newContext = new WorldGenContext(
worldGenContext.level(), irisGenerator,
worldGenContext.structureManager(), worldGenContext.lightEngine(), worldGenContext.mainThreadExecutor(), worldGenContext.unsavedListener());
worldGenContextField.set(chunkMap, newContext);
retargetStructureCheck(((CraftWorld) world).getHandle(), irisGenerator);
retargetStructureCheck(level, irisGenerator);
}
private void validateDimensionContract(Engine engine, World world, ServerLevel level) {
DimensionType actualType = level.dimensionType();
String actualTypeKey = level.dimensionTypeRegistration().unwrapKey()
.map(key -> key.identifier().toString())
.orElse("<unregistered>");
IrisDimensionRuntimeContract expected = IrisDimensionRuntimeContract.expected(engine.getDimension(), "iris");
IrisDimensionRuntimeContract actual = new IrisDimensionRuntimeContract(
actualTypeKey,
actualType.minY(),
actualType.height(),
actualType.logicalHeight());
String runtimeName = "Bukkit world '" + world.getName() + "'";
expected.requireExact(runtimeName, actual);
expected.requireHeight(runtimeName, level.getMinY(), level.getHeight());
expected.requireHeight(runtimeName, world.getMinHeight(), world.getMaxHeight() - world.getMinHeight());
IrisLogging.debug("Loaded world " + world.getName() + " with exact Iris dimension type " + actualTypeKey);
}
private static void retargetStructureCheck(ServerLevel level, IrisChunkGenerator generator) throws NoSuchFieldException, IllegalAccessException {
@@ -1208,33 +1220,38 @@ public class NMSBinding implements INMSBinding {
@Override
public boolean injectBukkit() {
if (injected.getAndSet(true))
return true;
try {
IrisLogging.info("Injecting Bukkit");
new AgentBuilder.Default()
.disableClassFormatChanges()
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
.type(ElementMatchers.is(ServerLevel.class))
.transform((builder, typeDescription, classLoader, module, protectionDomain) ->
builder.visit(Advice.to(ServerLevelAdvice.class).on(ElementMatchers.isConstructor()
.and(ElementMatchers.takesArgument(0, MinecraftServer.class))
.and(ElementMatchers.takesArgument(5, LevelStem.class)))))
.installOn(Agent.getInstrumentation());
ByteBuddy buddy = new ByteBuddy();
for (Class<?> clazz : List.of(ChunkAccess.class, ProtoChunk.class)) {
buddy.redefine(clazz)
.visit(Advice.to(ChunkAccessAdvice.class).on(ElementMatchers.isMethod().and(ElementMatchers.takesArguments(ShortList.class, int.class))))
.make()
.load(clazz.getClassLoader(), Agent.installed());
synchronized (injected) {
if (injected.get()) {
return true;
}
try {
IrisLogging.info("Injecting Bukkit");
new AgentBuilder.Default()
.disableClassFormatChanges()
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
.type(ElementMatchers.is(ServerLevel.class))
.transform((builder, typeDescription, classLoader, module, protectionDomain) ->
builder.visit(Advice.to(ServerLevelAdvice.class).on(ElementMatchers.isConstructor()
.and(ElementMatchers.takesArgument(0, MinecraftServer.class))
.and(ElementMatchers.takesArgument(5, LevelStem.class))
.and(ElementMatchers.takesArgument(12, ChunkGenerator.class)))))
.installOn(Agent.getInstrumentation());
ByteBuddy buddy = new ByteBuddy();
for (Class<?> clazz : List.of(ChunkAccess.class, ProtoChunk.class)) {
buddy.redefine(clazz)
.visit(Advice.to(ChunkAccessAdvice.class).on(ElementMatchers.isMethod().and(ElementMatchers.takesArguments(ShortList.class, int.class))))
.make()
.load(clazz.getClassLoader(), Agent.installed());
}
return true;
} catch (Throwable e) {
IrisLogging.error(C.RED + "Failed to inject Bukkit");
e.printStackTrace();
injected.set(true);
return true;
} catch (Throwable e) {
IrisLogging.error(C.RED + "Failed to inject Bukkit");
e.printStackTrace();
return false;
}
}
return false;
}
@Override
@@ -1304,7 +1321,8 @@ public class NMSBinding implements INMSBinding {
static void enter(
@Advice.Argument(0) MinecraftServer server,
@Advice.Argument(4) ResourceKey<Level> dimensionKey,
@Advice.Argument(value = 5, readOnly = false) LevelStem levelStem
@Advice.Argument(value = 5, readOnly = false) LevelStem levelStem,
@Advice.Argument(12) ChunkGenerator constructorGenerator
) {
if (dimensionKey == null)
return;
@@ -1315,18 +1333,19 @@ public class NMSBinding implements INMSBinding {
return;
}
Object generator = Class.forName("art.arcane.iris.core.lifecycle.WorldLifecycleStaging", true, Bukkit.getPluginManager().getPlugin("Iris")
.getClass()
.getClassLoader())
.getDeclaredMethod("consumeStemGenerator", String.class)
.invoke(null, levelId);
ClassLoader pluginClassLoader = Bukkit.getPluginManager().getPlugin("Iris").getClass().getClassLoader();
Class<?> generatorType = Class.forName("art.arcane.iris.engine.platform.PlatformChunkGenerator", true, pluginClassLoader);
Object generator = generatorType.isInstance(constructorGenerator) ? constructorGenerator : null;
if (generator == null) {
generator = Class.forName("art.arcane.iris.core.lifecycle.WorldLifecycleStaging", true, pluginClassLoader)
.getDeclaredMethod("consumeStemGenerator", String.class)
.invoke(null, levelId);
}
if (!(generator instanceof ChunkGenerator gen) || !gen.getClass().getPackageName().startsWith("art.arcane.iris")) {
return;
}
Object bindings = Class.forName("art.arcane.iris.core.nms.INMS", true, Bukkit.getPluginManager().getPlugin("Iris")
.getClass()
.getClassLoader())
Object bindings = Class.forName("art.arcane.iris.core.nms.INMS", true, pluginClassLoader)
.getDeclaredMethod("get")
.invoke(null);
if (bindings == null) {
@@ -25,7 +25,10 @@ final class VanillaStructureBiomes {
if (source == null) {
return keys;
}
for (Holder<Biome> holder : source.possibleBiomes()) {
Set<Holder<Biome>> possibleBiomes = source instanceof CustomBiomeSource customBiomeSource
? customBiomeSource.possibleStructureBiomes()
: source.possibleBiomes();
for (Holder<Biome> holder : possibleBiomes) {
Optional<ResourceKey<Biome>> key = holder.unwrapKey();
if (key.isPresent()) {
keys.add(key.get().identifier().toString());
@@ -0,0 +1,71 @@
package art.arcane.iris.core.nms.v26_2_R1;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertFalse;
public class IrisChunkGeneratorMonumentLocateContractTest {
@Test
public void irisPlacementRunsBeforeExactNativeMonumentIsRemovedFromDelegateLookup() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource")));
int findStart = source.indexOf("findNearestMapStructure(ServerLevel level");
assertTrue(findStart >= 0);
int filterStart = source.indexOf("private HolderSet<Structure> filterReachableStructures", findStart);
assertTrue(filterStart > findStart);
String findMethod = source.substring(findStart, filterStart);
int irisLocate = findMethod.indexOf("IrisStructureLocator.locate(");
int searchLimit = findMethod.indexOf("LocateStatus.SEARCH_LIMIT_REACHED", irisLocate);
int limitSkip = findMethod.indexOf("continue;", searchLimit);
int nativeFilter = findMethod.indexOf("filterReachableStructures(level, holders)");
int delegateLocate = findMethod.indexOf("delegate.findNearestMapStructure(level, reachable");
int reachabilityStart = source.indexOf("private Set<String> reachableStructureKeys", filterStart);
assertTrue(reachabilityStart > filterStart);
String filterMethod = source.substring(filterStart, reachabilityStart);
int monumentReject = filterMethod.indexOf("if (NATIVE_MONUMENT_KEY.equals(key)");
int rejectContinue = filterMethod.indexOf("continue;", monumentReject);
assertTrue(irisLocate >= 0);
assertTrue(searchLimit > irisLocate);
assertTrue(limitSkip > searchLimit);
assertTrue(nativeFilter > limitSkip);
assertTrue(delegateLocate > nativeFilter);
assertTrue(monumentReject >= 0);
assertTrue(rejectContinue > monumentReject);
assertTrue(findMethod.contains("new BlockPos(result.originX(), result.baseY(), result.originZ())"));
}
@Test
public void stiltSupportUsesPlacedSolidOccupancyWithoutSnapshotDifferenceRequirement() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nativeStructurePostProcessorSource")));
int placement = source.indexOf("start.placeInChunk(world, structureManager, generator");
int stiltPlacement = source.indexOf("placeStilts(world, area, structureId, start", placement);
int occupancyCheck = source.indexOf("if (state.isSolid())", stiltPlacement);
assertTrue(placement >= 0);
assertTrue(stiltPlacement > placement);
assertTrue(occupancyCheck > stiltPlacement);
assertFalse(source.contains("state.equals("));
assertFalse(source.contains("snapshot.states"));
}
@Test
public void verticalShiftMovesPiecesJigsawJunctionsAndCachedBoundsTogether() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nativeStructurePostProcessorSource")));
int method = source.indexOf("public static int applyVerticalShift");
int clamp = source.indexOf("StructureVerticalBounds.clampOffset", method);
int pieceMove = source.indexOf("piece.move(0, offsetY, 0)", clamp);
int junctionMove = source.indexOf("junction.getSourceGroundY() + offsetY", pieceMove);
int boundsMove = source.indexOf("bounds.move(0, offsetY, 0)", junctionMove);
assertTrue(method >= 0);
assertTrue(clamp > method);
assertTrue(pieceMove > clamp);
assertTrue(junctionMove > pieceMove);
assertTrue(boundsMove > junctionMove);
}
}
@@ -0,0 +1,38 @@
package art.arcane.iris.nativegen;
import net.minecraft.world.level.levelgen.GenerationStep;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class NativeStructurePostProcessorVegetationTest {
@Test
public void surfaceStructuresClearTreeColumnsAutomatically() {
assertTrue(NativeStructurePostProcessor.shouldClearVegetationColumn(100, 100, false));
assertTrue(NativeStructurePostProcessor.shouldClearVegetationColumn(116, 100, false));
}
@Test
public void buriedStructuresPreserveUnrelatedSurfaceForest() {
assertFalse(NativeStructurePostProcessor.shouldClearVegetationColumn(99, 100, false));
assertFalse(NativeStructurePostProcessor.shouldClearVegetationColumn(20, 100, false));
}
@Test
public void explicitVegetationOptionForcesUnusualPlacementCleanup() {
assertTrue(NativeStructurePostProcessor.shouldClearVegetationColumn(20, 100, true));
}
@Test
public void allUndergroundGenerationStepsShareOneClassification() {
assertTrue(NativeStructurePostProcessor.isUndergroundStep(
GenerationStep.Decoration.UNDERGROUND_STRUCTURES));
assertTrue(NativeStructurePostProcessor.isUndergroundStep(
GenerationStep.Decoration.UNDERGROUND_DECORATION));
assertTrue(NativeStructurePostProcessor.isUndergroundStep(
GenerationStep.Decoration.STRONGHOLDS));
assertFalse(NativeStructurePostProcessor.isUndergroundStep(
GenerationStep.Decoration.SURFACE_STRUCTURES));
}
}