Test- 26.3

This commit is contained in:
Brian Neumann-Fopiano
2026-09-19 23:48:44 -04:00
parent edadd14f4b
commit f0e7fea13b
107 changed files with 25110 additions and 86 deletions
@@ -958,7 +958,7 @@ public class NMSBinding implements INMSBinding {
if (global == null) {
throw new IllegalStateException("Iris cannot build the global block state palette on this server version");
}
java.util.Map<CompoundTag, BlockState> innerDecodeCache = new java.util.concurrent.ConcurrentHashMap<>(64);
java.util.Map<art.arcane.volmlib.util.nbt.tag.Tag<?>, BlockState> innerDecodeCache = new java.util.concurrent.ConcurrentHashMap<>(64);
java.util.Map<CompoundTag, BlockState> outerDecodeCache = new java.util.concurrent.ConcurrentHashMap<>(64);
MCAPalettedContainer<BlockState> container = new MCAPalettedContainer<>(global, registry,
i -> innerDecodeCache.computeIfAbsent(i, t -> ((CraftBlockData) NBTWorld.getBlockData(t)).getState()),
@@ -0,0 +1,83 @@
package art.arcane.iris.nativegen.v26_3_R1;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
import java.util.Set;
import com.mojang.serialization.MapCodec;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.server.level.WorldGenRegion;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.NoiseColumn;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeManager;
import net.minecraft.world.level.biome.FixedBiomeSource;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.RandomState;
import net.minecraft.world.level.levelgen.blending.Blender;
import java.util.List;
import java.util.concurrent.CompletableFuture;
final class ForcedStructureChunkGenerator extends ChunkGenerator {
private final ChunkGenerator delegate;
ForcedStructureChunkGenerator(ChunkGenerator delegate, Holder<Biome> sourceBiome) {
super(new FixedBiomeSource(sourceBiome));
this.delegate = delegate;
}
@Override
protected MapCodec<? extends ChunkGenerator> codec() {
return MapCodec.unit(this);
}
@Override
public void spawnOriginalMobs(WorldGenRegion worldGenRegion) {
delegate.spawnOriginalMobs(worldGenRegion);
}
@Override
public int getGenDepth() {
return delegate.getGenDepth();
}
@Override
public CompletableFuture<ChunkAccess> buildTerrain(ChunkAccess centerChunk, Blender blender, RandomState randomState,
StructureManager structureManager, BiomeManager biomeManager,
WorldGenRegion carverBiomeRegion, Set<Holder<Biome>> possibleBiomes) {
return delegate.buildTerrain(
centerChunk, blender, randomState, structureManager, biomeManager, carverBiomeRegion, possibleBiomes);
}
@Override
public int getSeaLevel() {
return delegate.getSeaLevel();
}
@Override
public int getMinY() {
return delegate.getMinY();
}
@Override
public int getBaseHeight(int x, int z, Heightmap.Types type,
LevelHeightAccessor heightAccessor, RandomState randomState) {
return delegate.getBaseHeight(x, z, type, heightAccessor, randomState);
}
@Override
public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor heightAccessor,
RandomState randomState) {
return delegate.getBaseColumn(x, z, heightAccessor, randomState);
}
@Override
public void addDebugScreenInfo(List<String> result, RandomState randomState,
BlockPos feetPos, SamplerContext samplerContext) {
delegate.addDebugScreenInfo(result, randomState, feetPos, samplerContext);
}
}
@@ -0,0 +1,31 @@
package art.arcane.iris.nativegen.v26_3_R1;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
/**
* Shared waiter for the racy build caches in this package: the loser of a putIfAbsent race
* must surface the same exception the builder threw, not a CompletionException wrapper.
*/
final class NativeBuildFutures {
private NativeBuildFutures() {
}
static <T> T awaitBuild(CompletableFuture<T> future, String what) {
try {
return future.join();
} catch (CompletionException error) {
Throwable cause = error.getCause();
if (cause instanceof RuntimeException runtime) {
throw runtime;
}
if (cause instanceof Error fatal) {
throw fatal;
}
if (cause == null) {
throw error;
}
throw new IllegalStateException(what + " failed", cause);
}
}
}
@@ -0,0 +1,78 @@
package art.arcane.iris.nativegen.v26_3_R1;
import art.arcane.iris.generation.runtime.DimensionStackContext;
import art.arcane.iris.generation.runtime.DimensionStackLayout;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.structure.object.IrisStaticObjectLayer;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.status.ChunkPyramid;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;
public final class NativeGenerationWriteGuard {
private NativeGenerationWriteGuard() {
}
public static boolean allowsDecoration(Engine engine, WorldGenLevel region, ChunkPos center) {
int radius = ChunkPyramid.GENERATION_PYRAMID.getStepTo(ChunkStatus.FEATURES).blockStateWriteRadius();
for (int offsetX = -radius; offsetX <= radius; offsetX++) {
for (int offsetZ = -radius; offsetZ <= radius; offsetZ++) {
int chunkX = Math.addExact(center.x(), offsetX);
int chunkZ = Math.addExact(center.z(), offsetZ);
ChunkAccess target = region.getChunk(chunkX, chunkZ);
if (target.getPersistedStatus().isOrAfter(ChunkStatus.FULL)
&& !engine.getComplex().allowsMantleChunkWrite(chunkX, chunkZ)) {
return false;
}
}
}
return true;
}
public static boolean isHistoricalStructure(Engine engine, long activation) {
if (activation <= 0 || !(engine instanceof IrisEngine irisEngine)) {
return false;
}
GenerationHistoryRuntimeRouter router = irisEngine.getGenerationHistoryRuntimeRouter().orElse(null);
return router != null && activation < router.history().activeActivation().activationId();
}
public static boolean allowsPendingStage(Engine engine, ChunkAccess chunk, ChunkStatus stage) {
return stage.isOrAfter(ChunkStatus.FEATURES)
&& chunk.getPersistedStatus().isOrAfter(ChunkStatus.TERRAIN)
&& !chunk.getPersistedStatus().isOrAfter(stage)
&& !engine.getComplex().allowsMantleChunkWrite(chunk.getPos().x(), chunk.getPos().z());
}
public static Predicate<BlockPos> protectedPositions(
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);
};
}
}
@@ -0,0 +1,286 @@
package art.arcane.iris.nativegen.v26_3_R1;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
import art.arcane.iris.structure.nativegen.NativeStructureStartPlan;
import art.arcane.iris.structure.jigsaw.IrisJigsawConfiguration;
import art.arcane.iris.structure.jigsaw.IrisJigsawHeightmap;
import art.arcane.iris.structure.jigsaw.IrisJigsawLiquidSettings;
import art.arcane.iris.spi.PlatformStructureHooks.JigsawSourceMetadata;
import net.minecraft.core.Holder;
import net.minecraft.core.RegistryAccess;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtOps;
import net.minecraft.nbt.Tag;
import net.minecraft.resources.RegistryOps;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeSource;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.RandomState;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.structures.JigsawStructure;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import java.util.Locale;
import java.util.Objects;
import java.util.function.IntBinaryOperator;
import java.util.function.Predicate;
public final class NativeStructureFactory {
private NativeStructureFactory() {
}
public static StructureStart generate(GenerationContext context, Holder<Structure> sourceHolder,
NativeStructureStartPlan plan, int references) {
Structure source = sourceHolder.value();
Structure configured = configure(
context.registryAccess(), source, plan.source().getJigsaw(),
plan.placement().isUnderground(), plan.baseY());
Holder<Biome> sourceBiome = source.biomes().stream().findFirst()
.or(() -> context.biomeSource().possibleBiomes().stream().findFirst())
.orElseThrow(() -> new IllegalStateException("Configured native structure '"
+ plan.source().getStructure() + "' has no usable generation biome"));
ChunkGenerator forcedGenerator = new ForcedStructureChunkGenerator(
context.generator(), sourceBiome);
StructureStart generated = configured.generate(
sourceHolder,
context.levelKey(),
context.registryAccess(),
forcedGenerator,
forcedGenerator.getBiomeSource(),
context.randomState().createClimateSampler(SamplerContext.EMPTY_UNCACHED),
context.randomState(),
context.templateManager(),
context.levelSeed(),
new ChunkPos(plan.chunkX(), plan.chunkZ()),
references,
context.heightAccessor(),
context.biomePredicate()
);
if (generated == null || !generated.isValid()) {
return StructureStart.INVALID_START;
}
StructureStart positioned;
if (plan.placement().isUnderground()) {
positioned = NativeStructureVerticalPlacer.relocateToMinY(
generated, source, plan.baseY(), context.heightAccessor());
} else {
NativeStructureVerticalPlacer.applyVerticalPlacement(
generated,
plan.source().getStructure(),
0,
context.seaLevel(),
context.heightAccessor().getMinY(),
Math.addExact(context.heightAccessor().getMinY(), context.heightAccessor().getHeight()),
false,
false,
null,
context.surfaceHeight());
positioned = generated;
}
if (!positioned.isValid()) {
return StructureStart.INVALID_START;
}
return NativeStructureReferenceEnvelope.wrapForPublication(
positioned,
source,
references,
plan.placement().resolvedTerrain(),
plan.source().getStructure()
);
}
public static JigsawSourceMetadata sourceMetadata(RegistryAccess registryAccess,
StructureTemplateManager templateManager,
JigsawStructure source) {
CompoundTag sourceTag = encodeJigsaw(registryAccess, source);
return new JigsawSourceMetadata(
sourceDistance(sourceTag, "horizontal"),
horizontalReferenceExpansion(source),
NativeStructureTemplatePoolBounds.sourceHorizontalSpan(
registryAccess, templateManager, source));
}
public static int templatePoolHorizontalSpan(RegistryAccess registryAccess,
StructureTemplateManager templateManager,
String templatePoolKey) {
return NativeStructureTemplatePoolBounds.horizontalSpan(
registryAccess, templateManager, templatePoolKey);
}
public static int jigsawStartPoolHorizontalSpan(RegistryAccess registryAccess,
StructureTemplateManager templateManager,
JigsawStructure source,
String templatePoolKey) {
return NativeStructureTemplatePoolBounds.horizontalSpan(
registryAccess, templateManager, source, templatePoolKey);
}
static Structure configure(RegistryAccess registryAccess, Structure source,
IrisJigsawConfiguration configuration,
boolean underground, int baseY) {
Objects.requireNonNull(source, "Native structure source must not be null");
if (!(source instanceof JigsawStructure sourceJigsaw)) {
if (configuration != null) {
throw new IllegalStateException("Jigsaw options require a registered jigsaw structure, found "
+ source.getClass().getName());
}
return source;
}
RegistryOps<Tag> registryOps = RegistryOps.create(NbtOps.INSTANCE, registryAccess);
CompoundTag structureTag = encodeJigsaw(registryOps, sourceJigsaw);
CompoundTag configuredTag = structureTag.copy();
if (underground) {
CompoundTag height = new CompoundTag();
height.putInt("absolute", baseY);
configuredTag.put("start_height", height);
configuredTag.remove("project_start_to_heightmap");
}
applyConfiguration(configuredTag, configuration);
Structure decoded = Structure.DIRECT_CODEC.parse(registryOps, configuredTag).getOrThrow();
if (!(decoded instanceof JigsawStructure configured)) {
throw new IllegalStateException("Configured native jigsaw codec produced "
+ decoded.getClass().getName());
}
return configured;
}
private static CompoundTag encodeJigsaw(RegistryAccess registryAccess,
JigsawStructure source) {
return encodeJigsaw(RegistryOps.create(NbtOps.INSTANCE, registryAccess), source);
}
private static CompoundTag encodeJigsaw(RegistryOps<Tag> registryOps,
JigsawStructure source) {
Tag encoded = Structure.DIRECT_CODEC.encodeStart(registryOps, source).getOrThrow();
if (!(encoded instanceof CompoundTag structureTag)) {
throw new IllegalStateException("Native jigsaw codec did not produce a compound");
}
return structureTag;
}
private static void applyConfiguration(CompoundTag tag, IrisJigsawConfiguration configuration) {
if (configuration == null) {
return;
}
if (configuration.getStartPool() != null && !configuration.getStartPool().isBlank()) {
tag.putString("start_pool", configuration.getStartPool().trim());
}
if (configuration.getStartJigsawName() != null
&& !configuration.getStartJigsawName().isBlank()) {
if ("NONE".equalsIgnoreCase(configuration.getStartJigsawName().trim())) {
tag.remove("start_jigsaw_name");
} else {
tag.putString("start_jigsaw_name", configuration.getStartJigsawName().trim());
}
}
if (configuration.getMaxDepth() != null) {
tag.putInt("size", configuration.getMaxDepth());
}
applyDistance(tag, configuration);
if (configuration.getUseExpansionHack() != null) {
tag.putBoolean("use_expansion_hack", configuration.getUseExpansionHack());
}
applyHeightmap(tag, configuration.getProjectStartToHeightmap());
applyDimensionPadding(tag, configuration);
applyLiquidSettings(tag, configuration.getLiquidSettings());
}
private static void applyDistance(CompoundTag tag, IrisJigsawConfiguration configuration) {
Integer horizontal = configuration.getMaxDistanceHorizontal();
Integer vertical = configuration.getMaxDistanceVertical();
if (horizontal == null && vertical == null) {
return;
}
int resolvedHorizontal = horizontal == null ? sourceDistance(tag, "horizontal") : horizontal;
int resolvedVertical = vertical == null ? sourceDistance(tag, "vertical") : vertical;
if (resolvedHorizontal == resolvedVertical) {
tag.putInt("max_distance_from_center", resolvedHorizontal);
return;
}
CompoundTag distance = new CompoundTag();
distance.putInt("horizontal", resolvedHorizontal);
distance.putInt("vertical", resolvedVertical);
tag.put("max_distance_from_center", distance);
}
static int sourceDistance(CompoundTag tag, String axis) {
Tag raw = tag.get("max_distance_from_center");
if (raw instanceof CompoundTag compound) {
return compound.getIntOr(axis, 128);
}
return tag.getIntOr("max_distance_from_center", 128);
}
static int horizontalReferenceExpansion(Structure structure) {
BoundingBox content = new BoundingBox(0, 0, 0, 0, 0, 0);
BoundingBox adjusted = structure.adjustBoundingBox(content);
int expansion = Math.max(content.minX() - adjusted.minX(), adjusted.maxX() - content.maxX());
expansion = Math.max(expansion, content.minZ() - adjusted.minZ());
return Math.max(0, Math.max(expansion, adjusted.maxZ() - content.maxZ()));
}
private static void applyHeightmap(CompoundTag tag, IrisJigsawHeightmap heightmap) {
if (heightmap == null || heightmap == IrisJigsawHeightmap.SOURCE) {
return;
}
if (heightmap == IrisJigsawHeightmap.NONE) {
tag.remove("project_start_to_heightmap");
return;
}
tag.putString("project_start_to_heightmap", heightmap.name());
}
private static void applyDimensionPadding(CompoundTag tag, IrisJigsawConfiguration configuration) {
Integer bottom = configuration.getDimensionPaddingBottom();
Integer top = configuration.getDimensionPaddingTop();
if (bottom == null && top == null) {
return;
}
int resolvedBottom = bottom == null ? sourcePadding(tag, "bottom") : bottom;
int resolvedTop = top == null ? sourcePadding(tag, "top") : top;
if (resolvedBottom == resolvedTop) {
tag.putInt("dimension_padding", resolvedBottom);
return;
}
CompoundTag padding = new CompoundTag();
padding.putInt("bottom", resolvedBottom);
padding.putInt("top", resolvedTop);
tag.put("dimension_padding", padding);
}
private static int sourcePadding(CompoundTag tag, String side) {
Tag raw = tag.get("dimension_padding");
if (raw instanceof CompoundTag compound) {
return compound.getIntOr(side, 0);
}
return tag.getIntOr("dimension_padding", 0);
}
private static void applyLiquidSettings(CompoundTag tag, IrisJigsawLiquidSettings settings) {
if (settings == null || settings == IrisJigsawLiquidSettings.SOURCE) {
return;
}
tag.putString("liquid_settings", settings.name().toLowerCase(Locale.ROOT));
}
public record GenerationContext(
RegistryAccess registryAccess,
ChunkGenerator generator,
BiomeSource biomeSource,
RandomState randomState,
StructureTemplateManager templateManager,
long levelSeed,
ResourceKey<Level> levelKey,
LevelHeightAccessor heightAccessor,
Predicate<Holder<Biome>> biomePredicate,
int seaLevel,
IntBinaryOperator surfaceHeight
) {
}
}
@@ -0,0 +1,212 @@
package art.arcane.iris.nativegen.v26_3_R1;
import art.arcane.iris.structure.placement.IrisStructureStiltSettings;
import art.arcane.volmlib.util.math.RNG;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.tags.BlockTags;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.SupportType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.List;
import java.util.Objects;
import java.util.function.IntBinaryOperator;
public final class NativeStructureFoundationBuilder {
private static final int FOUNDATION_VERTICAL_TOLERANCE = 1;
private NativeStructureFoundationBuilder() {
}
private static List<FoundationColumn> foundationEnvelope(BoundingBox area, StructureStart start) {
BoundingBox structure = NativeStructureReferenceEnvelope.contentBounds(start);
int minX = Math.max(area.minX(), structure.minX());
int minZ = Math.max(area.minZ(), structure.minZ());
int maxX = Math.min(area.maxX(), structure.maxX());
int maxZ = Math.min(area.maxZ(), structure.maxZ());
if (minX > maxX || minZ > maxZ) {
return List.of();
}
List<StructurePiece> pieces = start.getPieces();
List<FoundationColumn> columns = new ArrayList<>((maxX - minX + 1) * (maxZ - minZ + 1));
BitSet envelope = new BitSet(area.getYSpan());
for (int z = minZ; z <= maxZ; z++) {
for (int x = minX; x <= maxX; x++) {
envelope.clear();
markFoundationEnvelope(envelope, pieces, area, x, z);
int cellCount = envelope.cardinality();
if (cellCount == 0) {
continue;
}
int[] ys = new int[cellCount];
int cell = 0;
for (int bit = envelope.nextSetBit(0); bit >= 0; bit = envelope.nextSetBit(bit + 1)) {
ys[cell++] = area.minY() + bit;
}
columns.add(new FoundationColumn(x, z, ys));
}
}
return List.copyOf(columns);
}
private static void markFoundationEnvelope(BitSet envelope, List<StructurePiece> pieces, BoundingBox area,
int x, int z) {
for (StructurePiece piece : pieces) {
BoundingBox bounds = piece.getBoundingBox();
if (x < bounds.minX() || x > bounds.maxX() || z < bounds.minZ() || z > bounds.maxZ()) {
continue;
}
int groundY = bounds.minY();
if (piece instanceof PoolElementStructurePiece poolPiece) {
groundY += poolPiece.getGroundLevelDelta();
if (groundY < bounds.minY()) {
continue;
}
}
int minY = Math.max(area.minY(), bounds.minY());
int maxY = Math.min(area.maxY(), Math.min(bounds.maxY(), groundY + FOUNDATION_VERTICAL_TOLERANCE));
if (minY <= maxY) {
envelope.set(minY - area.minY(), maxY - area.minY() + 1);
}
}
}
static void placeStilts(WorldGenLevel world, BoundingBox area, String structureId,
StructureStart start, IrisStructureStiltSettings settings,
NativeStructurePostProcessor.PaletteBlockResolver paletteBlockResolver,
IntBinaryOperator surfaceHeight,
boolean surfaceStructure) {
Objects.requireNonNull(surfaceHeight, "Structure stilts require an Iris terrain height resolver");
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
int structureHash = structureId == null ? 0 : structureId.hashCode();
RNG rng = new RNG(world.getSeed() ^ structureHash);
for (FoundationColumn column : foundationEnvelope(area, start)) {
if (!isStiltColumn(column.x(), column.z(), settings.getSpacing())) {
continue;
}
int foundationY = findFoundationY(world, column, position);
if (foundationY == Integer.MIN_VALUE) {
continue;
}
int terrainY = surfaceStructure
? Math.max(area.minY(), Math.min(
area.maxY(), surfaceHeight.applyAsInt(column.x(), column.z())))
: area.minY() - 1;
int anchorY = findStiltAnchorY(
world, column.x(), column.z(), foundationY,
Math.max(1, settings.getMaxDepth()), terrainY, area.minY(), position);
if (anchorY == Integer.MIN_VALUE) {
continue;
}
for (int y = foundationY - 1; y > anchorY; y--) {
position.set(column.x(), y, column.z());
BlockState stilt = settings.getPalette() == null
? Blocks.COBBLESTONE.defaultBlockState()
: Objects.requireNonNull(
paletteBlockResolver.resolve(
settings.getPalette(), rng, column.x(), y, column.z()),
"Stilt palette returned no block for " + structureId + " at "
+ column.x() + "," + y + "," + column.z());
world.setBlock(position, stilt, 2);
}
}
}
static boolean isStiltColumn(int x, int z, int spacing) {
int resolvedSpacing = Math.max(1, spacing);
return resolvedSpacing == 1
|| Math.floorMod(x, resolvedSpacing) == 0
&& Math.floorMod(z, resolvedSpacing) == 0;
}
static int findStiltAnchorY(
WorldGenLevel world, int x, int z, int foundationY, int maxDepth,
int terrainY, int areaMinY, BlockPos.MutableBlockPos position) {
int minimumAnchorY = Math.max(
areaMinY, Math.max(terrainY, foundationY - maxDepth - 1));
for (int y = foundationY - 1; y >= minimumAnchorY; y--) {
BlockState state = world.getBlockState(position.set(x, y, z));
boolean vegetation = state.is(BlockTags.LOGS) || state.is(BlockTags.LEAVES);
if (!vegetation && state.isFaceSturdy(
world, position, Direction.UP, SupportType.FULL)) {
return y;
}
}
return Integer.MIN_VALUE;
}
public static StiltSupportAudit auditStiltSupport(WorldGenLevel world, BoundingBox area,
StructureStart start, BlockState expectedStilt,
IntBinaryOperator surfaceHeight) {
Objects.requireNonNull(expectedStilt, "Expected stilt state must not be null");
Objects.requireNonNull(surfaceHeight, "Structure stilt audit requires an Iris terrain height resolver");
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
int baseColumns = 0;
int stiltBlocks = 0;
int stiltColumns = 0;
int unsupportedColumns = 0;
for (FoundationColumn column : foundationEnvelope(area, start)) {
int foundationY = findFoundationY(world, column, position);
if (foundationY == Integer.MIN_VALUE) {
continue;
}
baseColumns++;
int terrainY = Math.max(area.minY(), Math.min(
area.maxY(), surfaceHeight.applyAsInt(column.x(), column.z())));
boolean grounded = foundationY <= terrainY + 1;
boolean stiltColumn = false;
for (int y = foundationY - 1; y >= area.minY(); y--) {
if (y <= terrainY) {
grounded = true;
break;
}
BlockState state = world.getBlockState(position.set(column.x(), y, column.z()));
if (state.is(expectedStilt.getBlock())) {
stiltBlocks++;
stiltColumn = true;
if (y == area.minY()) {
grounded = true;
}
continue;
}
boolean vegetation = state.is(BlockTags.LOGS) || state.is(BlockTags.LEAVES);
grounded = state.isSolid() && !vegetation;
break;
}
if (stiltColumn) {
stiltColumns++;
}
if (!grounded) {
unsupportedColumns++;
}
}
return new StiltSupportAudit(baseColumns, stiltBlocks, stiltColumns, unsupportedColumns);
}
private static int findFoundationY(WorldGenLevel world, FoundationColumn column,
BlockPos.MutableBlockPos position) {
for (int cell = 0; cell < column.ys().length; cell++) {
int y = column.ys()[cell];
BlockState state = world.getBlockState(position.set(column.x(), y, column.z()));
if (state.isSolid()) {
return y;
}
}
return Integer.MIN_VALUE;
}
private record FoundationColumn(int x, int z, int[] ys) {
}
public record StiltSupportAudit(int baseColumns, int stiltBlocks, int stiltColumns,
int unsupportedColumns) {
}
}
@@ -0,0 +1,35 @@
package art.arcane.iris.nativegen.v26_3_R1;
public final class NativeStructureGenerationException extends IllegalStateException {
private NativeStructureGenerationException(FailureContext context, Throwable cause) {
super(message(context), cause);
}
private NativeStructureGenerationException(FailureContext context) {
super(message(context));
}
public static NativeStructureGenerationException failure(String operation, String structureContext,
int chunkX, int chunkZ, Throwable cause) {
return new NativeStructureGenerationException(
new FailureContext(operation, structureContext, chunkX, chunkZ), cause);
}
public static NativeStructureGenerationException failure(String operation, String structureContext,
int chunkX, int chunkZ) {
return new NativeStructureGenerationException(
new FailureContext(operation, structureContext, chunkX, chunkZ));
}
private static String message(FailureContext context) {
String resolvedContext = context.structureContext() == null || context.structureContext().isBlank()
? "<unregistered structure>"
: context.structureContext();
return "Iris native structure " + context.operation() + " failed for " + resolvedContext
+ " in chunk " + context.chunkX() + "," + context.chunkZ()
+ "; chunk generation was aborted to prevent partial structure state";
}
private record FailureContext(String operation, String structureContext, int chunkX, int chunkZ) {
}
}
@@ -0,0 +1,121 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.nativegen.v26_3_R1;
import net.minecraft.core.Holder;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureSet;
import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement;
import net.minecraft.world.level.levelgen.structure.placement.AbstractSpreadingStructurePlacement;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
public final class NativeStructureGenerationKeys {
private static final Method FREQUENCY_METHOD = resolveFrequencyMethod();
private NativeStructureGenerationKeys() {
}
public static Set<String> reachable(ServerLevel level, Set<String> possibleBiomeKeys) {
ServerLevel activeLevel = Objects.requireNonNull(level, "Native structure reachability requires a level");
Set<String> possibleBiomes = Objects.requireNonNull(
possibleBiomeKeys, "Native structure reachability requires possible biome keys");
if (!activeLevel.getServer().getWorldGenSettings().options().generateStructures()) {
return Collections.emptySet();
}
Set<String> keys = new LinkedHashSet<>();
List<Holder<StructureSet>> structureSets =
activeLevel.getChunkSource().getGeneratorState().possibleStructureSets();
for (Holder<StructureSet> structureSetHolder : structureSets) {
StructureSet structureSet = structureSetHolder.value();
if (!isEnabledPlacement(structureSet.placement())) {
continue;
}
for (StructureSet.StructureSelectionEntry entry : structureSet.structures()) {
if (entry.weight() <= 0 || !hasPossibleBiome(entry.structure().value(), possibleBiomes)) {
continue;
}
Optional<ResourceKey<Structure>> key = entry.structure().unwrapKey();
key.ifPresent(resourceKey -> keys.add(resourceKey.identifier().toString()));
}
}
return Collections.unmodifiableSet(keys);
}
static boolean hasPossibleBiome(Structure structure, Set<String> possibleBiomeKeys) {
for (Holder<Biome> holder : structure.biomes()) {
Optional<ResourceKey<Biome>> key = holder.unwrapKey();
if (key.isPresent() && possibleBiomeKeys.contains(key.get().identifier().toString())) {
return true;
}
}
return false;
}
static boolean isEnabledPlacement(StructurePlacement placement) {
return placementFrequency(placement) > 0.0F;
}
private static float placementFrequency(StructurePlacement placement) {
StructurePlacement activePlacement = Objects.requireNonNull(
placement, "Native structure placement frequency requires a placement");
if (!(activePlacement instanceof AbstractSpreadingStructurePlacement)) {
return 1F;
}
try {
return ((Float) FREQUENCY_METHOD.invoke(activePlacement)).floatValue();
} catch (ReflectiveOperationException error) {
throw new IllegalStateException(
"Unable to read native structure placement frequency from "
+ activePlacement.getClass().getName(), error);
}
}
private static Method resolveFrequencyMethod() {
Method frequencyMethod = null;
for (Method method : AbstractSpreadingStructurePlacement.class.getDeclaredMethods()) {
if (Modifier.isStatic(method.getModifiers())
|| method.getParameterCount() != 0
|| method.getReturnType() != float.class) {
continue;
}
if (frequencyMethod != null) {
throw new IllegalStateException(
"Native structure placement exposes multiple zero-argument float accessors");
}
frequencyMethod = method;
}
if (frequencyMethod == null || !frequencyMethod.trySetAccessible()) {
throw new IllegalStateException(
"Native structure placement frequency accessor is unavailable");
}
return frequencyMethod;
}
}
@@ -0,0 +1,351 @@
package art.arcane.iris.nativegen.v26_3_R1;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.structure.placement.IrisStructureLocator;
import art.arcane.iris.structure.nativegen.NativeStructureOwnershipRecord;
import art.arcane.iris.spi.IrisLogging;
import net.minecraft.SharedConstants;
import net.minecraft.core.Registry;
import net.minecraft.core.registries.Registries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.IntTag;
import net.minecraft.nbt.NbtUtils;
import net.minecraft.nbt.StreamTagVisitor;
import net.minecraft.nbt.Tag;
import net.minecraft.nbt.visitors.CollectFields;
import net.minecraft.nbt.visitors.FieldSelector;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.util.datafix.DataFixTypes;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.storage.SimpleRegionStorage;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
public final class NativeStructureLocatePersistence {
public static final int MAX_SELECTED_CANDIDATE_RETRIES = 64;
private static final int MAX_STORAGE_PROBES = 512;
private static final Method PAPER_LEVEL_TYPE_KEY = paperLevelTypeKey();
private NativeStructureLocatePersistence() {
}
public static Probe probe(ServerLevel level, Structure structure, boolean requireUnreferenced) {
return probe(level, structure, requireUnreferenced, new ProbeBudget(MAX_STORAGE_PROBES));
}
public static ProbeBudget probeBudget() {
return new ProbeBudget(MAX_STORAGE_PROBES);
}
public static Probe probe(ServerLevel level, Structure structure, boolean requireUnreferenced,
ProbeBudget budget) {
return new Probe(level, structure, requireUnreferenced, budget);
}
public static Search search(Engine engine, String structureKey,
int blockX, int blockZ, int radius, Probe probe) {
return new Search(engine, structureKey, blockX, blockZ, radius, probe);
}
public static final class Probe {
private final ServerLevel level;
private final Structure structure;
private final String structureKey;
private final boolean requireUnreferenced;
private final ProbeBudget budget;
private final Map<Long, Boolean> storedDecisions = new HashMap<>();
Probe(ServerLevel level, Structure structure, boolean requireUnreferenced,
ProbeBudget budget) {
this.level = level;
this.structure = structure;
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
Identifier identifier = registry.getKey(structure);
if (identifier == null) {
throw new IllegalStateException("Cannot probe an unregistered native structure");
}
this.structureKey = identifier.toString();
this.requireUnreferenced = requireUnreferenced;
this.budget = budget;
}
public boolean accepts(int chunkX, int chunkZ) {
ChunkAccess loaded = level.getChunkSource().getChunk(
chunkX, chunkZ, ChunkStatus.STRUCTURE_STARTS, false);
if (loaded != null) {
StructureStart start = loaded.getStartForStructure(structure);
return start != null && start.isValid()
&& (!requireUnreferenced || start.canBeReferenced());
}
ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ);
Boolean storedDecision = storedDecisions.get(chunkPos.pack());
if (storedDecision != null) {
return storedDecision;
}
boolean accepted = budget.acceptsStored(
level, chunkPos, structureKey, requireUnreferenced);
storedDecisions.put(chunkPos.pack(), accepted);
return accepted;
}
public StructureStart verifySelected(int chunkX, int chunkZ) {
ChunkAccess chunk = level.getChunk(chunkX, chunkZ, ChunkStatus.STRUCTURE_STARTS);
StructureStart start = chunk.getStartForStructure(structure);
if (start == null || !start.isValid()) {
return null;
}
if (requireUnreferenced) {
if (!start.canBeReferenced()) {
return null;
}
}
return start;
}
private void reference(StructureStart start) {
if (requireUnreferenced) {
level.structureManager().addReference(start);
}
}
}
public static final class ProbeBudget {
private final int maximum;
private final Map<Long, StoredChunkState> storedChunks = new HashMap<>();
private final ChunkStorageScanner scanner;
private final StoredChunkDatafixer datafixer;
private int used;
private boolean scanFailureReported;
private ProbeBudget(int maximum) {
this(maximum, null, ProbeBudget::datafix);
}
ProbeBudget(int maximum, ChunkStorageScanner scanner) {
this(maximum, scanner, ProbeBudget::datafix);
}
ProbeBudget(int maximum, ChunkStorageScanner scanner,
StoredChunkDatafixer datafixer) {
this.maximum = maximum;
this.scanner = scanner;
this.datafixer = Objects.requireNonNull(
datafixer, "Stored chunk datafixer must not be null");
}
private void claim() {
if (used >= maximum) {
throw new IrisStructureLocator.CandidateSearchLimitException();
}
used++;
}
private StoredChunkState storedState(ServerLevel level, ChunkPos chunkPos) {
StoredChunkState cached = storedChunks.get(chunkPos.pack());
if (cached != null) {
return cached;
}
claim();
CollectFields fields = new CollectFields(
new FieldSelector(IntTag.TYPE, "DataVersion"),
new FieldSelector("Level", "Structures", CompoundTag.TYPE, "Starts"),
new FieldSelector("structures", CompoundTag.TYPE, "starts"));
ChunkStorageScanner activeScanner = scanner == null
? level.getChunkSource().chunkMap.chunkScanner()::scanChunk : scanner;
try {
activeScanner.scan(chunkPos, fields).join();
} catch (RuntimeException error) {
if (!scanFailureReported) {
scanFailureReported = true;
IrisLogging.reportError(
"Native structure locate could not scan stored chunk state; candidates will be verified by loading their structure starts.",
error);
}
StoredChunkState unresolved = StoredChunkState.missing();
storedChunks.put(chunkPos.pack(), unresolved);
return unresolved;
}
Tag result = fields.getResult();
StoredChunkState resolved;
try {
resolved = result instanceof CompoundTag storedChunk
? StoredChunkState.parse(datafixer.datafix(level, storedChunk))
: StoredChunkState.missing();
} catch (RuntimeException error) {
if (!scanFailureReported) {
scanFailureReported = true;
IrisLogging.reportError(
"Native structure locate could not datafix stored chunk state; candidates will be verified by loading their structure starts.",
error);
}
resolved = StoredChunkState.missing();
}
storedChunks.put(chunkPos.pack(), resolved);
return resolved;
}
private static CompoundTag datafix(ServerLevel level, CompoundTag storedChunk) {
int dataVersion = NbtUtils.getDataVersion(storedChunk);
int currentDataVersion = SharedConstants.getCurrentVersion().dataVersion().version();
if (dataVersion >= currentDataVersion) {
return storedChunk;
}
if (level == null) {
throw new IllegalStateException("Stored chunk datafix requires an active level");
}
CompoundTag context = chunkDataFixContext(level);
SimpleRegionStorage.injectDatafixingContext(storedChunk, context);
return DataFixTypes.CHUNK.updateToCurrentVersion(
level.getServer().getFixerUpper(), storedChunk, dataVersion);
}
private static CompoundTag chunkDataFixContext(ServerLevel level) {
CompoundTag context = new CompoundTag();
String levelIdentifier = level.dimension().identifier().toString();
if (PAPER_LEVEL_TYPE_KEY == null) {
context.putString("dimension", levelIdentifier);
} else {
context.putString("dimension", paperLevelTypeKey(level).identifier().toString());
context.putString("level_identifier", levelIdentifier);
}
level.getChunkSource().getGenerator().getTypeNameForDataFixer()
.ifPresent(identifier -> context.putString("generator", identifier.toString()));
return context;
}
boolean acceptsStored(ServerLevel level, ChunkPos chunkPos,
String structureKey, boolean requireUnreferenced) {
return storedState(level, chunkPos).accepts(structureKey, requireUnreferenced);
}
int used() {
return used;
}
}
public static final class Search {
private final Engine engine;
private final String structureKey;
private final int blockX;
private final int blockZ;
private final int radius;
private final Probe probe;
private final Set<Long> rejectedChunks = new HashSet<>();
private Search(Engine engine, String structureKey, int blockX, int blockZ,
int radius, Probe probe) {
this.engine = engine;
this.structureKey = structureKey;
this.blockX = blockX;
this.blockZ = blockZ;
this.radius = radius;
this.probe = probe;
}
public IrisStructureLocator.LocateResult predict() {
if (rejectedChunks.size() >= MAX_SELECTED_CANDIDATE_RETRIES) {
return new IrisStructureLocator.LocateResult(
IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED, 0, 0, 0);
}
return IrisStructureLocator.locateInPlacementRings(
engine, structureKey, blockX, blockZ, radius,
(chunkX, chunkZ) -> !rejectedChunks.contains(ChunkPos.pack(chunkX, chunkZ))
&& probe.accepts(chunkX, chunkZ));
}
public VerifiedStart verify(IrisStructureLocator.LocateResult result) {
int chunkX = result.originX() >> 4;
int chunkZ = result.originZ() >> 4;
StructureStart start = probe.verifySelected(chunkX, chunkZ);
if (start == null) {
return null;
}
NativeStructureOwnershipRecord ownership = NativeStructureOwnershipRecovery.resolve(
engine, probe.level, structureKey, probe.structure, start);
if (ownership == null) {
return null;
}
return new VerifiedStart(start, ownership);
}
public void reject(IrisStructureLocator.LocateResult result) {
rejectedChunks.add(ChunkPos.pack(result.originX() >> 4, result.originZ() >> 4));
}
public void reference(VerifiedStart verified) {
probe.reference(verified.start());
}
}
private static Method paperLevelTypeKey() {
try {
return ServerLevel.class.getMethod("getTypeKey");
} catch (NoSuchMethodException ignored) {
return null;
}
}
private static ResourceKey<?> paperLevelTypeKey(ServerLevel level) {
try {
Object value = PAPER_LEVEL_TYPE_KEY.invoke(level);
if (value instanceof ResourceKey<?> key) {
return key;
}
throw new IllegalStateException("Paper level type key has an unexpected value");
} catch (IllegalAccessException | InvocationTargetException error) {
throw new IllegalStateException("Cannot read Paper level type key", error);
}
}
public record VerifiedStart(StructureStart start,
NativeStructureOwnershipRecord ownership) {
}
@FunctionalInterface
interface ChunkStorageScanner {
CompletableFuture<Void> scan(ChunkPos chunkPos, StreamTagVisitor visitor);
}
@FunctionalInterface
interface StoredChunkDatafixer {
CompoundTag datafix(ServerLevel level, CompoundTag storedChunk);
}
private record StoredChunkState(boolean stored, boolean startsPresent, CompoundTag starts) {
private static StoredChunkState missing() {
return new StoredChunkState(false, false, new CompoundTag());
}
private static StoredChunkState parse(CompoundTag chunk) {
CompoundTag structures = chunk.getCompoundOrEmpty("structures");
return structures.getCompound("starts")
.map(starts -> new StoredChunkState(true, true, starts))
.orElseGet(() -> new StoredChunkState(true, false, new CompoundTag()));
}
private boolean accepts(String structureKey, boolean requireUnreferenced) {
if (!stored || !startsPresent) {
return true;
}
return starts.getCompound(structureKey).map(start -> {
String id = start.getStringOr("id", "");
return !StructureStart.INVALID_START_ID.equals(id)
&& (!requireUnreferenced || start.getIntOr("references", 0) == 0);
}).orElse(false);
}
}
}
@@ -0,0 +1,58 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.nativegen.v26_3_R1;
import com.mojang.datafixers.util.Pair;
import net.minecraft.core.BlockPos;
public final class NativeStructureLocateResults {
private NativeStructureLocateResults() {
}
public static <T> Pair<BlockPos, T> nearest(BlockPos origin, Pair<BlockPos, T> replacement,
Pair<BlockPos, T> nativeResult) {
if (replacement == null) {
return nativeResult;
}
if (nativeResult == null) {
return replacement;
}
return horizontalDistanceSquared(origin, nativeResult.getFirst())
<= horizontalDistanceSquared(origin, replacement.getFirst()) ? nativeResult : replacement;
}
public static <T> Pair<BlockPos, T> selectAndReference(
BlockPos origin,
Pair<BlockPos, T> replacement, Runnable replacementReference,
Pair<BlockPos, T> nativeResult, Runnable nativeReference) {
Pair<BlockPos, T> selected = nearest(origin, replacement, nativeResult);
if (selected == replacement && replacement != null) {
replacementReference.run();
} else if (selected == nativeResult && nativeResult != null) {
nativeReference.run();
}
return selected;
}
private static long horizontalDistanceSquared(BlockPos origin, BlockPos target) {
long dx = (long) target.getX() - origin.getX();
long dz = (long) target.getZ() - origin.getZ();
return dx * dx + dz * dz;
}
}
@@ -0,0 +1,239 @@
package art.arcane.iris.nativegen.v26_3_R1;
import art.arcane.iris.structure.nativegen.NativeStructureOwnershipRecord;
import art.arcane.iris.structure.nativegen.NativeStructurePlacementPlanner;
import art.arcane.iris.structure.nativegen.NativeStructureStartPlan;
import art.arcane.iris.structure.placement.StructurePlacementGrid;
import net.minecraft.core.Direction;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.Identifier;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentStructure;
import net.minecraft.world.level.levelgen.structure.pools.EmptyPoolElement;
import net.minecraft.world.level.levelgen.structure.pools.ListPoolElement;
import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HexFormat;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
public final class NativeStructureOwnershipFingerprint {
private static final Comparator<PieceIdentity> PIECE_ORDER = Comparator
.comparing(PieceIdentity::type)
.thenComparingInt(PieceIdentity::minX)
.thenComparingInt(PieceIdentity::minY)
.thenComparingInt(PieceIdentity::minZ)
.thenComparingInt(PieceIdentity::maxX)
.thenComparingInt(PieceIdentity::maxY)
.thenComparingInt(PieceIdentity::maxZ)
.thenComparingInt(PieceIdentity::orientation)
.thenComparingInt(PieceIdentity::generationDepth)
.thenComparing(PieceIdentity::detail);
private NativeStructureOwnershipFingerprint() {
}
public static NativeStructureOwnershipRecord capture(String structureKey,
StructureStart start,
NativeStructureStartPlan plan,
BoundingBox referenceEnvelope) {
StructureStart resolvedStart = requireValid(start);
NativeStructureStartPlan resolvedPlan = Objects.requireNonNull(
plan, "Native structure start plan must not be null");
BoundingBox content = NativeStructureReferenceEnvelope.contentBounds(resolvedStart);
BoundingBox references = Objects.requireNonNull(
referenceEnvelope, "Native structure reference envelope must not be null");
ChunkPos origin = resolvedStart.getChunkPos();
return NativeStructureOwnershipRecord.create(
structureKey,
origin.x(),
origin.z(),
StructurePlacementGrid.placementIdentity(resolvedPlan.placement()),
resolvedPlan.baseY(),
content.minX(),
content.minY(),
content.minZ(),
content.maxX(),
content.maxY(),
content.maxZ(),
locatorY(resolvedStart),
references.minX() >> 4,
references.maxX() >> 4,
references.minZ() >> 4,
references.maxZ() >> 4,
fingerprint(structureKey, resolvedStart),
NativeStructurePlacementPlanner.decisionFor(resolvedPlan)
);
}
public static boolean matches(NativeStructureOwnershipRecord record, StructureStart start) {
if (record == null || start == null || !start.isValid()) {
return false;
}
ChunkPos origin = start.getChunkPos();
if (origin.x() != record.originChunkX()
|| origin.z() != record.originChunkZ()
|| !fingerprint(record.structureKey(), start).equals(record.contentFingerprint())) {
return false;
}
BoundingBox content = NativeStructureReferenceEnvelope.contentBounds(start);
if (content.getXSpan() != (record.contentMaxX() - record.contentMinX()) + 1
|| content.getYSpan() != (record.contentMaxY() - record.contentMinY()) + 1
|| content.getZSpan() != (record.contentMaxZ() - record.contentMinZ()) + 1) {
return false;
}
return start.getStructure() instanceof OceanMonumentStructure
|| content.minX() == record.contentMinX()
&& content.minY() == record.contentMinY()
&& content.minZ() == record.contentMinZ()
&& content.maxX() == record.contentMaxX()
&& content.maxY() == record.contentMaxY()
&& content.maxZ() == record.contentMaxZ();
}
public static String fingerprint(String structureKey, StructureStart start) {
StructureStart resolvedStart = requireValid(start);
MessageDigest digest = sha256();
updateString(digest, Objects.requireNonNull(structureKey,
"Native structure key must not be null").trim().toLowerCase(Locale.ROOT));
updateInt(digest, resolvedStart.getChunkPos().x());
updateInt(digest, resolvedStart.getChunkPos().z());
BoundingBox content = NativeStructureReferenceEnvelope.contentBounds(resolvedStart);
List<PieceIdentity> pieces = contentPieces(resolvedStart, content.minY());
updateInt(digest, pieces.size());
for (PieceIdentity piece : pieces) {
updateString(digest, piece.type());
updateInt(digest, piece.minX());
updateInt(digest, piece.minY());
updateInt(digest, piece.minZ());
updateInt(digest, piece.maxX());
updateInt(digest, piece.maxY());
updateInt(digest, piece.maxZ());
updateInt(digest, piece.orientation());
updateInt(digest, piece.generationDepth());
updateString(digest, piece.detail());
}
return HexFormat.of().formatHex(digest.digest());
}
public static int locatorY(StructureStart start) {
StructureStart resolvedStart = requireValid(start);
if (!resolvedStart.getPieces().isEmpty()) {
return resolvedStart.getPieces().getFirst().getLocatorPosition().getY();
}
throw new IllegalStateException("Native structure contains no locatable content pieces");
}
private static List<PieceIdentity> contentPieces(StructureStart start, int contentMinY) {
List<PieceIdentity> pieces = new ArrayList<>();
for (StructurePiece piece : start.getPieces()) {
BoundingBox bounds = piece.getBoundingBox();
Identifier type = BuiltInRegistries.STRUCTURE_PIECE.getKey(piece.getType());
Direction orientation = piece.getOrientation();
String detail = pieceDetail(piece, contentMinY);
pieces.add(new PieceIdentity(
type == null ? "" : type.toString(),
bounds.minX(), bounds.minY() - contentMinY, bounds.minZ(),
bounds.maxX(), bounds.maxY() - contentMinY, bounds.maxZ(),
orientation == null ? -1 : orientation.get2DDataValue(),
piece.getGenDepth(),
detail
));
}
if (pieces.isEmpty()) {
throw new IllegalStateException("Native structure contains no content pieces");
}
pieces.sort(PIECE_ORDER);
return List.copyOf(pieces);
}
private static String pieceDetail(StructurePiece piece, int contentMinY) {
if (!(piece instanceof PoolElementStructurePiece poolPiece)) {
return "";
}
return poolElementIdentity(poolPiece.getElement()) + "|"
+ poolPiece.getPosition().getX() + ","
+ (poolPiece.getPosition().getY() - contentMinY) + ","
+ poolPiece.getPosition().getZ() + "|"
+ poolPiece.getGroundLevelDelta() + "|"
+ poolPiece.getRotation().getSerializedName();
}
private static String poolElementIdentity(StructurePoolElement element) {
Identifier type = BuiltInRegistries.STRUCTURE_POOL_ELEMENT.getKey(element.getType());
StringBuilder identity = new StringBuilder(element.getClass().getName())
.append('|')
.append(type == null ? "" : type)
.append('|')
.append(element.getProjection().getSerializedName())
.append('|')
.append(element.getGroundLevelDelta());
if (element == EmptyPoolElement.INSTANCE) {
return identity.append("|empty").toString();
}
if (element instanceof SinglePoolElement single) {
try {
return identity.append("|template=")
.append(single.getTemplateLocation()).toString();
} catch (RuntimeException inlineTemplate) {
return identity.append("|runtime-template").toString();
}
}
if (element instanceof ListPoolElement list) {
identity.append("|list[");
for (StructurePoolElement child : list.getElements()) {
String childIdentity = poolElementIdentity(child);
identity.append(childIdentity.length()).append(':').append(childIdentity);
}
return identity.append(']').toString();
}
return identity.append("|bounded-custom").toString();
}
private static StructureStart requireValid(StructureStart start) {
StructureStart resolved = Objects.requireNonNull(
start, "Native structure start must not be null");
if (!resolved.isValid()) {
throw new IllegalArgumentException("Native structure start must be valid");
}
return resolved;
}
private static MessageDigest sha256() {
try {
return MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException error) {
throw new IllegalStateException("SHA-256 is unavailable", error);
}
}
private static void updateString(MessageDigest digest, String value) {
byte[] encoded = value.getBytes(StandardCharsets.UTF_8);
updateInt(digest, encoded.length);
digest.update(encoded);
}
private static void updateInt(MessageDigest digest, int value) {
digest.update((byte) (value >>> 24));
digest.update((byte) (value >>> 16));
digest.update((byte) (value >>> 8));
digest.update((byte) value);
}
private record PieceIdentity(String type,
int minX, int minY, int minZ,
int maxX, int maxY, int maxZ,
int orientation, int generationDepth, String detail) {
}
}
@@ -0,0 +1,195 @@
package art.arcane.iris.nativegen.v26_3_R1;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.structure.nativegen.NativeStructureGenerationPolicy;
import art.arcane.iris.structure.nativegen.NativeStructureOwnershipRecord;
import art.arcane.iris.structure.nativegen.NativeStructureOwnershipStore;
import art.arcane.iris.structure.nativegen.NativeStructurePlacementPlanner;
import art.arcane.iris.structure.nativegen.NativeStructureStartPlan;
import art.arcane.iris.structure.placement.StructurePlacementGrid;
import art.arcane.iris.structure.placement.IrisStructureTerrain;
import art.arcane.iris.structure.placement.IrisStructureTerrainMode;
import art.arcane.iris.structure.nativegen.NativeStructureGenerationStatus;
import art.arcane.iris.structure.nativegen.NativeStructureSuppression;
import net.minecraft.core.Holder;
import net.minecraft.core.Registry;
import net.minecraft.core.registries.Registries;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.biome.BiomeSource;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement;
import java.util.Locale;
import java.util.Objects;
public final class NativeStructureOwnershipRecovery {
private NativeStructureOwnershipRecovery() {
}
public static NativeStructureOwnershipRecord resolve(
Engine engine, ServerLevel level, String structureKey,
Structure structure, StructureStart start) {
Objects.requireNonNull(engine, "Native structure ownership recovery requires an engine");
ServerLevel activeLevel = Objects.requireNonNull(
level, "Native structure ownership recovery requires a level");
Structure activeStructure = Objects.requireNonNull(
structure, "Native structure ownership recovery requires a structure");
if (start == null || !start.isValid() || start.getStructure() != activeStructure) {
return null;
}
ChunkPos origin = start.getChunkPos();
NativeStructureOwnershipRecord persisted = NativeStructureOwnershipStore.findPersisted(
engine, structureKey, origin.x(), origin.z());
if (persisted != null) {
NativeStructureOwnershipRecord refreshed = refreshReferenceEnvelope(
structureKey, activeStructure, start, persisted);
if (refreshed != null) {
if (refreshed != persisted) {
NativeStructureOwnershipStore.record(engine, refreshed);
}
return refreshed;
}
NativeStructureOwnershipStore.discard(
engine, structureKey, origin.x(), origin.z());
}
NativeStructureStartPlan plan = NativeStructurePlacementPlanner.matchingPlan(
engine, structureKey, origin.x(), origin.z());
if (!matchesPlan(structureKey, origin, plan)) {
return null;
}
Registry<Structure> registry = activeLevel.registryAccess().lookupOrThrow(Registries.STRUCTURE);
Holder<Structure> holder = registry.wrapAsHolder(activeStructure);
ChunkGeneratorStructureState state = activeLevel.getChunkSource().getGeneratorState();
if (naturalStartIsAmbiguous(engine, structureKey, activeStructure, holder, state, plan)) {
return null;
}
StructureStart expected = generateExpected(
engine, activeLevel, holder, plan, start.getReferences());
NativeStructureOwnershipRecord recovered = proveCandidate(
structureKey, activeStructure, start, plan, expected, false);
if (recovered == null) {
return null;
}
NativeStructureOwnershipStore.record(engine, recovered);
return recovered;
}
static NativeStructureOwnershipRecord refreshReferenceEnvelope(
String structureKey, Structure structure, StructureStart start,
NativeStructureOwnershipRecord ownership) {
if (structure == null || start == null || !start.isValid()
|| start.getStructure() != structure || ownership == null
|| !ownership.structureKey().equals(normalize(structureKey))
|| !NativeStructureOwnershipFingerprint.matches(ownership, start)) {
return null;
}
IrisStructureTerrain terrain = NativeStructureTerrainIntegrator.resolveNativeTerrain(
start, ownership.restoredDecision().terrain());
if (terrain.resolvedMode() != IrisStructureTerrainMode.VACUUM) {
return ownership;
}
BoundingBox expected = NativeStructureReferenceEnvelope.referenceBounds(
start, structure, terrain, structureKey);
int referenceMinChunkX = expected.minX() >> 4;
int referenceMaxChunkX = expected.maxX() >> 4;
int referenceMinChunkZ = expected.minZ() >> 4;
int referenceMaxChunkZ = expected.maxZ() >> 4;
if (ownership.referenceMinChunkX() == referenceMinChunkX
&& ownership.referenceMaxChunkX() == referenceMaxChunkX
&& ownership.referenceMinChunkZ() == referenceMinChunkZ
&& ownership.referenceMaxChunkZ() == referenceMaxChunkZ) {
return ownership;
}
return ownership.withReferenceEnvelope(
referenceMinChunkX, referenceMaxChunkX,
referenceMinChunkZ, referenceMaxChunkZ);
}
static NativeStructureOwnershipRecord proveCandidate(
String structureKey, Structure structure, StructureStart persisted,
NativeStructureStartPlan plan, StructureStart expected,
boolean naturalStartAmbiguous) {
if (naturalStartAmbiguous || structure == null
|| persisted == null || !persisted.isValid()
|| expected == null || !expected.isValid()
|| persisted.getStructure() != structure
|| expected.getStructure() != structure
|| !matchesPlan(structureKey, persisted.getChunkPos(), plan)
|| !persisted.getChunkPos().equals(expected.getChunkPos())) {
return null;
}
BoundingBox referenceEnvelope = NativeStructureReferenceEnvelope.referenceBounds(
expected, structure, plan.placement().resolvedTerrain(), structureKey);
NativeStructureOwnershipRecord candidate = NativeStructureOwnershipFingerprint.capture(
structureKey, expected, plan, referenceEnvelope);
if (candidate.placementIdentity()
!= StructurePlacementGrid.placementIdentity(plan.placement())) {
return null;
}
return NativeStructureOwnershipFingerprint.matches(candidate, persisted)
? candidate : null;
}
private static boolean matchesPlan(String structureKey, ChunkPos origin,
NativeStructureStartPlan plan) {
if (structureKey == null || structureKey.isBlank() || plan == null) {
return false;
}
return plan.chunkX() == origin.x()
&& plan.chunkZ() == origin.z()
&& normalize(structureKey).equals(normalize(plan.source().getStructure()));
}
private static StructureStart generateExpected(
Engine engine, ServerLevel level, Holder<Structure> holder,
NativeStructureStartPlan plan, int references) {
ChunkGeneratorStructureState state = level.getChunkSource().getGeneratorState();
ChunkGenerator generator = level.getChunkSource().getGenerator();
BiomeSource biomeSource = generator.getBiomeSource();
NativeStructureFactory.GenerationContext generationContext =
new NativeStructureFactory.GenerationContext(
level.registryAccess(),
generator,
biomeSource,
state.randomState(),
level.getStructureTemplateManager(),
state.getLevelSeed(),
level.dimension(),
level,
biome -> true,
generator.getSeaLevel(),
(x, z) -> Engine.hostHeight(engine, x, z, true) + engine.getMinHeight()
);
return NativeStructureFactory.generate(generationContext, holder, plan, references);
}
private static boolean naturalStartIsAmbiguous(
Engine engine, String structureKey, Structure structure,
Holder<Structure> holder, ChunkGeneratorStructureState state,
NativeStructureStartPlan plan) {
if (plan.placement().getNativeSuppression() == NativeStructureSuppression.REPLACE_SOURCE) {
return false;
}
NativeStructureGenerationStatus sourceStatus = NativeStructureGenerationPolicy.resolve(
engine, structureKey,
NativeStructureVegetationClearer.isUndergroundStep(structure.step())).status();
if (sourceStatus == NativeStructureGenerationStatus.REPLACED_BY_IRIS) {
return false;
}
for (StructurePlacement placement : state.getPlacementsForStructure(holder)) {
if (placement.isStructureChunk(state, plan.chunkX(), plan.chunkZ())) {
return true;
}
}
return false;
}
private static String normalize(String structureKey) {
return structureKey == null ? "" : structureKey.trim().toLowerCase(Locale.ROOT);
}
}
@@ -0,0 +1,66 @@
package art.arcane.iris.nativegen.v26_3_R1;
import art.arcane.iris.generation.terrain.IrisMaterialPalette;
import art.arcane.iris.structure.nativegen.IrisNativeStructureDecision;
import art.arcane.iris.structure.placement.IrisStructureStiltSettings;
import art.arcane.volmlib.util.math.RNG;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.WorldgenRandom;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import java.util.List;
import java.util.function.IntBinaryOperator;
public final class NativeStructurePostProcessor {
private NativeStructurePostProcessor() {
}
public static void place(WorldGenLevel world, StructureManager structureManager, ChunkGenerator generator,
WorldgenRandom random, BoundingBox area, ChunkPos chunkPos, String structureId,
StructureStart start, IrisNativeStructureDecision decision,
PaletteBlockResolver paletteBlockResolver,
IntBinaryOperator surfaceHeight) {
IrisStructureStiltSettings stilt = decision.stilt();
NativeStructureVerticalPlacer.ensureMonumentSeaLevelAlignment(start, structureId, decision.yShift(),
generator.getSeaLevel(), area.minY(), area.maxY() + 1);
start.placeInChunk(world, structureManager, generator, random, area, chunkPos);
if (stilt != null) {
NativeStructureFoundationBuilder.placeStilts(world, area, structureId, start, stilt,
paletteBlockResolver, surfaceHeight,
!NativeStructureVegetationClearer.isUndergroundStep(start.getStructure().step()));
}
}
public static void prepareTerrain(WorldGenLevel world, BoundingBox area,
List<NativeStructureTerrainIntegrator.TerrainTarget> targets,
PaletteBlockResolver paletteBlockResolver) {
if (targets == null || targets.isEmpty()) {
return;
}
NativeStructureTerrainIntegrator.SourceTerrainSnapshot sourceTerrain =
NativeStructureTerrainIntegrator.captureSourceTerrain(world, area, targets);
for (NativeStructureTerrainIntegrator.TerrainTarget target : targets) {
NativeStructureTerrainIntegrator.integrateTerrain(world, area, target.structureId(), target.start(),
target.terrain(), paletteBlockResolver, sourceTerrain);
}
for (NativeStructureTerrainIntegrator.TerrainTarget target : targets) {
if (NativeStructureTerrainIntegrator.clearsLegacyTemplateAir(
target.start(), target.terrain())) {
NativeStructureTerrainIntegrator.clearLegacyTemplateAir(
world, area, target.start(), () -> world.getLevel().getStructureTemplateManager());
}
}
NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport(
world, area, targets, () -> world.getLevel().getStructureTemplateManager());
}
@FunctionalInterface
public interface PaletteBlockResolver {
BlockState resolve(IrisMaterialPalette palette, RNG rng, int x, int y, int z);
}
}
@@ -0,0 +1,239 @@
package art.arcane.iris.nativegen.v26_3_R1;
import art.arcane.iris.structure.placement.IrisStructureTerrain;
import art.arcane.iris.structure.placement.IrisStructureTerrainMode;
import art.arcane.iris.spi.IrisLogging;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public final class NativeStructureReferenceEnvelope {
private static final int MAX_REFERENCE_DISTANCE_CHUNKS = 8;
private static final Set<String> WARNED_CLIPPED_TERRAIN = ConcurrentHashMap.newKeySet();
private static final Set<String> WARNED_SKIPPED_CONTENT = ConcurrentHashMap.newKeySet();
private NativeStructureReferenceEnvelope() {
}
public static StructureStart wrap(StructureStart generated, Structure source, int references,
IrisStructureTerrain terrain) {
return wrap(generated, source, references, terrain, null);
}
public static StructureStart wrap(StructureStart generated, Structure source, int references,
IrisStructureTerrain terrain, String structureKey) {
referenceBounds(generated, source, terrain, structureKey);
return new StructureStart(
source,
generated.getChunkPos(),
references,
new PiecesContainer(List.copyOf(generated.getPieces()))
);
}
public static StructureStart wrapForPublication(
StructureStart generated, Structure source, int references,
IrisStructureTerrain terrain, String structureKey) {
try {
return wrap(generated, source, references, terrain, structureKey);
} catch (UnrepresentableContentException exception) {
warnSkippedContent(generated, source, structureKey);
return StructureStart.INVALID_START;
}
}
public static BoundingBox referenceBounds(StructureStart start, Structure source,
IrisStructureTerrain terrain) {
return referenceBounds(start, source, terrain, null);
}
public static BoundingBox referenceBounds(StructureStart start, Structure source,
IrisStructureTerrain terrain,
String structureKey) {
IrisStructureTerrainMode mode = terrain == null
? IrisStructureTerrainMode.PRESERVE : terrain.resolvedMode();
boolean usesEnvelope = mode == IrisStructureTerrainMode.BORE
|| mode == IrisStructureTerrainMode.FORCE_CARVE
|| mode == IrisStructureTerrainMode.VACUUM
|| mode == IrisStructureTerrainMode.FLATTEN
|| mode == IrisStructureTerrainMode.ENCASE;
int horizontalPadding = mode == IrisStructureTerrainMode.VACUUM
? NativeStructureSurfaceFitter.surfaceTerrainRadius()
: usesEnvelope ? Math.max(0, terrain.getHorizontalPadding()) : 0;
BoundingBox content = contentBounds(start);
if (!fitsReferenceRange(start.getChunkPos(), content)) {
throw new UnrepresentableContentException("Native structure content at "
+ start.getChunkPos().x() + "," + start.getChunkPos().z()
+ " exceeds Minecraft's " + MAX_REFERENCE_DISTANCE_CHUNKS
+ "-chunk structure reference range");
}
BoundingBox adjustedEnvelope;
boolean arithmeticClipped = false;
try {
BoundingBox envelope = horizontalPadding == 0
? content
: new BoundingBox(
Math.subtractExact(content.minX(), horizontalPadding),
content.minY(),
Math.subtractExact(content.minZ(), horizontalPadding),
Math.addExact(content.maxX(), horizontalPadding),
content.maxY(),
Math.addExact(content.maxZ(), horizontalPadding)
);
adjustedEnvelope = mode == IrisStructureTerrainMode.SOURCE
? source.adjustBoundingBox(content) : envelope;
adjustedEnvelope = encapsulating(adjustedEnvelope, content);
} catch (ArithmeticException exception) {
adjustedEnvelope = maximumReferenceBounds(start.getChunkPos(), content);
arithmeticClipped = true;
}
BoundingBox referencedEnvelope = clampReferenceRange(start.getChunkPos(), adjustedEnvelope);
if (arithmeticClipped || !sameHorizontalBounds(adjustedEnvelope, referencedEnvelope)) {
warnClippedTerrain(start, source, structureKey);
return referencedEnvelope;
}
return adjustedEnvelope;
}
public static boolean contentFitsReferenceRange(StructureStart start) {
try {
return fitsReferenceRange(start.getChunkPos(), contentBounds(start));
} catch (UnrepresentableContentException exception) {
return false;
}
}
public static BoundingBox contentBounds(StructureStart start) {
return contentBounds(start.getPieces());
}
private static BoundingBox contentBounds(List<StructurePiece> pieces) {
BoundingBox bounds = null;
for (StructurePiece piece : pieces) {
bounds = bounds == null
? copy(piece.getBoundingBox())
: bounds.encapsulate(piece.getBoundingBox());
}
if (bounds == null) {
throw new IllegalStateException("Native jigsaw generated no content pieces");
}
return bounds;
}
private static BoundingBox clampReferenceRange(ChunkPos startChunk, BoundingBox envelope) {
int minX = minimumReferenceBlock(startChunk.x());
int maxX = maximumReferenceBlock(startChunk.x());
int minZ = minimumReferenceBlock(startChunk.z());
int maxZ = maximumReferenceBlock(startChunk.z());
return new BoundingBox(
Math.max(envelope.minX(), minX),
envelope.minY(),
Math.max(envelope.minZ(), minZ),
Math.min(envelope.maxX(), maxX),
envelope.maxY(),
Math.min(envelope.maxZ(), maxZ)
);
}
private static BoundingBox maximumReferenceBounds(ChunkPos startChunk, BoundingBox content) {
return new BoundingBox(
minimumReferenceBlock(startChunk.x()), content.minY(),
minimumReferenceBlock(startChunk.z()),
maximumReferenceBlock(startChunk.x()), content.maxY(),
maximumReferenceBlock(startChunk.z())
);
}
private static boolean fitsReferenceRange(ChunkPos startChunk, BoundingBox content) {
return content.minX() >= minimumReferenceBlock(startChunk.x())
&& content.maxX() <= maximumReferenceBlock(startChunk.x())
&& content.minZ() >= minimumReferenceBlock(startChunk.z())
&& content.maxZ() <= maximumReferenceBlock(startChunk.z());
}
private static int minimumReferenceBlock(int originChunk) {
return checkedReferenceBlock(
((long) originChunk - MAX_REFERENCE_DISTANCE_CHUNKS) << 4, originChunk);
}
private static int maximumReferenceBlock(int originChunk) {
return checkedReferenceBlock(
(((long) originChunk + MAX_REFERENCE_DISTANCE_CHUNKS) << 4) + 15L, originChunk);
}
private static int checkedReferenceBlock(long block, int originChunk) {
if (block < Integer.MIN_VALUE || block > Integer.MAX_VALUE) {
throw new UnrepresentableContentException("Native structure origin chunk " + originChunk
+ " exceeds Minecraft's block coordinate range");
}
return (int) block;
}
private static boolean sameHorizontalBounds(BoundingBox left, BoundingBox right) {
return left.minX() == right.minX()
&& left.minZ() == right.minZ()
&& left.maxX() == right.maxX()
&& left.maxZ() == right.maxZ();
}
private static BoundingBox copy(BoundingBox bounds) {
return new BoundingBox(
bounds.minX(), bounds.minY(), bounds.minZ(),
bounds.maxX(), bounds.maxY(), bounds.maxZ());
}
private static BoundingBox encapsulating(BoundingBox left, BoundingBox right) {
return new BoundingBox(
Math.min(left.minX(), right.minX()),
Math.min(left.minY(), right.minY()),
Math.min(left.minZ(), right.minZ()),
Math.max(left.maxX(), right.maxX()),
Math.max(left.maxY(), right.maxY()),
Math.max(left.maxZ(), right.maxZ())
);
}
private static void warnClippedTerrain(
StructureStart start, Structure source, String structureKey) {
String key = warningKey(source, structureKey);
if (WARNED_CLIPPED_TERRAIN.add(key)) {
IrisLogging.warn("Clipping optional terrain envelope for native structure '"
+ key + "' at " + start.getChunkPos().x() + "," + start.getChunkPos().z()
+ " to Minecraft's " + MAX_REFERENCE_DISTANCE_CHUNKS
+ "-chunk reference range; all generated content remains exactly referenced");
}
}
private static void warnSkippedContent(
StructureStart start, Structure source, String structureKey) {
String key = warningKey(source, structureKey);
if (WARNED_SKIPPED_CONTENT.add(key)) {
IrisLogging.warn("Skipping native structure '" + key + "' at "
+ start.getChunkPos().x() + "," + start.getChunkPos().z()
+ " because its generated content exceeds Minecraft's "
+ MAX_REFERENCE_DISTANCE_CHUNKS
+ "-chunk reference range; no structure start, ownership, references, or blocks were published");
}
}
private static String warningKey(Structure source, String structureKey) {
if (structureKey != null && !structureKey.isBlank()) {
return structureKey.trim().toLowerCase(Locale.ROOT);
}
return source.getClass().getName();
}
public static final class UnrepresentableContentException extends IllegalStateException {
private UnrepresentableContentException(String message) {
super(message);
}
}
}
@@ -0,0 +1,246 @@
package art.arcane.iris.nativegen.v26_3_R1;
import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.structure.nativegen.NativeStructureGenerationPolicy;
import art.arcane.iris.structure.nativegen.NativeStructureOwnershipRecord;
import art.arcane.iris.structure.nativegen.NativeStructureOwnershipStore;
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.structure.nativegen.IrisNativeStructureDecision;
import art.arcane.iris.spi.IrisLogging;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import it.unimi.dsi.fastutil.longs.LongSet;
import net.minecraft.core.Registry;
import net.minecraft.core.SectionPos;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import java.util.ArrayList;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public final class NativeStructureReferenceRepair {
private static final int REFERENCE_DISTANCE_CHUNKS =
NativeStructureOwnershipRecord.MAX_REFERENCE_DISTANCE_CHUNKS;
private static final Set<String> WARNED_POLICY_INVALIDATIONS = ConcurrentHashMap.newKeySet();
private NativeStructureReferenceRepair() {
}
public static void createReferences(Engine engine, WorldGenLevel level,
StructureManager structureManager, ChunkAccess targetChunk) {
ChunkPos target = targetChunk.getPos();
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
ServerLevel serverLevel = level.getLevel();
List<ScannedStart> starts = new ArrayList<>();
for (int originChunkX = target.x() - REFERENCE_DISTANCE_CHUNKS;
originChunkX <= target.x() + REFERENCE_DISTANCE_CHUNKS; originChunkX++) {
for (int originChunkZ = target.z() - REFERENCE_DISTANCE_CHUNKS;
originChunkZ <= target.z() + REFERENCE_DISTANCE_CHUNKS; originChunkZ++) {
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openOriginRuntimeScope(engine, originChunkX, originChunkZ)) {
ChunkAccess originChunk = level.getChunk(
originChunkX, originChunkZ, ChunkStatus.STRUCTURE_STARTS);
for (Map.Entry<Structure, StructureStart> entry : originChunk.getAllStarts().entrySet()) {
ScannedStart start = scanStart(
engine, serverLevel, structureManager, targetChunk, registry,
originChunk, entry.getKey(), entry.getValue());
if (start != null && isTargetRelevant(engine, targetChunk, start)) {
starts.add(start);
}
}
}
}
}
for (ScannedStart start : starts) {
boolean valid;
synchronized (start.originChunk()) {
StructureStart current = start.originChunk().getStartForStructure(start.structure());
valid = current == start.start() && current.isValid();
}
if (!valid) {
continue;
}
structureManager.addReferenceForStructure(
start.structure(),
start.origin().pack(), targetChunk);
}
}
private static GenerationHistoryRuntimeRouter.CoordinateScope openOriginRuntimeScope(
Engine engine,
int chunkX,
int chunkZ
) {
if (!(engine instanceof IrisEngine irisEngine)) {
return null;
}
try {
return irisEngine.openGenerationHistoryCoordinateScope(chunkX << 4, chunkZ << 4);
} catch (IOException failure) {
throw new IllegalStateException("Unable to resolve native structure references for origin "
+ chunkX + "," + chunkZ + " through generation history.", failure);
}
}
private static boolean isTargetRelevant(
Engine engine, ChunkAccess targetChunk, ScannedStart scanned) {
StructureStart start = scanned.start();
Structure structure = scanned.structure();
ChunkPos target = targetChunk.getPos();
if (scanned.ownership() != null) {
return requiresReference(target, scanned.structureKey(), start, scanned.ownership());
}
if (scanned.registered()) {
return requiresNaturalReference(
engine, target, scanned.structureKey(), structure, start);
}
return NativeStructureReferenceEnvelope.contentBounds(start).intersects(
target.getMinBlockX(), target.getMinBlockZ(),
target.getMaxBlockX(), target.getMaxBlockZ());
}
private static ScannedStart scanStart(
Engine engine, ServerLevel level, StructureManager structureManager,
ChunkAccess targetChunk, Registry<Structure> registry,
ChunkAccess originChunk, Structure structure, StructureStart start) {
Identifier identifier = registry.getKey(structure);
String structureKey = identifier == null
? structure.getClass().getName() : identifier.toString();
if (start == null || !start.isValid()) {
return null;
}
if (!NativeStructureReferenceEnvelope.contentFitsReferenceRange(start)) {
if (identifier != null) {
NativeStructureOwnershipStore.discard(
engine, structureKey, start.getChunkPos().x(), start.getChunkPos().z());
}
invalidateStart(structureManager, originChunk, structure, start, targetChunk);
return null;
}
ChunkPos startOrigin = start.getChunkPos();
NativeStructureOwnershipRecord ownership = identifier == null ? null
: NativeStructureOwnershipRecovery.resolve(
engine, level, structureKey, structure, start);
if (ownership != null) {
return new ScannedStart(
originChunk, structure, start, structureKey, ownership, true);
}
if (identifier != null && !naturalPolicyAllows(engine, structureKey, structure)) {
invalidateStart(structureManager, originChunk, structure, start, targetChunk);
if (WARNED_POLICY_INVALIDATIONS.add(structureKey)) {
IrisLogging.warn("Invalidating persisted natural structure start '"
+ structureKey + "' because the current Iris dimension policy disables it");
}
return null;
}
return new ScannedStart(
originChunk, structure, start, structureKey, null, identifier != null);
}
private static boolean naturalPolicyAllows(
Engine engine, String structureKey, Structure structure) {
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(
engine, structureKey,
NativeStructureVegetationClearer.isUndergroundStep(structure.step()));
return naturalDecisionAllows(decision);
}
static boolean naturalDecisionAllows(IrisNativeStructureDecision decision) {
return Objects.requireNonNull(
decision, "Native structure decision must not be null").generate();
}
private static void invalidateStart(
StructureManager structureManager, ChunkAccess originChunk,
Structure structure, StructureStart start, ChunkAccess targetChunk) {
synchronized (originChunk) {
StructureStart current = originChunk.getStartForStructure(structure);
if (current == start && current.isValid()) {
structureManager.setStartForStructure(
structure,
StructureStart.INVALID_START, originChunk);
}
}
if (targetChunk != null) {
removeTargetReference(targetChunk, structure, start.getChunkPos().pack());
}
}
private static void removeTargetReference(
ChunkAccess targetChunk, Structure structure, long origin) {
LongSet current = targetChunk.getAllReferences().get(structure);
if (current == null || !current.contains(origin)) {
return;
}
Map<Structure, LongSet> updated = new HashMap<>(targetChunk.getAllReferences());
LongSet retained = new LongOpenHashSet(current);
retained.remove(origin);
if (retained.isEmpty()) {
updated.remove(structure);
} else {
updated.put(structure, retained);
}
targetChunk.setAllReferences(updated);
}
static boolean requiresNaturalReference(Engine engine, ChunkPos target, String structureKey,
Structure structure, StructureStart start) {
if (target == null || start == null || !start.isValid()) {
return false;
}
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(
engine, structureKey,
NativeStructureVegetationClearer.isUndergroundStep(structure.step()));
if (!decision.generate()) {
return false;
}
BoundingBox referenceBounds = NativeStructureReferenceEnvelope.referenceBounds(
start, structure,
NativeStructureTerrainIntegrator.resolveNativeTerrain(start, decision.terrain()),
structureKey);
return referenceBounds.intersects(
target.getMinBlockX(), target.getMinBlockZ(),
target.getMaxBlockX(), target.getMaxBlockZ());
}
static boolean requiresReference(ChunkPos target, String structureKey,
StructureStart start,
NativeStructureOwnershipRecord ownership) {
if (target == null || start == null || !start.isValid()
|| ownership == null || !ownership.structureKey().equals(structureKey)
|| !ownership.covers(target.x(), target.z())) {
return false;
}
return NativeStructureOwnershipFingerprint.matches(ownership, start);
}
private record ScannedStart(
ChunkAccess originChunk,
Structure structure,
StructureStart start,
String structureKey,
NativeStructureOwnershipRecord ownership,
boolean registered
) {
private ChunkPos origin() {
return ownership == null
? start.getChunkPos()
: new ChunkPos(ownership.originChunkX(), ownership.originChunkZ());
}
}
}
@@ -0,0 +1,386 @@
package art.arcane.iris.nativegen.v26_3_R1;
import com.mojang.datafixers.util.Either;
import net.minecraft.core.BlockPos;
import net.minecraft.resources.Identifier;
import net.minecraft.world.level.ServerLevelAccessor;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.levelgen.structure.ScatteredFeaturePiece;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement;
import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings;
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentPieces;
import net.minecraft.world.level.levelgen.structure.structures.RuinedPortalPiece;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.function.Supplier;
final class NativeStructureReflection {
private NativeStructureReflection() {
}
static Field resolveScatteredHeightPositionField() {
Field resolved = null;
for (Field field : ScatteredFeaturePiece.class.getDeclaredFields()) {
int modifiers = field.getModifiers();
if (Modifier.isStatic(modifiers) || field.getType() != int.class
|| !Modifier.isProtected(modifiers) || Modifier.isFinal(modifiers)) {
continue;
}
if (resolved != null) {
throw new IllegalStateException("ScatteredFeaturePiece has multiple mutable protected int fields");
}
resolved = field;
}
if (resolved == null) {
throw new IllegalStateException("ScatteredFeaturePiece height-position field is missing");
}
if (!resolved.trySetAccessible()) {
throw new IllegalStateException("ScatteredFeaturePiece height-position field is inaccessible");
}
return resolved;
}
static RuinedPortalPiece.VerticalPlacement ruinedPortalVerticalPlacement(RuinedPortalPiece piece) {
try {
return (RuinedPortalPiece.VerticalPlacement) RuinedPortalPlacementAccess.FIELD.get(piece);
} catch (IllegalAccessException error) {
throw new IllegalStateException("Cannot read native ruined portal vertical placement", error);
}
}
private static Field resolveRuinedPortalPlacementField() {
Field resolved = null;
for (Field field : RuinedPortalPiece.class.getDeclaredFields()) {
if (Modifier.isStatic(field.getModifiers())
|| field.getType() != RuinedPortalPiece.VerticalPlacement.class) {
continue;
}
if (resolved != null) {
throw new IllegalStateException("RuinedPortalPiece has multiple vertical-placement fields");
}
resolved = field;
}
if (resolved == null || !resolved.trySetAccessible()) {
throw new IllegalStateException("RuinedPortalPiece vertical-placement field is inaccessible");
}
return resolved;
}
private static Field resolveMonumentChildPiecesField() {
Field resolved = null;
for (Field field : OceanMonumentPieces.MonumentBuilding.class.getDeclaredFields()) {
if (Modifier.isStatic(field.getModifiers()) || field.getType() != List.class) {
continue;
}
if (resolved != null) {
throw new IllegalStateException("Ocean Monument has multiple instance List fields");
}
resolved = field;
}
if (resolved == null) {
throw new IllegalStateException("Ocean Monument child-pieces List field is missing");
}
if (!Modifier.isPrivate(resolved.getModifiers()) || !Modifier.isFinal(resolved.getModifiers())) {
throw new IllegalStateException("Ocean Monument child-pieces field has an unexpected access contract");
}
if (!resolved.trySetAccessible()) {
throw new IllegalStateException("Ocean Monument child-pieces field is inaccessible");
}
return resolved;
}
static Field resolveSinglePoolTemplateField() {
Field resolved = null;
for (Field field : SinglePoolElement.class.getDeclaredFields()) {
if (Modifier.isStatic(field.getModifiers()) || field.getType() != Either.class) {
continue;
}
if (resolved != null) {
throw new IllegalStateException("SinglePoolElement has multiple instance Either fields");
}
resolved = field;
}
if (resolved == null) {
throw new IllegalStateException("SinglePoolElement template Either field is missing");
}
if (!Modifier.isProtected(resolved.getModifiers()) || !Modifier.isFinal(resolved.getModifiers())) {
throw new IllegalStateException("SinglePoolElement template field has an unexpected access contract");
}
if (!resolved.trySetAccessible()) {
throw new IllegalStateException("SinglePoolElement template field is inaccessible");
}
return resolved;
}
static StructureTemplate resolveTemplate(SinglePoolElement element,
Supplier<StructureTemplateManager> templates) {
Object value;
try {
value = SinglePoolTemplateAccess.FIELD.get(element);
} catch (IllegalAccessException error) {
throw new IllegalStateException("Cannot read native structure pool template", error);
}
if (!(value instanceof Either<?, ?> reference)) {
throw new IllegalStateException("Native structure pool template field is not an Either");
}
return resolveTemplateReference(reference, templates);
}
@SuppressWarnings("unchecked")
static List<StructureTemplate.StructureBlockInfo> resolveTemplateBlocks(
StructureTemplate template, StructurePlaceSettings settings,
BlockPos origin) {
Object value;
try {
value = StructureTemplatePalettesAccess.FIELD.get(template);
} catch (IllegalAccessException error) {
throw new IllegalStateException("Cannot read native structure template palettes", error);
}
if (!(value instanceof List<?> paletteValues)) {
throw new IllegalStateException("Native structure template palettes field is not a List");
}
List<StructureTemplate.Palette> palettes =
(List<StructureTemplate.Palette>) paletteValues;
if (palettes.isEmpty()) {
return List.of();
}
return settings.getRandomPalette(palettes, origin).blocks();
}
static StructurePlaceSettings resolvePlacementSettings(
SinglePoolElement element, PoolElementStructurePiece piece,
BoundingBox area) {
LiquidSettings liquidSettings;
try {
liquidSettings = (LiquidSettings) PoolPieceLiquidSettingsAccess.FIELD.get(piece);
} catch (IllegalAccessException error) {
throw new IllegalStateException("Cannot read native structure piece liquid settings", error);
}
try {
return (StructurePlaceSettings) SinglePoolSettingsAccess.METHOD.invoke(
element, piece.getRotation(), area, liquidSettings, false);
} catch (IllegalAccessException | InvocationTargetException error) {
throw new IllegalStateException("Cannot create native structure placement settings", error);
}
}
@SuppressWarnings("unchecked")
static List<StructureTemplate.StructureBlockInfo> processTemplateBlocks(
ServerLevelAccessor world, BlockPos origin, BlockPos referencePos,
StructurePlaceSettings settings,
List<StructureTemplate.StructureBlockInfo> blocks,
StructureTemplate template) {
Method forgeMethod = StructureTemplateProcessingAccess.FORGE_METHOD;
if (forgeMethod == null) {
return StructureTemplate.processBlockInfos(
world, origin, referencePos, settings, blocks);
}
try {
return (List<StructureTemplate.StructureBlockInfo>) forgeMethod.invoke(
null, world, origin, referencePos, settings, blocks, template);
} catch (IllegalAccessException | InvocationTargetException error) {
throw new IllegalStateException("Cannot process native structure template blocks", error);
}
}
static StructureTemplate resolveTemplateReference(Either<?, ?> reference,
Supplier<StructureTemplateManager> templates) {
return reference.map(
location -> resolveNamedTemplate(location, templates),
NativeStructureReflection::requireRuntimeTemplate);
}
private static StructureTemplate resolveNamedTemplate(Object value,
Supplier<StructureTemplateManager> templates) {
if (!(value instanceof Identifier identifier)) {
throw new IllegalStateException("Native structure pool template identifier is "
+ (value == null ? "null" : value.getClass().getName()));
}
return Objects.requireNonNull(templates == null ? null : templates.get(),
"Native structure template manager is unavailable").getOrCreate(identifier);
}
private static StructureTemplate requireRuntimeTemplate(Object value) {
if (!(value instanceof StructureTemplate template)) {
throw new IllegalStateException("Native runtime structure pool template is "
+ (value == null ? "null" : value.getClass().getName()));
}
return template;
}
private static Field resolveStructureTemplatePalettesField() {
Field resolved = null;
for (Field field : StructureTemplate.class.getDeclaredFields()) {
if (Modifier.isStatic(field.getModifiers()) || field.getType() != List.class
|| !isPaletteList(field.getGenericType())) {
continue;
}
if (resolved != null) {
throw new IllegalStateException("StructureTemplate has multiple palette List fields");
}
resolved = field;
}
if (resolved == null) {
throw new IllegalStateException("StructureTemplate palette List field is missing");
}
if (!Modifier.isFinal(resolved.getModifiers())) {
throw new IllegalStateException("StructureTemplate palette field has an unexpected access contract");
}
if (!resolved.trySetAccessible()) {
throw new IllegalStateException("StructureTemplate palette field is inaccessible");
}
return resolved;
}
private static boolean isPaletteList(Type type) {
if (!(type instanceof ParameterizedType parameterized)) {
return false;
}
Type[] arguments = parameterized.getActualTypeArguments();
return arguments.length == 1 && arguments[0] == StructureTemplate.Palette.class;
}
private static Method resolveSinglePoolSettingsMethod() {
Method resolved = null;
Class<?>[] parameterTypes = {
Rotation.class, BoundingBox.class, LiquidSettings.class, boolean.class
};
for (Method method : SinglePoolElement.class.getDeclaredMethods()) {
if (Modifier.isStatic(method.getModifiers())
|| method.getReturnType() != StructurePlaceSettings.class
|| !Arrays.equals(method.getParameterTypes(), parameterTypes)) {
continue;
}
if (resolved != null) {
throw new IllegalStateException("SinglePoolElement has multiple placement-settings methods");
}
resolved = method;
}
if (resolved == null) {
throw new IllegalStateException("SinglePoolElement placement-settings method is missing");
}
if (!Modifier.isProtected(resolved.getModifiers())) {
throw new IllegalStateException("SinglePoolElement placement-settings method has an unexpected access contract");
}
if (!resolved.trySetAccessible()) {
throw new IllegalStateException("SinglePoolElement placement-settings method is inaccessible");
}
return resolved;
}
private static Field resolvePoolPieceLiquidSettingsField() {
Field resolved = null;
for (Field field : PoolElementStructurePiece.class.getDeclaredFields()) {
if (Modifier.isStatic(field.getModifiers())
|| field.getType() != LiquidSettings.class) {
continue;
}
if (resolved != null) {
throw new IllegalStateException("PoolElementStructurePiece has multiple liquid-settings fields");
}
resolved = field;
}
if (resolved == null) {
throw new IllegalStateException("PoolElementStructurePiece liquid-settings field is missing");
}
if (!Modifier.isPrivate(resolved.getModifiers())
|| !Modifier.isFinal(resolved.getModifiers())) {
throw new IllegalStateException("PoolElementStructurePiece liquid-settings field has an unexpected access contract");
}
if (!resolved.trySetAccessible()) {
throw new IllegalStateException("PoolElementStructurePiece liquid-settings field is inaccessible");
}
return resolved;
}
private static Method resolveForgeTemplateProcessingMethod() {
Class<?>[] parameterTypes = {
ServerLevelAccessor.class, BlockPos.class, BlockPos.class,
StructurePlaceSettings.class, List.class, StructureTemplate.class
};
Method resolved = null;
for (Method method : StructureTemplate.class.getDeclaredMethods()) {
if (!Modifier.isPublic(method.getModifiers())
|| !Modifier.isStatic(method.getModifiers())
|| method.getReturnType() != List.class
|| !Arrays.equals(method.getParameterTypes(), parameterTypes)) {
continue;
}
if (resolved != null) {
throw new IllegalStateException(
"StructureTemplate has multiple Forge block-processing methods");
}
resolved = method;
}
return resolved;
}
static final class MonumentChildPiecesAccess {
static final Field FIELD = resolveMonumentChildPiecesField();
private MonumentChildPiecesAccess() {
}
}
private static final class RuinedPortalPlacementAccess {
private static final Field FIELD = resolveRuinedPortalPlacementField();
private RuinedPortalPlacementAccess() {
}
}
static final class ScatteredHeightPositionAccess {
static final Field FIELD = resolveScatteredHeightPositionField();
private ScatteredHeightPositionAccess() {
}
}
private static final class SinglePoolTemplateAccess {
private static final Field FIELD = resolveSinglePoolTemplateField();
private SinglePoolTemplateAccess() {
}
}
private static final class StructureTemplatePalettesAccess {
private static final Field FIELD = resolveStructureTemplatePalettesField();
private StructureTemplatePalettesAccess() {
}
}
private static final class SinglePoolSettingsAccess {
private static final Method METHOD = resolveSinglePoolSettingsMethod();
private SinglePoolSettingsAccess() {
}
}
private static final class PoolPieceLiquidSettingsAccess {
private static final Field FIELD = resolvePoolPieceLiquidSettingsField();
private PoolPieceLiquidSettingsAccess() {
}
}
private static final class StructureTemplateProcessingAccess {
private static final Method FORGE_METHOD = resolveForgeTemplateProcessingMethod();
private StructureTemplateProcessingAccess() {
}
}
}
@@ -0,0 +1,147 @@
package art.arcane.iris.nativegen.v26_3_R1;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.structure.nativegen.NativeStructureGenerationPolicy;
import art.arcane.iris.structure.nativegen.NativeStructureOwnershipRecord;
import art.arcane.iris.structure.nativegen.NativeStructureOwnershipStore;
import art.arcane.iris.structure.nativegen.NativeStructurePlacementPlanner;
import art.arcane.iris.structure.nativegen.NativeStructureStartPlan;
import art.arcane.iris.structure.nativegen.NativeStructureGenerationStatus;
import art.arcane.iris.structure.nativegen.NativeStructureSuppression;
import art.arcane.iris.spi.IrisLogging;
import net.minecraft.core.Holder;
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.world.level.Level;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.biome.BiomeSource;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public final class NativeStructureStartInjector {
private static final Set<String> WARNED_DUPLICATE_STRUCTURES = ConcurrentHashMap.newKeySet();
private NativeStructureStartInjector() {
}
public static Map<Structure, NativeStructureStartPlan> inject(InjectionContext context) {
Objects.requireNonNull(context, "Native structure injection context must not be null");
ChunkAccess chunk = context.chunk();
Registry<Structure> registry = context.registryAccess().lookupOrThrow(Registries.STRUCTURE);
SectionPos section = SectionPos.bottomOf(chunk);
Map<Structure, NativeStructureStartPlan> configuredStarts = new LinkedHashMap<>();
for (NativeStructureStartPlan plan : NativeStructurePlacementPlanner.plansAt(
context.engine(), chunk.getPos().x(), chunk.getPos().z())) {
Identifier identifier = Identifier.tryParse(plan.source().getStructure());
if (identifier == null) {
throw new IllegalStateException("Configured native structure key is invalid: "
+ plan.source().getStructure());
}
Structure structure = registry.getValue(identifier);
if (structure == null) {
throw new IllegalStateException("Configured native structure is not registered: " + identifier);
}
Holder<Structure> holder = registry.wrapAsHolder(structure);
if (configuredStarts.containsKey(structure)) {
if (WARNED_DUPLICATE_STRUCTURES.add(identifier.toString())) {
IrisLogging.warn("Ignoring duplicate native structure placements for '"
+ identifier + "'; the first deterministic candidate owns each chunk start");
}
continue;
}
StructureStart existing = context.structureManager().getStartForStructure(
structure, chunk);
boolean replacement = plan.placement().getNativeSuppression()
== NativeStructureSuppression.REPLACE_SOURCE;
if (!replacement && existing != null && existing.isValid()) {
NativeStructureGenerationStatus sourceStatus = NativeStructureGenerationPolicy.resolve(
context.engine(), identifier.toString(),
NativeStructureVegetationClearer.isUndergroundStep(structure.step())).status();
replacement = sourceStatus == NativeStructureGenerationStatus.REPLACED_BY_IRIS;
}
if (!replacement && existing != null && existing.isValid()) {
continue;
}
int references = existing != null && existing.isValid() ? existing.getReferences() : 0;
NativeStructureFactory.GenerationContext generationContext =
new NativeStructureFactory.GenerationContext(
context.registryAccess(),
context.generator(),
context.biomeSource(),
context.structureState().randomState(),
context.templateManager(),
context.structureState().getLevelSeed(),
context.levelKey(),
chunk,
biome -> true,
context.generator().getSeaLevel(),
(x, z) -> Engine.hostHeight(context.engine(), x, z, true)
+ context.engine().getMinHeight()
);
StructureStart generated = NativeStructureFactory.generate(
generationContext, holder, plan, references);
if (!isUsableGeneratedStart(generated)) {
if (replacement) {
context.structureManager().setStartForStructure(
structure, StructureStart.INVALID_START, chunk);
}
NativeStructureOwnershipStore.discard(
context.engine(), identifier.toString(),
chunk.getPos().x(), chunk.getPos().z());
continue;
}
BoundingBox referenceBounds = NativeStructureReferenceEnvelope.referenceBounds(
generated, structure, plan.placement().resolvedTerrain(), identifier.toString());
NativeStructureOwnershipRecord ownership = NativeStructureOwnershipFingerprint.capture(
identifier.toString(), generated, plan, referenceBounds);
NativeStructureOwnershipStore.record(context.engine(), ownership);
try {
context.structureManager().setStartForStructure(
structure, generated, chunk);
} catch (RuntimeException | Error publicationError) {
try {
NativeStructureOwnershipStore.discard(
context.engine(), identifier.toString(),
chunk.getPos().x(), chunk.getPos().z());
} catch (RuntimeException | Error cleanupError) {
publicationError.addSuppressed(cleanupError);
}
throw publicationError;
}
configuredStarts.put(structure, plan);
}
return Map.copyOf(configuredStarts);
}
static boolean isUsableGeneratedStart(StructureStart start) {
return start != null && start.isValid();
}
public record InjectionContext(
Engine engine,
RegistryAccess registryAccess,
ChunkGeneratorStructureState structureState,
StructureManager structureManager,
ChunkAccess chunk,
StructureTemplateManager templateManager,
ResourceKey<Level> levelKey,
ChunkGenerator generator,
BiomeSource biomeSource
) {
}
}
@@ -0,0 +1,305 @@
package art.arcane.iris.nativegen.v26_3_R1;
import art.arcane.iris.structure.placement.IrisStructureTerrainMode;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
final class NativeStructureSurfaceSupportBuilder {
private static final int MAX_BATCH_CELLS = 524_288;
private static final int MAX_BRIDGE_SPAN = 2;
private NativeStructureSurfaceSupportBuilder() {
}
static Set<Long> bridgeRigidPieceSupport(
WorldGenLevel world, BoundingBox area,
List<NativeStructureTerrainIntegrator.TerrainTarget> targets,
Supplier<StructureTemplateManager> templates) {
if (targets == null || targets.isEmpty()) {
return Set.of();
}
Map<Long, BlockState> planned;
try {
BatchCellBudget budget = new BatchCellBudget();
List<SupportRequest> requests = new ArrayList<>();
Set<StructureStart> seenStarts = Collections.newSetFromMap(
new IdentityHashMap<>());
for (NativeStructureTerrainIntegrator.TerrainTarget target : targets) {
if (target == null
|| !NativeStructureSurfaceFitter.requiresSurfaceTerrain(target)
|| target.terrain().resolvedMode() == IrisStructureTerrainMode.FLATTEN
&& !NativeStructureSurfaceFitter.hasExposedFlattenPiece(
target.start(), (x, z) -> world.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z) - 1,
target.terrain().resolvedFlattenRange())
|| !seenStarts.add(target.start())) {
continue;
}
collectTargetRequests(
world, area, target, templates, budget, requests);
}
planned = planWrites(world, area, requests, budget);
} catch (BatchBudgetExceeded ignored) {
return Set.of();
}
if (planned.isEmpty()) {
return Set.of();
}
List<Map.Entry<Long, BlockState>> ordered = new ArrayList<>(planned.entrySet());
ordered.sort(Map.Entry.comparingByKey());
Set<Long> written = new HashSet<>();
for (Map.Entry<Long, BlockState> entry : ordered) {
BlockPos position = BlockPos.of(entry.getKey());
BlockState existing = world.getBlockState(position);
if (!existing.isSolid()
&& !NativeStructureVegetationClearer.isTreeBlock(existing)
&& existing.getFluidState().isEmpty()
&& world.setBlock(position, entry.getValue(), 2)) {
written.add(entry.getKey());
}
}
return Set.copyOf(written);
}
private static void collectTargetRequests(
WorldGenLevel world, BoundingBox area,
NativeStructureTerrainIntegrator.TerrainTarget target,
Supplier<StructureTemplateManager> templates,
BatchCellBudget budget, List<SupportRequest> requests) {
if (!NativeStructureSurfaceFitter.requiresSurfaceTerrain(target)) {
return;
}
List<RigidAnchor> anchors = rigidAnchors(target.start());
if (anchors.size() < 2) {
return;
}
BlockPos referencePos = referencePosition(target.start());
for (RigidAnchor upper : anchors) {
Map<Long, Integer> lowerMeets = candidateLowerMeets(
area, upper, anchors, budget);
if (lowerMeets.isEmpty()) {
continue;
}
NativeStructureTemplateOccupancy.OccupancyResult occupancy =
NativeStructureTemplateOccupancy.resolve(
world, upper.piece(), referencePos, area, templates,
position -> retain(position, area, lowerMeets), budget::consume);
if (!occupancy.resolved()) {
continue;
}
Map<Long, NativeStructureTemplateOccupancy.LowestCell> lowest =
NativeStructureTemplateOccupancy.lowestCells(occupancy.cells());
for (Map.Entry<Long, NativeStructureTemplateOccupancy.LowestCell> entry
: lowest.entrySet()) {
NativeStructureTemplateOccupancy.LowestCell cell = entry.getValue();
Integer lowerMeet = lowerMeets.get(entry.getKey());
if (lowerMeet == null || cell.occupancy().blocker()
|| !NativeStructureTemplateOccupancy.isSolidBase(
cell.occupancy().state())
|| cell.y() - lowerMeet < 2
|| cell.y() - lowerMeet > MAX_BRIDGE_SPAN) {
continue;
}
budget.consume(1);
requests.add(new SupportRequest(
cell.x(), cell.z(), lowerMeet, cell.y()));
}
}
}
private static List<RigidAnchor> rigidAnchors(StructureStart start) {
List<RigidAnchor> anchors = new ArrayList<>();
for (StructurePiece piece : start.getPieces()) {
if (!(piece instanceof PoolElementStructurePiece poolPiece)
|| poolPiece.getElement().getProjection()
!= StructureTemplatePool.Projection.RIGID) {
continue;
}
BoundingBox bounds = poolPiece.getBoundingBox();
int meetY = bounds.minY() + poolPiece.getGroundLevelDelta() - 1;
anchors.add(new RigidAnchor(poolPiece, bounds, meetY));
}
return List.copyOf(anchors);
}
private static Map<Long, Integer> candidateLowerMeets(
BoundingBox area, RigidAnchor upper, List<RigidAnchor> anchors,
BatchCellBudget budget) {
Map<Long, Integer> lowerMeets = new HashMap<>();
for (RigidAnchor lower : anchors) {
budget.consume(1);
int span = upper.meetY() - lower.meetY();
if (lower.piece() == upper.piece() || span < 1
|| span > MAX_BRIDGE_SPAN
|| lower.meetY() < area.minY() || lower.meetY() > area.maxY()) {
continue;
}
int minimumX = Math.max(area.minX(),
Math.max(upper.bounds().minX(), lower.bounds().minX()));
int maximumX = Math.min(area.maxX(),
Math.min(upper.bounds().maxX(), lower.bounds().maxX()));
int minimumZ = Math.max(area.minZ(),
Math.max(upper.bounds().minZ(), lower.bounds().minZ()));
int maximumZ = Math.min(area.maxZ(),
Math.min(upper.bounds().maxZ(), lower.bounds().maxZ()));
if (minimumX > maximumX || minimumZ > maximumZ) {
continue;
}
for (int x = minimumX; x <= maximumX; x++) {
for (int z = minimumZ; z <= maximumZ; z++) {
budget.consume(1);
lowerMeets.merge(columnKey(x, z), lower.meetY(), Math::max);
}
}
}
return lowerMeets;
}
private static boolean retain(
BlockPos position, BoundingBox area, Map<Long, Integer> lowerMeets) {
if (!area.isInside(position)) {
return false;
}
Integer lowerMeet = lowerMeets.get(columnKey(position.getX(), position.getZ()));
return lowerMeet != null && position.getY() <= lowerMeet + MAX_BRIDGE_SPAN;
}
private static Map<Long, BlockState> planWrites(
WorldGenLevel world, BoundingBox area,
List<SupportRequest> requests, BatchCellBudget budget) {
List<SupportRequest> eligible = new ArrayList<>();
Set<MaterialKey> materialKeys = new HashSet<>();
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
for (SupportRequest request : requests) {
BlockState lowerTerrain = world.getBlockState(position.set(
request.x(), request.lowerMeetY(), request.z()));
if (!isTerrainSupport(lowerTerrain)
|| !gapIsClear(world, area, position, request, budget)) {
continue;
}
eligible.add(request);
materialKeys.add(new MaterialKey(
request.x(), request.z(), request.lowerMeetY()));
}
Map<MaterialKey, BlockState> materials = new HashMap<>();
for (MaterialKey key : materialKeys) {
materials.put(key, resolveSupportState(
world, position, key.x(), key.z(), key.lowerMeetY(), budget));
}
Map<Long, BlockState> planned = new HashMap<>();
Set<Long> conflicts = new HashSet<>();
eligible.sort(Comparator.comparingInt(SupportRequest::x)
.thenComparingInt(SupportRequest::z)
.thenComparingInt(SupportRequest::lowerMeetY)
.thenComparingInt(SupportRequest::baseY));
for (SupportRequest request : eligible) {
MaterialKey materialKey = new MaterialKey(
request.x(), request.z(), request.lowerMeetY());
BlockState material = materials.get(materialKey);
for (int y = request.lowerMeetY() + 1; y < request.baseY(); y++) {
budget.consume(1);
long positionKey = BlockPos.asLong(request.x(), y, request.z());
if (conflicts.contains(positionKey)) {
continue;
}
BlockState existing = planned.putIfAbsent(positionKey, material);
if (existing != null && !existing.equals(material)) {
planned.remove(positionKey);
conflicts.add(positionKey);
}
}
}
return planned;
}
private static boolean gapIsClear(
WorldGenLevel world, BoundingBox area, BlockPos.MutableBlockPos position,
SupportRequest request, BatchCellBudget budget) {
for (int y = request.lowerMeetY() + 1; y < request.baseY(); y++) {
budget.consume(1);
if (!area.isInside(position.set(request.x(), y, request.z()))) {
return false;
}
BlockState state = world.getBlockState(position);
if (state.isSolid() || NativeStructureVegetationClearer.isTreeBlock(state)
|| !state.getFluidState().isEmpty()) {
return false;
}
}
return true;
}
private static BlockState resolveSupportState(
WorldGenLevel world, BlockPos.MutableBlockPos position,
int x, int z, int lowerMeetY, BatchCellBudget budget) {
budget.consume(1);
BlockState below = world.getBlockState(position.set(x, lowerMeetY - 1, z));
if (isTerrainSupport(below)) {
return below;
}
return world.getBlockState(position.set(x, lowerMeetY, z));
}
private static boolean isTerrainSupport(BlockState state) {
return state.isSolid()
&& !NativeStructureVegetationClearer.isTreeBlock(state)
&& state.getFluidState().isEmpty();
}
private static BlockPos referencePosition(StructureStart start) {
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
BlockPos center = bounds.getCenter();
return new BlockPos(center.getX(), bounds.minY(), center.getZ());
}
private static long columnKey(int x, int z) {
return (long) x << 32 ^ z & 0xffffffffL;
}
private record RigidAnchor(
PoolElementStructurePiece piece, BoundingBox bounds, int meetY) {
}
private record SupportRequest(int x, int z, int lowerMeetY, int baseY) {
}
private record MaterialKey(int x, int z, int lowerMeetY) {
}
private static final class BatchCellBudget {
private int consumed;
private void consume(int amount) {
if (amount < 0 || consumed > MAX_BATCH_CELLS - amount) {
throw BatchBudgetExceeded.INSTANCE;
}
consumed += amount;
}
}
private static final class BatchBudgetExceeded extends RuntimeException {
private static final BatchBudgetExceeded INSTANCE = new BatchBudgetExceeded();
private BatchBudgetExceeded() {
super(null, null, false, false);
}
}
}
@@ -0,0 +1,165 @@
package art.arcane.iris.nativegen.v26_3_R1;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
import net.minecraft.world.level.levelgen.structure.pools.EmptyPoolElement;
import net.minecraft.world.level.levelgen.structure.pools.ListPoolElement;
import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
final class NativeStructureTemplateOccupancy {
private static final OccupancyCell BLOCKER = new OccupancyCell(null, true);
private NativeStructureTemplateOccupancy() {
}
static OccupancyResult resolve(
WorldGenLevel world, PoolElementStructurePiece piece,
BlockPos referencePosition, BoundingBox processingArea,
Supplier<StructureTemplateManager> templates,
PositionFilter filter, CellBudget budget) {
List<SinglePoolElement> leaves = new ArrayList<>();
if (!flattenSingles(piece.getElement(), leaves)) {
return new OccupancyResult(false, Map.of());
}
List<LeafPlacement> placements = new ArrayList<>(leaves.size());
Map<Long, OccupancyCell> occupancy = new HashMap<>();
for (SinglePoolElement leaf : leaves) {
StructurePlaceSettings settings = NativeStructureReflection.resolvePlacementSettings(
leaf, piece, processingArea);
StructureTemplate template = NativeStructureReflection.resolveTemplate(leaf, templates);
List<StructureTemplate.StructureBlockInfo> rawBlocks =
NativeStructureReflection.resolveTemplateBlocks(
template, settings, piece.getPosition());
budget.consume(rawBlocks.size());
placements.add(new LeafPlacement(settings, rawBlocks, template));
for (StructureTemplate.StructureBlockInfo raw : rawBlocks) {
BlockPos position = piece.getPosition().offset(
StructureTemplate.calculateRelativePosition(settings, raw.pos()));
if (filter.retain(position)) {
occupancy.put(position.asLong(), BLOCKER);
}
}
}
for (LeafPlacement placement : placements) {
List<StructureTemplate.StructureBlockInfo> processed =
NativeStructureReflection.processTemplateBlocks(
world, piece.getPosition(), referencePosition,
placement.settings(), placement.rawBlocks(), placement.template());
budget.consume(processed.size());
for (StructureTemplate.StructureBlockInfo block : processed) {
BlockPos position = block.pos();
if (!filter.retain(position)) {
continue;
}
BlockState state = block.state()
.mirror(placement.settings().getMirror())
.rotate(placement.settings().getRotation());
occupancy.put(position.asLong(), new OccupancyCell(state, false));
}
}
return new OccupancyResult(true, occupancy);
}
static Map<Long, LowestCell> lowestCells(Map<Long, OccupancyCell> occupancy) {
Map<Long, LowestCell> lowest = new HashMap<>();
for (Map.Entry<Long, OccupancyCell> entry : occupancy.entrySet()) {
OccupancyCell cell = entry.getValue();
BlockPos position = BlockPos.of(entry.getKey());
long column = columnKey(position.getX(), position.getZ());
LowestCell current = lowest.get(column);
if (current == null || position.getY() < current.y()) {
lowest.put(column, new LowestCell(
position.getX(), position.getY(), position.getZ(), cell));
}
}
return lowest;
}
static Map<Long, LowestCell> lowestProcessedSolidCells(Map<Long, OccupancyCell> occupancy) {
Map<Long, LowestCell> lowest = new HashMap<>();
for (Map.Entry<Long, OccupancyCell> entry : occupancy.entrySet()) {
OccupancyCell cell = entry.getValue();
if (cell.blocker() || !isSolidBase(cell.state())) {
continue;
}
BlockPos position = BlockPos.of(entry.getKey());
long column = columnKey(position.getX(), position.getZ());
LowestCell current = lowest.get(column);
if (current == null || position.getY() < current.y()) {
lowest.put(column, new LowestCell(
position.getX(), position.getY(), position.getZ(), cell));
}
}
return lowest;
}
static boolean isSolidBase(BlockState state) {
return state != null && state.isSolid()
&& !state.is(Blocks.STRUCTURE_VOID)
&& !state.is(Blocks.JIGSAW)
&& state.getFluidState().isEmpty();
}
static long columnKey(int x, int z) {
return (long) x << 32 ^ z & 0xffffffffL;
}
private static boolean flattenSingles(
StructurePoolElement element, List<SinglePoolElement> leaves) {
if (element instanceof ListPoolElement listElement) {
for (StructurePoolElement child : listElement.getElements()) {
if (!flattenSingles(child, leaves)) {
return false;
}
}
return true;
}
if (element == EmptyPoolElement.INSTANCE) {
return true;
}
if (element instanceof SinglePoolElement singleElement) {
leaves.add(singleElement);
return true;
}
return false;
}
@FunctionalInterface
interface PositionFilter {
boolean retain(BlockPos position);
}
@FunctionalInterface
interface CellBudget {
void consume(int amount);
}
record OccupancyResult(boolean resolved, Map<Long, OccupancyCell> cells) {
}
record OccupancyCell(BlockState state, boolean blocker) {
}
record LowestCell(int x, int y, int z, OccupancyCell occupancy) {
}
private record LeafPlacement(
StructurePlaceSettings settings,
List<StructureTemplate.StructureBlockInfo> rawBlocks,
StructureTemplate template) {
}
}
@@ -0,0 +1,228 @@
package art.arcane.iris.nativegen.v26_3_R1;
import com.mojang.datafixers.util.Pair;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.Registry;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.pools.EmptyPoolElement;
import net.minecraft.world.level.levelgen.structure.pools.FeaturePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.ListPoolElement;
import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
import net.minecraft.world.level.levelgen.structure.pools.alias.DirectPoolAlias;
import net.minecraft.world.level.levelgen.structure.pools.alias.PoolAliasBinding;
import net.minecraft.world.level.levelgen.structure.pools.alias.RandomGroupPoolAlias;
import net.minecraft.world.level.levelgen.structure.pools.alias.RandomPoolAlias;
import net.minecraft.world.level.levelgen.structure.structures.JigsawStructure;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import java.util.Collections;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
public final class NativeStructureTemplatePoolBounds {
private NativeStructureTemplatePoolBounds() {
}
public static int sourceHorizontalSpan(RegistryAccess registryAccess,
StructureTemplateManager templateManager,
JigsawStructure source) {
Registry<StructureTemplatePool> pools = registryAccess.lookupOrThrow(Registries.TEMPLATE_POOL);
return sourceHorizontalSpan(
templateManager,
source.getStartPool(),
source.getPoolAliases(),
key -> pools.getValue(key.identifier()));
}
public static int horizontalSpan(RegistryAccess registryAccess,
StructureTemplateManager templateManager,
String templatePoolKey) {
Identifier identifier = Identifier.tryParse(templatePoolKey);
if (identifier == null) {
throw new IllegalArgumentException("Invalid registered template pool key: " + templatePoolKey);
}
Registry<StructureTemplatePool> pools = registryAccess.lookupOrThrow(Registries.TEMPLATE_POOL);
StructureTemplatePool pool = pools.getValue(identifier);
if (pool == null) {
throw new IllegalArgumentException("Registered template pool does not exist: " + templatePoolKey);
}
return horizontalSpan(pool, templateManager);
}
public static int horizontalSpan(RegistryAccess registryAccess,
StructureTemplateManager templateManager,
JigsawStructure source,
String templatePoolKey) {
Identifier identifier = Identifier.tryParse(templatePoolKey);
if (identifier == null) {
throw new IllegalArgumentException("Invalid registered template pool key: " + templatePoolKey);
}
Registry<StructureTemplatePool> pools = registryAccess.lookupOrThrow(Registries.TEMPLATE_POOL);
ResourceKey<StructureTemplatePool> startPoolKey = ResourceKey.create(
Registries.TEMPLATE_POOL, identifier);
StructureTemplatePool startPool = pools.getValue(identifier);
if (startPool == null) {
throw new IllegalArgumentException("Registered template pool does not exist: " + templatePoolKey);
}
return sourceHorizontalSpan(
templateManager,
startPool,
startPoolKey,
source.getPoolAliases(),
key -> pools.getValue(key.identifier()));
}
static int sourceHorizontalSpan(StructureTemplateManager templateManager,
Holder<StructureTemplatePool> startPool,
List<PoolAliasBinding> aliases,
Function<ResourceKey<StructureTemplatePool>, StructureTemplatePool> poolLookup) {
return sourceHorizontalSpan(templateManager, startPool.value(),
startPool.unwrapKey().orElse(null), aliases, poolLookup);
}
static int sourceHorizontalSpan(StructureTemplateManager templateManager,
StructureTemplatePool startPool,
ResourceKey<StructureTemplatePool> startPoolKey,
List<PoolAliasBinding> aliases,
Function<ResourceKey<StructureTemplatePool>, StructureTemplatePool> poolLookup) {
int maximumSpan = directHorizontalSpan(startPool, templateManager);
if (startPoolKey == null) {
return maximumSpan;
}
Set<ResourceKey<StructureTemplatePool>> targets = new HashSet<>();
for (PoolAliasBinding binding : aliases) {
collectTargets(binding, startPoolKey, targets);
}
for (ResourceKey<StructureTemplatePool> target : targets) {
StructureTemplatePool targetPool = poolLookup.apply(target);
if (targetPool == null) {
throw new IllegalStateException("Jigsaw start-pool alias target does not exist: "
+ target.identifier());
}
maximumSpan = Math.max(maximumSpan, directHorizontalSpan(targetPool, templateManager));
}
return maximumSpan;
}
private static void collectTargets(PoolAliasBinding binding,
ResourceKey<StructureTemplatePool> startPoolKey,
Set<ResourceKey<StructureTemplatePool>> targets) {
if (binding instanceof DirectPoolAlias direct) {
if (direct.alias().equals(startPoolKey)) {
targets.add(direct.target());
}
return;
}
if (binding instanceof RandomPoolAlias random) {
if (random.alias().equals(startPoolKey)) {
random.targets().unwrap().forEach(entry -> targets.add(entry.value()));
}
return;
}
if (binding instanceof RandomGroupPoolAlias group) {
group.groups().unwrap().forEach(entry -> entry.value().forEach(
nested -> collectTargets(nested, startPoolKey, targets)));
return;
}
throw new IllegalStateException("Unsupported jigsaw pool alias type: "
+ binding.getClass().getName());
}
static int horizontalSpan(StructureTemplatePool pool,
StructureTemplateManager templateManager) {
Set<StructureTemplatePool> visited = Collections.newSetFromMap(new IdentityHashMap<>());
return horizontalSpan(pool, templateManager, visited);
}
private static int horizontalSpan(StructureTemplatePool pool,
StructureTemplateManager templateManager,
Set<StructureTemplatePool> visited) {
if (!visited.add(pool)) {
return 0;
}
int maximumSpan = directHorizontalSpan(pool, templateManager);
Holder<StructureTemplatePool> fallback = pool.getFallback();
if (fallback == null || !fallback.isBound()) {
throw new IllegalStateException("Template pool has an unresolved fallback");
}
return Math.max(maximumSpan, horizontalSpan(fallback.value(), templateManager, visited));
}
private static int directHorizontalSpan(StructureTemplatePool pool,
StructureTemplateManager templateManager) {
int maximumSpan = 0;
for (Pair<StructurePoolElement, Integer> entry : pool.getTemplates()) {
StructurePoolElement element = entry.getFirst();
validateElement(element, templateManager);
maximumSpan = Math.max(maximumSpan, horizontalSpan(element, templateManager));
}
return maximumSpan;
}
private static void validateElement(StructurePoolElement element,
StructureTemplateManager templateManager) {
if (element == EmptyPoolElement.INSTANCE || element instanceof FeaturePoolElement) {
return;
}
if (element instanceof SinglePoolElement single) {
Identifier template;
try {
template = single.getTemplateLocation();
} catch (RuntimeException inlineTemplate) {
return;
}
if (templateManager.get(template).isEmpty()) {
throw new IllegalStateException("Template pool element does not resolve a structure template: "
+ template);
}
return;
}
if (element instanceof ListPoolElement list) {
for (StructurePoolElement child : list.getElements()) {
validateElement(child, templateManager);
}
return;
}
}
private static int horizontalSpan(StructurePoolElement element,
StructureTemplateManager templateManager) {
if (element == EmptyPoolElement.INSTANCE) {
return 0;
}
long maximumSpan = 0L;
for (Rotation rotation : Rotation.values()) {
BoundingBox box;
try {
box = element.getBoundingBox(templateManager, BlockPos.ZERO, rotation);
} catch (RuntimeException | LinkageError error) {
throw new IllegalStateException("Template pool element "
+ element.getClass().getName()
+ " could not provide bounded geometry for rotation " + rotation, error);
}
if (box == null) {
throw new IllegalStateException("Template pool element "
+ element.getClass().getName()
+ " returned no bounded geometry for rotation " + rotation);
}
long xSpan = (long) box.maxX() - box.minX() + 1L;
long zSpan = (long) box.maxZ() - box.minZ() + 1L;
maximumSpan = Math.max(maximumSpan, Math.max(xSpan, zSpan));
}
if (maximumSpan < 0L || maximumSpan > Integer.MAX_VALUE) {
throw new IllegalStateException("Template pool element has an unbounded horizontal span");
}
return (int) maximumSpan;
}
}
@@ -0,0 +1,224 @@
package art.arcane.iris.nativegen.v26_3_R1;
import com.mojang.datafixers.util.Pair;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderSet;
import net.minecraft.core.SectionPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureCheckResult;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.placement.ConcentricRingsStructurePlacement;
import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement;
import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
public final class NativeStructureVanillaLocator {
private NativeStructureVanillaLocator() {
}
public static Candidate predict(ServerLevel level, HolderSet<Structure> holders,
BlockPos origin, int radius, boolean findUnexplored) {
if (SharedConstants.DEBUG_DISABLE_FEATURES
|| !level.getServer().getWorldGenSettings().options().generateStructures()) {
return null;
}
ChunkGeneratorStructureState state = level.getChunkSource().getGeneratorState();
Map<StructurePlacement, Set<Holder<Structure>>> byPlacement = new LinkedHashMap<>();
for (Holder<Structure> holder : holders) {
for (StructurePlacement placement : state.getPlacementsForStructure(holder)) {
byPlacement.computeIfAbsent(placement, ignored -> new LinkedHashSet<>()).add(holder);
}
}
if (byPlacement.isEmpty()) {
return null;
}
StructureManager structureManager = level.structureManager();
Candidate best = null;
double bestDistance = Double.MAX_VALUE;
List<Map.Entry<RandomSpreadStructurePlacement, Set<Holder<Structure>>>> randomPlacements =
new ArrayList<>(byPlacement.size());
for (Map.Entry<StructurePlacement, Set<Holder<Structure>>> entry : byPlacement.entrySet()) {
StructurePlacement placement = entry.getKey();
if (placement instanceof ConcentricRingsStructurePlacement concentric) {
Candidate candidate = predictConcentric(
entry.getValue(), level, structureManager, origin, findUnexplored,
state, concentric);
if (candidate != null) {
double distance = origin.distSqr(candidate.result().getFirst());
if (distance < bestDistance) {
best = candidate;
bestDistance = distance;
}
}
} else if (placement instanceof RandomSpreadStructurePlacement randomSpread) {
randomPlacements.add(Map.entry(randomSpread, entry.getValue()));
}
}
int centerChunkX = SectionPos.blockToSectionCoord(origin.getX());
int centerChunkZ = SectionPos.blockToSectionCoord(origin.getZ());
int searchRadius = Math.max(0, radius);
for (int ring = 0; ring <= searchRadius; ring++) {
boolean foundInRing = false;
for (Map.Entry<RandomSpreadStructurePlacement, Set<Holder<Structure>>> entry
: randomPlacements) {
Candidate candidate = predictRandomSpread(
entry.getValue(), level, structureManager,
centerChunkX, centerChunkZ, ring, findUnexplored,
state.getLevelSeed(), entry.getKey());
if (candidate == null) {
continue;
}
foundInRing = true;
double distance = origin.distSqr(candidate.result().getFirst());
if (distance < bestDistance) {
best = candidate;
bestDistance = distance;
}
}
if (foundInRing) {
return best;
}
}
return best;
}
private static Candidate predictConcentric(Set<Holder<Structure>> holders,
ServerLevel level,
StructureManager structureManager,
BlockPos origin,
boolean findUnexplored,
ChunkGeneratorStructureState state,
ConcentricRingsStructurePlacement placement) {
List<ChunkPos> positions = state.getRingPositionsFor(placement);
if (positions == null) {
throw new IllegalStateException(
"Tried to locate structures for an unavailable concentric placement");
}
Candidate best = null;
double bestDistance = Double.MAX_VALUE;
for (ChunkPos position : positions) {
BlockPos locatePosition = new BlockPos(
SectionPos.sectionToBlockCoord(position.x(), 8),
32,
SectionPos.sectionToBlockCoord(position.z(), 8));
double distance = locatePosition.distSqr(origin);
if (best != null && distance >= bestDistance) {
continue;
}
Candidate candidate = predictAt(
holders, level, structureManager, findUnexplored, placement, position);
if (candidate != null) {
best = candidate;
bestDistance = distance;
}
}
return best;
}
private static Candidate predictRandomSpread(Set<Holder<Structure>> holders,
ServerLevel level,
StructureManager structureManager,
int centerChunkX, int centerChunkZ,
int ring, boolean findUnexplored,
long seed,
RandomSpreadStructurePlacement placement) {
int spacing = placement.spacing();
for (int offsetX = -ring; offsetX <= ring; offsetX++) {
boolean edgeX = offsetX == -ring || offsetX == ring;
for (int offsetZ = -ring; offsetZ <= ring; offsetZ++) {
boolean edgeZ = offsetZ == -ring || offsetZ == ring;
if (!edgeX && !edgeZ) {
continue;
}
int gridChunkX = centerChunkX + spacing * offsetX;
int gridChunkZ = centerChunkZ + spacing * offsetZ;
ChunkPos candidatePosition = placement.getPotentialStructureChunk(
seed, gridChunkX, gridChunkZ);
Candidate candidate = predictAt(
holders, level, structureManager,
findUnexplored, placement, candidatePosition);
if (candidate != null) {
return candidate;
}
}
}
return null;
}
private static Candidate predictAt(Set<Holder<Structure>> holders,
ServerLevel level,
StructureManager structureManager,
boolean findUnexplored,
StructurePlacement placement,
ChunkPos candidatePosition) {
for (Holder<Structure> holder : holders) {
Structure structure = holder.value();
StructureCheckResult result = structureManager.checkStructurePresence(
candidatePosition, structure, placement, findUnexplored);
if (result == StructureCheckResult.START_NOT_PRESENT) {
continue;
}
if (!findUnexplored && result == StructureCheckResult.START_PRESENT) {
return new Candidate(
Pair.of(placement.getLocatePos(candidatePosition), holder), null);
}
ChunkAccess chunk = level.getChunk(
candidatePosition.x(), candidatePosition.z(), ChunkStatus.STRUCTURE_STARTS);
StructureStart start = structureManager.getStartForStructure(
structure, chunk);
if (start == null || !start.isValid()
|| findUnexplored && !start.canBeReferenced()) {
continue;
}
return new Candidate(
Pair.of(placement.getLocatePos(start.getChunkPos()), holder),
findUnexplored ? start : null);
}
return null;
}
public static final class Candidate {
private final Pair<BlockPos, Holder<Structure>> result;
private final StructureStart referenceStart;
private final AtomicBoolean committed;
private Candidate(Pair<BlockPos, Holder<Structure>> result,
StructureStart referenceStart) {
this.result = result;
this.referenceStart = referenceStart;
this.committed = new AtomicBoolean();
}
public Pair<BlockPos, Holder<Structure>> result() {
return result;
}
public boolean reference(StructureManager structureManager) {
if (referenceStart == null || !committed.compareAndSet(false, true)) {
return false;
}
if (!referenceStart.canBeReferenced()) {
return false;
}
structureManager.addReference(referenceStart);
return true;
}
}
}
@@ -0,0 +1,174 @@
package art.arcane.iris.nativegen.v26_3_R1;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.tags.BlockTags;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.LevelChunkSection;
import net.minecraft.world.level.levelgen.GenerationStep;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import java.util.Arrays;
import java.util.BitSet;
import java.util.List;
public final class NativeStructureVegetationClearer {
private NativeStructureVegetationClearer() {
}
public static boolean isUndergroundStep(GenerationStep.Decoration step) {
return step == GenerationStep.Decoration.UNDERGROUND_STRUCTURES
|| step == GenerationStep.Decoration.UNDERGROUND_DECORATION
|| step == GenerationStep.Decoration.STRONGHOLDS;
}
public static void clearIntersectingVegetation(WorldGenLevel world, ChunkAccess chunk, BoundingBox area,
List<StructureStart> starts) {
if (starts == null || starts.isEmpty()) {
return;
}
VegetationSnapshot snapshot = captureVegetation(chunk, area);
if (snapshot.treeBlockCount() == 0) {
return;
}
int columns = area.getXSpan() * area.getZSpan();
int[] clearMinY = new int[columns];
int[] clearMaxY = new int[columns];
Arrays.fill(clearMinY, Integer.MAX_VALUE);
Arrays.fill(clearMaxY, Integer.MIN_VALUE);
for (StructureStart start : starts) {
if (shouldProcessStart(start)) {
markVegetationColumns(area, snapshot, start, clearMinY, clearMaxY);
}
}
clearVegetationColumns(world, area, snapshot, clearMinY, clearMaxY);
}
static boolean shouldProcessStart(StructureStart start) {
return start != null && start.isValid();
}
private static VegetationSnapshot captureVegetation(ChunkAccess chunk, BoundingBox area) {
int width = area.getXSpan();
int depth = area.getZSpan();
BitSet[] columns = new BitSet[width * depth];
int treeBlockCount = 0;
LevelChunkSection[] sections = chunk.getSections();
int chunkMinX = chunk.getPos().getMinBlockX();
int chunkMinZ = chunk.getPos().getMinBlockZ();
for (int sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) {
LevelChunkSection section = sections[sectionIndex];
int sectionMinY = chunk.getSectionYFromSectionIndex(sectionIndex) << 4;
int minY = Math.max(area.minY(), sectionMinY);
int maxY = Math.min(area.maxY(), sectionMinY + 15);
if (minY > maxY || section.hasOnlyAir()
|| !section.maybeHas(NativeStructureVegetationClearer::isTreeBlock)) {
continue;
}
for (int y = minY; y <= maxY; y++) {
int localY = y - sectionMinY;
for (int z = area.minZ(); z <= area.maxZ(); z++) {
int localZ = z - chunkMinZ;
for (int x = area.minX(); x <= area.maxX(); x++) {
int localX = x - chunkMinX;
if (!isTreeBlock(section.getBlockState(localX, localY, localZ))) {
continue;
}
int column = (z - area.minZ()) * width + x - area.minX();
BitSet treeBlocks = columns[column];
if (treeBlocks == null) {
treeBlocks = new BitSet(area.getYSpan());
columns[column] = treeBlocks;
}
treeBlocks.set(y - area.minY());
treeBlockCount++;
}
}
}
}
return new VegetationSnapshot(columns, treeBlockCount);
}
private static void markVegetationColumns(BoundingBox area, VegetationSnapshot snapshot,
StructureStart start, int[] clearMinY, int[] clearMaxY) {
int width = area.getXSpan();
int[] pieceTops = new int[clearMinY.length];
int[] pieceBottoms = new int[clearMinY.length];
Arrays.fill(pieceTops, Integer.MIN_VALUE);
Arrays.fill(pieceBottoms, Integer.MAX_VALUE);
for (StructurePiece piece : start.getPieces()) {
BoundingBox bounds = piece.getBoundingBox();
int minX = Math.max(area.minX(), bounds.minX());
int maxX = Math.min(area.maxX(), bounds.maxX());
int minZ = Math.max(area.minZ(), bounds.minZ());
int maxZ = Math.min(area.maxZ(), bounds.maxZ());
if (minX > maxX || minZ > maxZ) {
continue;
}
for (int z = minZ; z <= maxZ; z++) {
for (int x = minX; x <= maxX; x++) {
int column = (z - area.minZ()) * width + x - area.minX();
pieceTops[column] = Math.max(pieceTops[column], bounds.maxY());
pieceBottoms[column] = Math.min(pieceBottoms[column], bounds.minY());
}
}
}
for (int column = 0; column < pieceTops.length; column++) {
if (snapshot.columns()[column] == null || pieceTops[column] == Integer.MIN_VALUE) {
continue;
}
clearMinY[column] = Math.min(clearMinY[column], pieceBottoms[column] - 1);
clearMaxY[column] = Math.max(clearMaxY[column], pieceTops[column]);
}
}
private static void clearVegetationColumns(WorldGenLevel world, BoundingBox area,
VegetationSnapshot snapshot, int[] clearMinY, int[] clearMaxY) {
int width = area.getXSpan();
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
BlockState air = Blocks.AIR.defaultBlockState();
for (int z = area.minZ(); z <= area.maxZ(); z++) {
for (int x = area.minX(); x <= area.maxX(); x++) {
int column = (z - area.minZ()) * width + x - area.minX();
BitSet treeBlocks = snapshot.columns()[column];
if (treeBlocks == null || clearMaxY[column] < clearMinY[column]) {
continue;
}
int firstBit = Math.max(0, clearMinY[column] - area.minY());
int lastBit = Math.min(area.getYSpan() - 1, clearMaxY[column] - area.minY());
if (firstBit > lastBit) {
continue;
}
for (int bit = treeBlocks.nextSetBit(firstBit); bit >= 0 && bit <= lastBit;
bit = treeBlocks.nextSetBit(bit + 1)) {
int y = area.minY() + bit;
position.set(x, y, z);
BlockState state = world.getBlockState(position);
if (isTreeBlock(state)) {
world.setBlock(position, air, 2);
}
}
}
}
}
static boolean isTreeBlock(BlockState state) {
if (state.is(BlockTags.LOGS) || state.is(BlockTags.LEAVES)) {
return true;
}
// Field reads instead of a registry byValue map hit — this predicate runs per block
// probe on the hottest native-structure scans.
String path = state.getBlock().builtInRegistryHolder().key().identifier().getPath();
return path.endsWith("_log") || path.endsWith("_wood")
|| path.endsWith("_stem") || path.endsWith("_hyphae")
|| path.endsWith("_leaves");
}
private record VegetationSnapshot(BitSet[] columns, int treeBlockCount) {
}
}
@@ -0,0 +1,384 @@
package art.arcane.iris.nativegen.v26_3_R1;
import art.arcane.iris.structure.placement.StructureVerticalBounds;
import art.arcane.iris.structure.placement.IrisStructureYBand;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.math.RNG;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
import net.minecraft.world.level.levelgen.structure.ScatteredFeaturePiece;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
import net.minecraft.world.level.levelgen.structure.pools.JigsawJunction;
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidPiece;
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidStructure;
import net.minecraft.world.level.levelgen.structure.structures.JungleTemplePiece;
import net.minecraft.world.level.levelgen.structure.structures.JungleTempleStructure;
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentPieces;
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentStructure;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.IntBinaryOperator;
public final class NativeStructureVerticalPlacer {
private static final String DESERT_PYRAMID_ID = "minecraft:desert_pyramid";
private static final String JUNGLE_PYRAMID_ID = "minecraft:jungle_pyramid";
private static final int MAX_BURIAL_COLUMNS = 2_000_000;
private static final int MONUMENT_BASE_BELOW_SEA_LEVEL = 24;
private static final String OCEAN_MONUMENT_ID = "minecraft:monument";
private static final int UNDERGROUND_SURFACE_CLEARANCE = 1;
private NativeStructureVerticalPlacer() {
}
public static int applyVerticalPlacement(StructureStart start, String structureId, int requestedOffset,
int seaLevel, int worldMinY, int worldMaxYExclusive,
boolean underground, boolean preserveSourceY,
IrisStructureYBand yBand,
IntBinaryOperator surfaceHeight) {
if (isOceanMonument(start, structureId)) {
return alignOceanMonumentToSeaLevel(
start, requestedOffset, seaLevel, worldMinY, worldMaxYExclusive);
}
if (isAdjustedScatteredStructure(start, structureId)) {
return alignScatteredStructureToSurface(
start, structureId, requestedOffset, worldMinY, worldMaxYExclusive, surfaceHeight);
}
return applyVerticalShift(start, requestedOffset, worldMinY, worldMaxYExclusive,
underground, preserveSourceY, yBand, surfaceHeight);
}
public static StructureStart relocateToMinY(StructureStart start, Structure source, int targetMinY,
LevelHeightAccessor heightAccessor) {
Objects.requireNonNull(start, "Native structure start must not be null");
Objects.requireNonNull(source, "Native structure source must not be null");
Objects.requireNonNull(heightAccessor, "Native structure height accessor must not be null");
if (!start.isValid()) {
return StructureStart.INVALID_START;
}
List<StructurePiece> pieces = start.getPieces();
int minY = Integer.MAX_VALUE;
for (StructurePiece piece : pieces) {
minY = Math.min(minY, piece.getBoundingBox().minY());
}
if (minY == Integer.MAX_VALUE) {
return StructureStart.INVALID_START;
}
int offsetY = Math.subtractExact(targetMinY, minY);
if (offsetY != 0) {
for (StructurePiece piece : pieces) {
moveStructurePiece(piece, offsetY);
}
}
int worldMinY = heightAccessor.getMinY() + 1;
int worldMaxYExclusive = Math.addExact(
heightAccessor.getMinY(), heightAccessor.getHeight());
for (StructurePiece piece : pieces) {
BoundingBox bounds = piece.getBoundingBox();
if (bounds.minY() < worldMinY || bounds.maxY() >= worldMaxYExclusive) {
throw new IllegalStateException("Native structure cannot fit target minimum Y "
+ targetMinY + " inside world bounds [" + worldMinY + ","
+ worldMaxYExclusive + ")");
}
}
return new StructureStart(
source,
start.getChunkPos(),
start.getReferences(),
new PiecesContainer(List.copyOf(pieces))
);
}
static int alignScatteredStructureToSurface(StructureStart start, String structureId,
int configuredOffset, int worldMinY,
int worldMaxYExclusive,
IntBinaryOperator surfaceHeight) {
Objects.requireNonNull(surfaceHeight, "Scattered native structure requires a terrain height resolver");
ScatteredFeaturePiece piece = requireAdjustedScatteredPiece(start, structureId);
BoundingBox bounds = start.getBoundingBox();
BoundingBox pieceBounds = piece.getBoundingBox();
int surfaceY = representativeScatteredSurfaceY(structureId, pieceBounds, surfaceHeight);
int targetMinY = Math.addExact(Math.addExact(surfaceY, 1), configuredOffset);
int requestedMove = Math.subtractExact(targetMinY, pieceBounds.minY());
int offsetY = StructureVerticalBounds.clampOffset(
bounds.minY(), bounds.maxY(), requestedMove, worldMinY, worldMaxYExclusive);
if (offsetY != 0) {
moveStructureStart(start, bounds, offsetY);
}
setScatteredHeightPosition(piece, Math.max(0, piece.getBoundingBox().minY()));
return offsetY;
}
public static int applyVerticalShift(StructureStart start, int requestedOffset, int worldMinY,
int worldMaxYExclusive, boolean underground,
boolean preserveSourceY, IrisStructureYBand yBand,
IntBinaryOperator surfaceHeight) {
BoundingBox bounds = start.getBoundingBox();
int resolvedOffset = resolveShiftOffset(start, bounds, requestedOffset, worldMinY,
worldMaxYExclusive, underground, preserveSourceY, yBand, surfaceHeight);
int offsetY = StructureVerticalBounds.clampOffset(
bounds.minY(), bounds.maxY(), resolvedOffset, worldMinY, worldMaxYExclusive);
if (offsetY == 0) {
return 0;
}
moveStructureStart(start, bounds, offsetY);
return offsetY;
}
private static int resolveShiftOffset(StructureStart start, BoundingBox bounds, int requestedOffset,
int worldMinY, int worldMaxYExclusive, boolean underground,
boolean preserveSourceY, IrisStructureYBand yBand,
IntBinaryOperator surfaceHeight) {
if (preserveSourceY) {
return requestedOffset;
}
if (yBand != null) {
return resolveYBandOffset(bounds, yBand, start.getChunkPos());
}
if (underground) {
return resolveBuriedOffset(
bounds, requestedOffset, worldMinY, worldMaxYExclusive, surfaceHeight);
}
return requestedOffset;
}
static int alignOceanMonumentToSeaLevel(StructureStart start, int configuredOffset, int seaLevel,
int worldMinY, int worldMaxYExclusive) {
OceanMonumentPieces.MonumentBuilding building = requireOceanMonumentBuilding(start);
BoundingBox bounds = start.getBoundingBox();
int targetMinY = Math.addExact(
Math.subtractExact(seaLevel, MONUMENT_BASE_BELOW_SEA_LEVEL), configuredOffset);
int requestedOffset = Math.subtractExact(targetMinY, building.getBoundingBox().minY());
int offsetY = StructureVerticalBounds.clampOffset(
bounds.minY(), bounds.maxY(), requestedOffset, worldMinY, worldMaxYExclusive);
if (offsetY != requestedOffset) {
throw new IllegalStateException("Ocean monument cannot align to sea level " + seaLevel
+ " with configured offset " + configuredOffset + " inside world bounds ["
+ worldMinY + "," + worldMaxYExclusive + ")");
}
if (offsetY == 0) {
return 0;
}
moveStructureStart(start, bounds, offsetY);
return offsetY;
}
static void ensureMonumentSeaLevelAlignment(StructureStart start, String structureId,
int configuredOffset, int seaLevel,
int worldMinY, int worldMaxYExclusive) {
if (!isOceanMonument(start, structureId)) {
return;
}
synchronized (start) {
alignOceanMonumentToSeaLevel(
start, configuredOffset, seaLevel, worldMinY, worldMaxYExclusive);
}
}
private static boolean isOceanMonument(StructureStart start, String structureId) {
return OCEAN_MONUMENT_ID.equals(structureId)
&& start != null
&& start.getStructure() instanceof OceanMonumentStructure;
}
private static boolean isAdjustedScatteredStructure(StructureStart start, String structureId) {
if (start == null) {
return false;
}
Structure source = start.getStructure();
return (DESERT_PYRAMID_ID.equals(structureId) && source instanceof DesertPyramidStructure)
|| (JUNGLE_PYRAMID_ID.equals(structureId) && source instanceof JungleTempleStructure);
}
private static ScatteredFeaturePiece requireAdjustedScatteredPiece(StructureStart start,
String structureId) {
Objects.requireNonNull(start, "Scattered native structure start must not be null");
List<StructurePiece> pieces = start.getPieces();
if (pieces.size() != 1) {
throw new IllegalStateException(structureId + " must contain exactly one scattered piece, found "
+ pieces.size());
}
StructurePiece piece = pieces.get(0);
if (DESERT_PYRAMID_ID.equals(structureId) && piece instanceof DesertPyramidPiece desertPyramid) {
return desertPyramid;
}
if (JUNGLE_PYRAMID_ID.equals(structureId) && piece instanceof JungleTemplePiece jungleTemple) {
return jungleTemple;
}
throw new IllegalStateException(structureId + " contains unexpected piece "
+ piece.getClass().getName());
}
private static int representativeScatteredSurfaceY(String structureId, BoundingBox bounds,
IntBinaryOperator surfaceHeight) {
if (DESERT_PYRAMID_ID.equals(structureId)) {
return lowestSurfaceY(bounds, surfaceHeight);
}
if (JUNGLE_PYRAMID_ID.equals(structureId)) {
return averageSurfaceY(bounds, surfaceHeight);
}
throw new IllegalStateException("Unsupported scattered native structure " + structureId);
}
private static int lowestSurfaceY(BoundingBox bounds, IntBinaryOperator surfaceHeight) {
int lowestY = Integer.MAX_VALUE;
for (int x = bounds.minX(); x <= bounds.maxX(); x++) {
for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) {
lowestY = Math.min(lowestY, surfaceHeight.applyAsInt(x, z));
}
}
if (lowestY == Integer.MAX_VALUE) {
throw new IllegalStateException("Scattered native structure has an empty terrain footprint");
}
return lowestY;
}
private static int averageSurfaceY(BoundingBox bounds, IntBinaryOperator surfaceHeight) {
long totalY = 0L;
long columns = 0L;
for (int x = bounds.minX(); x <= bounds.maxX(); x++) {
for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) {
totalY += surfaceHeight.applyAsInt(x, z);
columns++;
}
}
if (columns == 0L) {
throw new IllegalStateException("Scattered native structure has an empty terrain footprint");
}
return Math.toIntExact(totalY / columns);
}
private static void setScatteredHeightPosition(ScatteredFeaturePiece piece, int heightPosition) {
try {
NativeStructureReflection.ScatteredHeightPositionAccess.FIELD.setInt(piece, heightPosition);
} catch (IllegalAccessException error) {
throw new IllegalStateException("Cannot lock scattered native structure height", error);
}
}
private static OceanMonumentPieces.MonumentBuilding requireOceanMonumentBuilding(StructureStart start) {
Objects.requireNonNull(start, "Ocean monument start must not be null");
List<StructurePiece> pieces = start.getPieces();
OceanMonumentPieces.MonumentBuilding building = null;
for (StructurePiece piece : pieces) {
if (piece instanceof OceanMonumentPieces.MonumentBuilding monumentBuilding) {
building = monumentBuilding;
}
}
if (pieces.size() != 1 || building == null) {
throw new IllegalStateException("minecraft:monument must contain exactly one MonumentBuilding, found "
+ pieces.size() + " pieces");
}
return building;
}
private static void moveStructureStart(StructureStart start, BoundingBox cachedBounds, int offsetY) {
for (StructurePiece piece : start.getPieces()) {
moveStructurePiece(piece, offsetY);
}
cachedBounds.move(0, offsetY, 0);
}
private static void moveStructurePiece(StructurePiece piece, int offsetY) {
piece.move(0, offsetY, 0);
if (piece instanceof OceanMonumentPieces.MonumentBuilding building) {
for (StructurePiece child : monumentChildPieces(building)) {
child.move(0, offsetY, 0);
}
}
if (piece instanceof PoolElementStructurePiece poolPiece) {
List<JigsawJunction> junctions = poolPiece.getJunctions();
for (int i = 0; i < junctions.size(); i++) {
JigsawJunction junction = junctions.get(i);
junctions.set(i, new JigsawJunction(
junction.getSourceX(),
junction.getSourceGroundY() + offsetY,
junction.getSourceZ(),
junction.getDeltaY(),
junction.getDestProjection()));
}
}
}
static List<StructurePiece> monumentChildPieces(OceanMonumentPieces.MonumentBuilding building) {
Object value;
try {
value = NativeStructureReflection.MonumentChildPiecesAccess.FIELD.get(building);
} catch (IllegalAccessException error) {
throw new IllegalStateException("Cannot read Ocean Monument child pieces", error);
}
if (!(value instanceof List<?> children)) {
throw new IllegalStateException("Ocean Monument child-pieces field is not a list");
}
List<StructurePiece> pieces = new ArrayList<>(children.size());
for (Object child : children) {
if (!(child instanceof StructurePiece monumentPiece)
|| child.getClass().getEnclosingClass() != OceanMonumentPieces.class) {
throw new IllegalStateException("Ocean Monument child-pieces list contains "
+ (child == null ? "null" : child.getClass().getName()));
}
pieces.add(monumentPiece);
}
return List.copyOf(pieces);
}
static int resolveYBandOffset(BoundingBox bounds, IrisStructureYBand yBand, ChunkPos startChunk) {
Objects.requireNonNull(bounds, "Native structure bounds must not be null");
Objects.requireNonNull(startChunk, "Native structure Y band requires a start chunk");
int target = yBandTargetMidpointY(
bounds.minY(), bounds.maxY(), yBand.resolvedMin(), yBand.resolvedMax(), startChunk);
return Math.subtractExact(target, midpointY(bounds.minY(), bounds.maxY()));
}
static int yBandTargetMidpointY(int minY, int maxY, int bandMin, int bandMax, ChunkPos startChunk) {
int height = Math.subtractExact(maxY, minY);
int lowest = Math.addExact(bandMin, height / 2);
int highest = Math.subtractExact(bandMax, height - height / 2);
if (lowest > highest) {
// The band is shorter than the structure, so only its midpoint can be honoured.
return Math.floorDiv(Math.addExact(bandMin, bandMax), 2);
}
int span = highest - lowest + 1;
if (span == 1) {
return lowest;
}
long identity = (long) startChunk.x() * 341873128712L ^ (long) startChunk.z() * 132897987541L;
return lowest + new RNG(identity).nextInt(span);
}
private static int midpointY(int minY, int maxY) {
return minY + (maxY - minY) / 2;
}
static int resolveBuriedOffset(BoundingBox bounds, int requestedOffset, int worldMinY,
int worldMaxYExclusive, IntBinaryOperator surfaceHeight) {
Objects.requireNonNull(bounds, "Native structure bounds must not be null");
Objects.requireNonNull(surfaceHeight, "Underground native structure requires a terrain height resolver");
long width = (long) bounds.maxX() - bounds.minX() + 1L;
long depth = (long) bounds.maxZ() - bounds.minZ() + 1L;
if (width <= 0L || depth <= 0L || width > MAX_BURIAL_COLUMNS / depth) {
throw new IllegalStateException("Underground native structure burial footprint is invalid or exceeds "
+ MAX_BURIAL_COLUMNS + " columns");
}
int maximumOffset = requestedOffset;
for (int x = bounds.minX(); x <= bounds.maxX(); x++) {
for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) {
int allowedTopY = surfaceHeight.applyAsInt(x, z) - UNDERGROUND_SURFACE_CLEARANCE;
maximumOffset = Math.min(maximumOffset, allowedTopY - bounds.maxY());
}
}
int clampedOffset = StructureVerticalBounds.clampOffset(
bounds.minY(), bounds.maxY(), maximumOffset, worldMinY, worldMaxYExclusive);
if (clampedOffset > maximumOffset) {
IrisLogging.warn("Native structure burial at " + bounds.minX() + "," + bounds.minZ()
+ " clamped to world floor: wanted " + maximumOffset + ", used " + clampedOffset);
}
return clampedOffset;
}
}
@@ -0,0 +1,643 @@
package art.arcane.iris.nativegen.v26_3_R1;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.structure.nativegen.NativeStructureGenerationPolicy;
import art.arcane.iris.structure.nativegen.NativeStructureOwnershipRecord;
import art.arcane.iris.structure.nativegen.NativeStructurePlacementPlanner;
import art.arcane.iris.structure.nativegen.NativeStructureStartPlan;
import art.arcane.iris.structure.nativegen.NativeStructureVolume;
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.structure.nativegen.IrisNativeStructureDecision;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.collection.KList;
import net.minecraft.core.Holder;
import net.minecraft.core.Registry;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.biome.BiomeSource;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.StructureSet;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.IntBinaryOperator;
import java.util.function.IntConsumer;
import java.util.function.Supplier;
/**
* Resolves the world-space piece bounds of every native structure that will generate around a query rect.
*
* <p>The answer is derived from the seed, the registries and this pack's structure policy only: candidate origins
* come from the placement grid and from vanilla structure placements, and each candidate is assembled through the
* same {@link NativeStructureFactory} path chunk generation uses. Nothing here reads chunk state, ownership records
* or generation progress, so the same query answers identically no matter which chunks already exist.
*/
public final class NativeStructureVolumeIndex {
private static final int ORIGIN_REACH_CHUNKS = NativeStructureOwnershipRecord.MAX_REFERENCE_DISTANCE_CHUNKS;
private static final int ORIGIN_WINDOW_DIAMETER = ORIGIN_REACH_CHUNKS * 2 + 1;
private static final int ORIGIN_WINDOW_STRIPE_COUNT = Long.SIZE;
private static final int MAX_CACHED_ORIGIN_CHUNKS = 16_384;
private static final int MAX_CACHED_QUERY_CHUNKS = 4_096;
private static final Map<Engine, NativeStructureVolumeIndex> INDEXES =
Collections.synchronizedMap(new WeakHashMap<>());
private static final Set<String> WARNED_RESOLUTION_FAILURES = ConcurrentHashMap.newKeySet();
private final Context context;
private final OriginResolver origins;
private final Map<RuntimeChunkKey, KList<NativeStructureVolume>> originCache = lru(MAX_CACHED_ORIGIN_CHUNKS);
private final Map<RuntimeChunkKey, KList<NativeStructureVolume>> queryCache = lru(MAX_CACHED_QUERY_CHUNKS);
private final ConcurrentHashMap<RuntimeChunkKey, CompletableFuture<KList<NativeStructureVolume>>> originBuilds =
new ConcurrentHashMap<>();
private final ConcurrentHashMap<RuntimeChunkKey, CompletableFuture<KList<NativeStructureVolume>>> queryBuilds =
new ConcurrentHashMap<>();
private final ReentrantLock[] originWindowStripes = createOriginWindowStripes();
private final IntConsumer retirementListener = this::evictRuntime;
private NativeStructureVolumeIndex(Context context) {
this.context = Objects.requireNonNull(context, "Native structure volume context must not be null");
this.origins = this::buildOriginVolumes;
}
private NativeStructureVolumeIndex(OriginResolver origins) {
this.context = null;
this.origins = Objects.requireNonNull(origins, "Native structure origin resolver must not be null");
}
public static void install(Engine engine, Context context) {
Engine requiredEngine = Objects.requireNonNull(engine, "Native structure volume index requires an engine");
NativeStructureVolumeIndex installed = new NativeStructureVolumeIndex(context);
synchronized (INDEXES) {
NativeStructureVolumeIndex previous = INDEXES.put(requiredEngine, installed);
if (previous != null) {
previous.detachRetirementListener(requiredEngine);
}
installed.attachRetirementListener(requiredEngine);
}
}
public static void uninstall(Engine engine) {
if (engine != null) {
synchronized (INDEXES) {
NativeStructureVolumeIndex removed = INDEXES.remove(engine);
if (removed != null) {
removed.detachRetirementListener(engine);
}
}
}
}
public static void invalidate(Engine engine) {
if (engine == null) {
return;
}
synchronized (INDEXES) {
NativeStructureVolumeIndex current = INDEXES.get(engine);
if (current != null) {
NativeStructureVolumeIndex replacement = current.fresh();
current.detachRetirementListener(engine);
INDEXES.put(engine, replacement);
replacement.attachRetirementListener(engine);
}
}
}
public static KList<NativeStructureVolume> volumes(Engine engine, int minX, int minZ, int maxX, int maxZ) {
NativeStructureVolumeIndex index = engine == null ? null : INDEXES.get(engine);
return index == null ? NativeStructureVolume.NONE : index.resolve(engine, minX, minZ, maxX, maxZ);
}
static NativeStructureVolumeIndex forTesting(OriginResolver origins) {
return new NativeStructureVolumeIndex(origins);
}
static int originReachChunks() {
return ORIGIN_REACH_CHUNKS;
}
private NativeStructureVolumeIndex fresh() {
return context == null
? new NativeStructureVolumeIndex(origins)
: new NativeStructureVolumeIndex(context);
}
KList<NativeStructureVolume> resolve(Engine engine, int minX, int minZ, int maxX, int maxZ) {
int runtimeId = runtimeId(engine);
int fromChunkX = Math.min(minX, maxX) >> 4;
int toChunkX = Math.max(minX, maxX) >> 4;
int fromChunkZ = Math.min(minZ, maxZ) >> 4;
int toChunkZ = Math.max(minZ, maxZ) >> 4;
KList<NativeStructureVolume> matches = null;
for (int chunkX = fromChunkX; chunkX <= toChunkX; chunkX++) {
for (int chunkZ = fromChunkZ; chunkZ <= toChunkZ; chunkZ++) {
for (NativeStructureVolume volume : chunkVolumes(engine, runtimeId, chunkX, chunkZ)) {
if (!volume.intersectsRect(minX, minZ, maxX, maxZ)) {
continue;
}
if (matches == null) {
matches = new KList<>();
}
if (!matches.contains(volume)) {
matches.add(volume);
}
}
}
}
return matches == null ? NativeStructureVolume.NONE : matches;
}
private KList<NativeStructureVolume> chunkVolumes(
Engine engine,
int runtimeId,
int chunkX,
int chunkZ
) {
return cached(queryCache, queryBuilds, new RuntimeChunkKey(runtimeId, chunkKey(chunkX, chunkZ)),
() -> buildChunkVolumes(engine, chunkX, chunkZ));
}
private KList<NativeStructureVolume> buildChunkVolumes(Engine engine, int chunkX, int chunkZ) {
long stripeMask = originWindowStripeMask(chunkX, chunkZ);
lockOriginWindow(stripeMask);
try {
return buildChunkVolumesCoordinated(engine, chunkX, chunkZ);
} finally {
unlockOriginWindow(stripeMask);
}
}
private KList<NativeStructureVolume> buildChunkVolumesCoordinated(Engine engine, int chunkX, int chunkZ) {
int minX = chunkX << 4;
int minZ = chunkZ << 4;
int maxX = minX + 15;
int maxZ = minZ + 15;
Integer pinnedRuntimeId = pinnedRuntimeId(engine);
KList<NativeStructureVolume> volumes = null;
for (int originX = chunkX - ORIGIN_REACH_CHUNKS; originX <= chunkX + ORIGIN_REACH_CHUNKS; originX++) {
for (int originZ = chunkZ - ORIGIN_REACH_CHUNKS; originZ <= chunkZ + ORIGIN_REACH_CHUNKS; originZ++) {
for (NativeStructureVolume volume : scopedOriginVolumes(engine, originX, originZ, pinnedRuntimeId)) {
if (!volume.intersectsRect(minX, minZ, maxX, maxZ)) {
continue;
}
if (volumes == null) {
volumes = new KList<>();
}
volumes.add(volume);
}
}
}
return volumes == null ? NativeStructureVolume.NONE : volumes;
}
private static Integer pinnedRuntimeId(Engine engine) {
if (!(engine instanceof IrisEngine irisEngine) || !irisEngine.hasGenerationRuntimeScope()) {
return null;
}
GenerationHistoryRuntimeRouter router = irisEngine.getGenerationHistoryRuntimeRouter().orElse(null);
if (router == null) {
return null;
}
GenerationHistoryRuntimeRouter.RuntimeOwnership ownership = router.currentRuntimeOwnership().orElse(null);
int runtimeId = runtimeId(engine);
return ownership != null && ownership.binding().runtimeId() == runtimeId ? runtimeId : null;
}
private KList<NativeStructureVolume> scopedOriginVolumes(
Engine engine, int chunkX, int chunkZ, Integer pinnedRuntimeId
) {
if (pinnedRuntimeId != null) {
synchronized (originCache) {
KList<NativeStructureVolume> cached = originCache.get(
new RuntimeChunkKey(pinnedRuntimeId, chunkKey(chunkX, chunkZ)));
if (cached != null) {
return cached;
}
}
}
return originVolumes(engine, chunkX, chunkZ);
}
static long originWindowStripeMask(int chunkX, int chunkZ) {
int minCellX = Math.floorDiv(chunkX - ORIGIN_REACH_CHUNKS, ORIGIN_WINDOW_DIAMETER);
int maxCellX = Math.floorDiv(chunkX + ORIGIN_REACH_CHUNKS, ORIGIN_WINDOW_DIAMETER);
int minCellZ = Math.floorDiv(chunkZ - ORIGIN_REACH_CHUNKS, ORIGIN_WINDOW_DIAMETER);
int maxCellZ = Math.floorDiv(chunkZ + ORIGIN_REACH_CHUNKS, ORIGIN_WINDOW_DIAMETER);
long mask = 0L;
for (int cellX = minCellX; cellX <= maxCellX; cellX++) {
for (int cellZ = minCellZ; cellZ <= maxCellZ; cellZ++) {
mask |= 1L << originWindowStripe(cellX, cellZ);
}
}
return mask;
}
KList<NativeStructureVolume> originVolumes(Engine engine, int chunkX, int chunkZ) {
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openHistoryCoordinateScope(engine, chunkX, chunkZ)) {
int runtimeId = runtimeId(engine);
return cached(
originCache,
originBuilds,
new RuntimeChunkKey(runtimeId, chunkKey(chunkX, chunkZ)),
() -> origins.volumesAt(engine, chunkX, chunkZ)
);
}
}
private KList<NativeStructureVolume> buildOriginVolumes(Engine engine, int chunkX, int chunkZ) {
ChunkGeneratorStructureState state = context.structureState().get();
ChunkGenerator generator = context.generator().get();
BiomeSource biomeSource = context.biomeSource().get();
if (state == null || generator == null || biomeSource == null) {
return NativeStructureVolume.NONE;
}
Registry<Structure> registry = context.registryAccess().lookupOrThrow(Registries.STRUCTURE);
Bindings bindings = new Bindings(state, generator, biomeSource);
KList<NativeStructureVolume> volumes = appendPlannedVolumes(engine, bindings, registry, chunkX, chunkZ, null);
volumes = appendVanillaVolumes(engine, bindings, registry, chunkX, chunkZ, volumes);
return volumes == null ? NativeStructureVolume.NONE : volumes;
}
private KList<NativeStructureVolume> appendPlannedVolumes(Engine engine, Bindings bindings,
Registry<Structure> registry, int chunkX, int chunkZ,
KList<NativeStructureVolume> volumes) {
KList<NativeStructureStartPlan> plans;
try {
plans = NativeStructurePlacementPlanner.plansAt(engine, chunkX, chunkZ);
} catch (RuntimeException | Error error) {
warnOnce("iris-placements", error);
return volumes;
}
KList<NativeStructureVolume> target = volumes;
for (NativeStructureStartPlan plan : plans) {
IrisNativeStructureDecision decision = NativeStructurePlacementPlanner.decisionFor(plan);
if (!decision.generate()) {
continue;
}
Identifier identifier = Identifier.tryParse(plan.source().getStructure());
if (identifier == null) {
continue;
}
Structure structure = registry.getValue(identifier);
if (structure == null) {
continue;
}
String structureId = identifier.toString();
try {
StructureStart generated = NativeStructureFactory.generate(
generationContext(engine, bindings), registry.wrapAsHolder(structure), plan, 0);
target = appendPieces(target, structureId, generated);
} catch (RuntimeException | Error error) {
warnOnce(structureId, error);
}
}
return target;
}
private KList<NativeStructureVolume> appendVanillaVolumes(Engine engine, Bindings bindings,
Registry<Structure> registry, int chunkX, int chunkZ,
KList<NativeStructureVolume> volumes) {
ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ);
IntBinaryOperator surfaceHeight = surfaceHeight(engine);
int worldMinY = context.heightAccessor().getMinY();
int worldMaxYExclusive = worldMinY + context.heightAccessor().getHeight();
KList<NativeStructureVolume> target = volumes;
for (Holder<StructureSet> setHolder : bindings.state().possibleStructureSets()) {
StructureSet set = setHolder.value();
if (!set.placement().isStructureChunk(bindings.state(), chunkX, chunkZ)) {
continue;
}
for (StructureSet.StructureSelectionEntry entry : set.structures()) {
Holder<Structure> holder = entry.structure();
Structure structure = holder.value();
Identifier identifier = registry.getKey(structure);
if (identifier == null) {
continue;
}
String structureId = identifier.toString();
boolean undergroundStep = NativeStructureVegetationClearer.isUndergroundStep(structure.step());
try {
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(
engine, structureId, undergroundStep);
if (!decision.generate()) {
continue;
}
StructureStart generated = structure.generate(
holder,
context.levelKey(),
context.registryAccess(),
bindings.generator(),
bindings.biomeSource(),
bindings.state().randomState().createClimateSampler(SamplerContext.EMPTY_UNCACHED),
bindings.state().randomState(),
context.templateManager(),
bindings.state().getLevelSeed(),
chunkPos,
0,
context.heightAccessor(),
structure.biomes()::contains
);
if (generated == null || !generated.isValid()) {
continue;
}
NativeStructureVerticalPlacer.applyVerticalPlacement(
generated,
structureId,
decision.yShift(),
bindings.generator().getSeaLevel(),
worldMinY,
worldMaxYExclusive,
undergroundStep,
decision.preserveSourceY(),
decision.yBand(),
surfaceHeight);
target = appendPieces(target, structureId, generated);
} catch (RuntimeException | Error error) {
warnOnce(structureId, error);
}
}
}
return target;
}
private NativeStructureFactory.GenerationContext generationContext(Engine engine, Bindings bindings) {
return new NativeStructureFactory.GenerationContext(
context.registryAccess(),
bindings.generator(),
bindings.biomeSource(),
bindings.state().randomState(),
context.templateManager(),
bindings.state().getLevelSeed(),
context.levelKey(),
context.heightAccessor(),
biome -> true,
bindings.generator().getSeaLevel(),
surfaceHeight(engine)
);
}
private record Bindings(ChunkGeneratorStructureState state, ChunkGenerator generator, BiomeSource biomeSource) {
}
@FunctionalInterface
interface OriginResolver {
KList<NativeStructureVolume> volumesAt(Engine engine, int chunkX, int chunkZ);
}
private static IntBinaryOperator surfaceHeight(Engine engine) {
return (x, z) -> Engine.hostHeight(engine, x, z, true) + engine.getMinHeight();
}
static KList<NativeStructureVolume> appendPieces(KList<NativeStructureVolume> volumes,
String structureId, StructureStart start) {
if (start == null || !start.isValid()) {
return volumes;
}
KList<NativeStructureVolume> target = volumes;
for (StructurePiece piece : start.getPieces()) {
BoundingBox bounds = piece.getBoundingBox();
if (target == null) {
target = new KList<>();
}
target.add(new NativeStructureVolume(structureId,
bounds.minX(), bounds.minY(), bounds.minZ(),
bounds.maxX(), bounds.maxY(), bounds.maxZ()));
}
return target;
}
void evictRuntime(int runtimeId) {
synchronized (originCache) {
originBuilds.keySet().removeIf(key -> key.runtimeId() == runtimeId);
originCache.keySet().removeIf(key -> key.runtimeId() == runtimeId);
}
synchronized (queryCache) {
queryBuilds.keySet().removeIf(key -> key.runtimeId() == runtimeId);
queryCache.keySet().removeIf(key -> key.runtimeId() == runtimeId);
}
}
private KList<NativeStructureVolume> cached(
Map<RuntimeChunkKey, KList<NativeStructureVolume>> cache,
ConcurrentHashMap<RuntimeChunkKey, CompletableFuture<KList<NativeStructureVolume>>> builds,
RuntimeChunkKey key,
Supplier<KList<NativeStructureVolume>> loader
) {
synchronized (cache) {
KList<NativeStructureVolume> hit = cache.get(key);
if (hit != null) {
return hit;
}
}
CompletableFuture<KList<NativeStructureVolume>> future = new CompletableFuture<>();
CompletableFuture<KList<NativeStructureVolume>> existing = builds.putIfAbsent(key, future);
if (existing != null) {
return NativeBuildFutures.awaitBuild(existing, "Native structure volume build");
}
// Close the check-then-claim window: a racer whose cache check missed before the
// previous builder cached could claim the build slot after it retired and rebuild.
synchronized (cache) {
KList<NativeStructureVolume> published = cache.get(key);
if (published != null) {
future.complete(published);
builds.remove(key, future);
return published;
}
}
try {
KList<NativeStructureVolume> built = loader.get();
synchronized (cache) {
if (builds.get(key) == future) {
cache.put(key, built);
}
}
future.complete(built);
return built;
} catch (RuntimeException | Error error) {
future.completeExceptionally(error);
throw error;
} finally {
builds.remove(key, future);
}
}
private static void warnOnce(String structureId, Throwable error) {
if (WARNED_RESOLUTION_FAILURES.add(structureId)) {
IrisLogging.reportError("Native structure volume resolution failed for '" + structureId
+ "'; objects will not be vetoed against it.", error);
}
}
private static long chunkKey(int chunkX, int chunkZ) {
return ((long) chunkX << 32) ^ (chunkZ & 0xffffffffL);
}
private static int originWindowStripe(int cellX, int cellZ) {
long mixed = chunkKey(cellX, cellZ);
mixed ^= mixed >>> 33;
mixed *= 0xff51afd7ed558ccdL;
mixed ^= mixed >>> 33;
mixed *= 0xc4ceb9fe1a85ec53L;
mixed ^= mixed >>> 33;
return (int) mixed & (ORIGIN_WINDOW_STRIPE_COUNT - 1);
}
private static ReentrantLock[] createOriginWindowStripes() {
ReentrantLock[] stripes = new ReentrantLock[ORIGIN_WINDOW_STRIPE_COUNT];
for (int i = 0; i < stripes.length; i++) {
stripes[i] = new ReentrantLock();
}
return stripes;
}
private void lockOriginWindow(long stripeMask) {
long pending = stripeMask;
while (pending != 0L) {
int stripe = Long.numberOfTrailingZeros(pending);
lockOriginStripe(originWindowStripes[stripe]);
pending &= pending - 1L;
}
}
private static void lockOriginStripe(ReentrantLock lock) {
if (lock.tryLock()) {
return;
}
if (!ForkJoinTask.inForkJoinPool()) {
lock.lock();
return;
}
OriginLockBlocker blocker = new OriginLockBlocker(lock);
try {
ForkJoinPool.managedBlock(blocker);
} catch (InterruptedException interruption) {
Thread.currentThread().interrupt();
blocker.block();
} catch (RejectedExecutionException exhaustedPool) {
blocker.block();
}
}
private void unlockOriginWindow(long stripeMask) {
long pending = stripeMask;
while (pending != 0L) {
int stripe = Long.SIZE - 1 - Long.numberOfLeadingZeros(pending);
originWindowStripes[stripe].unlock();
pending &= ~(1L << stripe);
}
}
private static final class OriginLockBlocker implements ForkJoinPool.ManagedBlocker {
private final ReentrantLock lock;
private boolean acquired;
private OriginLockBlocker(ReentrantLock lock) {
this.lock = lock;
}
@Override
public boolean block() {
if (!acquired) {
lock.lock();
acquired = true;
}
return true;
}
@Override
public boolean isReleasable() {
if (!acquired) {
acquired = lock.tryLock();
}
return acquired;
}
}
private static GenerationHistoryRuntimeRouter.CoordinateScope openHistoryCoordinateScope(
Engine engine,
int chunkX,
int chunkZ
) {
if (!(engine instanceof IrisEngine irisEngine)) {
return null;
}
try {
return irisEngine.openGenerationHistoryCoordinateScope(chunkX << 4, chunkZ << 4);
} catch (IOException failure) {
throw new IllegalStateException("Unable to route native structure origin "
+ chunkX + "," + chunkZ + " through generation history.", failure);
}
}
private static int runtimeId(Engine engine) {
return engine == null ? 0 : engine.getCacheID();
}
private void attachRetirementListener(Engine engine) {
if (engine instanceof IrisEngine irisEngine) {
irisEngine.addGenerationRuntimeRetirementListener(retirementListener);
}
}
private void detachRetirementListener(Engine engine) {
if (engine instanceof IrisEngine irisEngine) {
irisEngine.removeGenerationRuntimeRetirementListener(retirementListener);
}
}
private static Map<RuntimeChunkKey, KList<NativeStructureVolume>> lru(int capacity) {
return new LinkedHashMap<>(16, 0.75F, true) {
@Override
protected boolean removeEldestEntry(
Map.Entry<RuntimeChunkKey, KList<NativeStructureVolume>> eldest
) {
return size() > capacity;
}
};
}
private record RuntimeChunkKey(int runtimeId, long chunkKey) {
}
/**
* The generator, biome source and structure state arrive as suppliers so an installed index never holds the
* level (and through it the engine) strongly. That keeps the weakly keyed registry collectable on world unload.
*/
public record Context(
RegistryAccess registryAccess,
StructureTemplateManager templateManager,
ResourceKey<Level> levelKey,
LevelHeightAccessor heightAccessor,
Supplier<ChunkGenerator> generator,
Supplier<BiomeSource> biomeSource,
Supplier<ChunkGeneratorStructureState> structureState
) {
}
}
@@ -0,0 +1,42 @@
package art.arcane.iris.nativegen.v26_3_R1;
import it.unimi.dsi.fastutil.objects.Object2IntLinkedOpenHashMap;
import net.minecraft.world.level.levelgen.Heightmap;
import java.util.OptionalInt;
import java.util.function.Supplier;
public final class NativeTerrainHeightCache {
private static final int MAXIMUM_CACHED_QUERIES = 65_536;
private final Object2IntLinkedOpenHashMap<Query> heights = new Object2IntLinkedOpenHashMap<>();
public OptionalInt resolvedHeight(Query query, Supplier<OptionalInt> resolver) {
synchronized (heights) {
if (heights.containsKey(query)) {
return OptionalInt.of(heights.getAndMoveToFirst(query));
}
}
OptionalInt resolved = resolver.get();
if (resolved.isEmpty()) {
return resolved;
}
int height = resolved.getAsInt();
synchronized (heights) {
heights.putAndMoveToFirst(query, height);
while (heights.size() > MAXIMUM_CACHED_QUERIES) {
heights.removeLastInt();
}
}
return resolved;
}
public void evictRuntime(int runtimeId) {
synchronized (heights) {
heights.keySet().removeIf(query -> query.runtimeId() == runtimeId);
}
}
public record Query(int runtimeId, int x, int z, Heightmap.Types type, int minimumY, int height) {
}
}
@@ -0,0 +1,63 @@
package art.arcane.iris.nativegen.v26_3_R1;
import art.arcane.iris.world.history.TerrainBoundarySignature;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBlockState;
import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.minecraft.commands.arguments.blocks.BlockStateParser;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.NoiseColumn;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.levelgen.Heightmap;
import java.util.HashMap;
import java.util.Map;
public final class NativeTransitionColumn {
private NativeTransitionColumn() {
}
public static NoiseColumn column(TerrainBoundarySignature signature, LevelHeightAccessor height) {
BlockState[] states = new BlockState[height.getHeight()];
Map<String, BlockState> palette = new HashMap<>();
for (int offset = 0; offset < states.length; offset++) {
String state = signature.geometry().voxelAt(height.getMinY() + offset).stateKey();
states[offset] = palette.computeIfAbsent(state, NativeTransitionColumn::parse);
}
return new NoiseColumn(height.getMinY(), states);
}
public static int height(TerrainBoundarySignature signature, Heightmap.Types type, LevelHeightAccessor height) {
Map<String, BlockState> palette = new HashMap<>();
for (int offset = height.getHeight() - 1; offset >= 0; offset--) {
String state = signature.geometry().voxelAt(height.getMinY() + offset).stateKey();
if (type.isOpaque().test(palette.computeIfAbsent(state, NativeTransitionColumn::parse))) {
return height.getMinY() + offset + 1;
}
}
return height.getMinY();
}
private static BlockState parse(String key) {
String nativeKey = key;
if (!key.startsWith("minecraft:") && IrisPlatforms.isBound()) {
PlatformBlockState logicalState = IrisPlatforms.get().registries().blockOrNull(key);
PlatformBlockState baseState = logicalState == null ? null : logicalState.placementBaseState();
if (baseState != null) {
nativeKey = baseState.key();
}
}
StringReader reader = new StringReader(nativeKey);
try {
BlockState state = BlockStateParser.parseForBlock(BuiltInRegistries.BLOCK, reader, false).blockState();
if (reader.canRead()) {
throw new IllegalArgumentException("Unexpected terrain block state suffix: " + reader.getRemaining());
}
return state;
} catch (CommandSyntaxException failure) {
throw new IllegalArgumentException("Saved terrain block state cannot be resolved: " + key, failure);
}
}
}
@@ -0,0 +1,113 @@
package art.arcane.iris.nativegen.v26_3_R1;
import net.minecraft.core.SectionPos;
import net.minecraft.util.Mth;
import net.minecraft.util.SimpleBitStorage;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.IntBinaryOperator;
/**
* Writes WORLD_SURFACE_WG and OCEAN_FLOOR_WG from Iris engine heights.
*
* Iris writes its terrain straight into LevelChunkSection, so neither vanilla's noise fill nor
* ProtoChunk.setBlockState ever updates the two worldgen heightmaps. Without these writes
* ChunkAccess.getHeight lazily primes the map from finished blocks - Iris trees, leaves and snow
* included - and vanilla pieces that self-snap against WORLD_SURFACE_WG (igloo) or OCEAN_FLOOR_WG
* (ocean ruins, buried treasure, mineshaft interiors) anchor to the canopy instead of the terrain.
*
* Both resolvers take world block coordinates and return the absolute first free Y, matching
* Heightmap's internal "first available" semantics: WORLD_SURFACE_WG counts fluid, OCEAN_FLOOR_WG
* does not.
*/
public final class WorldgenTerrainHeightmaps {
private static final int COLUMNS = 256;
private static final int PLACEMENT_CHUNK_MARGIN = 1;
private WorldgenTerrainHeightmaps() {
}
public static void primeTerrain(ChunkAccess chunk, IntBinaryOperator surfaceFirstFreeY,
IntBinaryOperator floorFirstFreeY) {
Objects.requireNonNull(chunk, "Iris worldgen heightmap priming requires a chunk");
Objects.requireNonNull(surfaceFirstFreeY,
"Iris worldgen heightmap priming requires a surface height resolver");
Objects.requireNonNull(floorFirstFreeY,
"Iris worldgen heightmap priming requires an ocean floor height resolver");
write(chunk, Heightmap.Types.WORLD_SURFACE_WG, surfaceFirstFreeY);
write(chunk, Heightmap.Types.OCEAN_FLOOR_WG, floorFirstFreeY);
}
public static void primeStructurePlacement(WorldGenLevel world, ChunkPos generationCenter,
List<StructureStart> starts,
IntBinaryOperator surfaceFirstFreeY,
IntBinaryOperator floorFirstFreeY) {
Objects.requireNonNull(world,
"Iris worldgen heightmap priming requires a generation level");
Objects.requireNonNull(generationCenter,
"Iris worldgen heightmap priming requires a generation center");
if (starts == null || starts.isEmpty()) {
return;
}
Set<Long> primed = new HashSet<>();
for (StructureStart start : starts) {
if (start == null || !start.isValid()) {
continue;
}
BoundingBox bounds = start.getBoundingBox();
int minChunkX = SectionPos.blockToSectionCoord(bounds.minX()) - PLACEMENT_CHUNK_MARGIN;
int maxChunkX = SectionPos.blockToSectionCoord(bounds.maxX()) + PLACEMENT_CHUNK_MARGIN;
int minChunkZ = SectionPos.blockToSectionCoord(bounds.minZ()) - PLACEMENT_CHUNK_MARGIN;
int maxChunkZ = SectionPos.blockToSectionCoord(bounds.maxZ()) + PLACEMENT_CHUNK_MARGIN;
for (int chunkX = minChunkX; chunkX <= maxChunkX; chunkX++) {
for (int chunkZ = minChunkZ; chunkZ <= maxChunkZ; chunkZ++) {
if (Math.abs(chunkX - generationCenter.x()) > PLACEMENT_CHUNK_MARGIN
|| Math.abs(chunkZ - generationCenter.z()) > PLACEMENT_CHUNK_MARGIN) {
continue;
}
if (!primed.add(ChunkPos.pack(chunkX, chunkZ))) {
continue;
}
if (!world.hasChunk(chunkX, chunkZ)) {
continue;
}
primeTerrain(world.getChunk(chunkX, chunkZ), surfaceFirstFreeY, floorFirstFreeY);
}
}
}
}
private static void write(ChunkAccess chunk, Heightmap.Types type, IntBinaryOperator firstFreeY) {
ChunkPos pos = chunk.getPos();
int minY = chunk.getMinY();
int height = chunk.getHeight();
int baseX = pos.getMinBlockX();
int baseZ = pos.getMinBlockZ();
SimpleBitStorage storage = new SimpleBitStorage(Mth.ceillog2(height + 1), COLUMNS);
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
int firstFree = firstFreeY.applyAsInt(baseX + x, baseZ + z) - minY;
storage.set(x + z * 16, Mth.clamp(firstFree, 0, height));
}
}
Heightmap heightmap = chunk.getOrCreateHeightmapUnprimed(type);
long[] packed = storage.getRaw();
int expected = heightmap.getRawData().length;
if (expected != packed.length) {
throw new IllegalStateException("Iris cannot prime " + type + " for chunk "
+ pos.x() + "," + pos.z() + ": packed " + packed.length
+ " words for a heightmap storing " + expected
+ "; Heightmap.setRawData would silently fall back to block scanning");
}
heightmap.setRawData(chunk, type, packed);
}
}
@@ -0,0 +1,513 @@
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
import net.minecraft.world.level.levelgen.structure.placement.AbstractSpreadingStructurePlacement;
import art.arcane.iris.pack.datapack.DatapackStructureScopeIndex;
import art.arcane.iris.structure.nativegen.NativeStructureFrequencyScale;
import art.arcane.iris.structure.nativegen.IrisImportedStructureControl;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderOwner;
import net.minecraft.core.Vec3i;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.levelgen.structure.StructureSet;
import net.minecraft.world.level.levelgen.structure.placement.ConcentricRingsStructurePlacement;
import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement;
import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
final class DatapackStructureStateFilter {
private DatapackStructureStateFilter() {
}
static Selection filter(
List<Holder<StructureSet>> structureSets,
DatapackStructureScopeIndex scopeIndex,
Set<String> declaredSources
) {
return filter(structureSets, scopeIndex, declaredSources, new IrisImportedStructureControl());
}
static Selection filter(
List<Holder<StructureSet>> structureSets,
DatapackStructureScopeIndex scopeIndex,
Set<String> declaredSources,
IrisImportedStructureControl importedStructures
) {
return filter(
structureSets,
scopeIndex,
declaredSources,
importedStructures,
keyIndex(structureSets));
}
static Selection filter(
List<Holder<StructureSet>> structureSets,
DatapackStructureScopeIndex scopeIndex,
Set<String> declaredSources,
IrisImportedStructureControl importedStructures,
StructureSetKeyIndex keyIndex
) {
if (scopeIndex.isEmpty()) {
return new Selection(
scaleFrequencyOverrides(structureSets, importedStructures, keyIndex),
0,
0);
}
Map<String, Holder<StructureSet>> holdersByKey = new HashMap<>();
for (Holder<StructureSet> holder : structureSets) {
String key = structureSetKey(holder, keyIndex);
if (key != null) {
holdersByKey.putIfAbsent(key, holder);
}
}
Map<Holder<StructureSet>, Holder<StructureSet>> scopedByIdentity = new IdentityHashMap<>();
Set<Holder<StructureSet>> visiting = Collections.newSetFromMap(new IdentityHashMap<>());
List<Holder<StructureSet>> filteredSets = new ArrayList<>(structureSets.size());
int retainedManagedSets = 0;
int excludedManagedSets = 0;
for (Holder<StructureSet> holder : structureSets) {
String setKey = structureSetKey(holder, keyIndex);
boolean managedSet = setKey != null && scopeIndex.isManagedStructureSet(setKey);
Holder<StructureSet> scopedHolder = scopeHolder(
holder,
scopeIndex,
declaredSources,
holdersByKey,
scopedByIdentity,
visiting,
keyIndex);
if (scopedHolder == null) {
if (managedSet) {
excludedManagedSets++;
}
continue;
}
if (managedSet) {
retainedManagedSets++;
}
filteredSets.add(scopedHolder);
}
return new Selection(
scaleFrequencyOverrides(
List.copyOf(filteredSets), importedStructures, keyIndex),
retainedManagedSets,
excludedManagedSets);
}
static String structureSetKey(Holder<StructureSet> holder) {
return structureSetKey(holder, keyIndex(List.of(holder)));
}
static StructureSetKeyIndex keyIndex(
Iterable<? extends Holder<StructureSet>> registeredStructureSets
) {
return StructureSetKeyIndex.create(registeredStructureSets);
}
private static String structureSetKey(
Holder<StructureSet> holder,
StructureSetKeyIndex keyIndex
) {
return keyIndex.resolve(holder)
.map(key -> key.identifier().toString())
.orElse(null);
}
private static List<StructureSet.StructureSelectionEntry> filterEntries(
List<StructureSet.StructureSelectionEntry> entries,
DatapackStructureScopeIndex scopeIndex,
Set<String> declaredSources
) {
List<StructureSet.StructureSelectionEntry> filtered = new ArrayList<>(entries.size());
for (StructureSet.StructureSelectionEntry entry : entries) {
String structureKey = entry.structure().unwrapKey()
.map(key -> key.identifier().toString())
.orElse(null);
if (structureKey == null || scopeIndex.allowsStructure(structureKey, declaredSources)) {
filtered.add(entry);
}
}
return filtered;
}
private static Holder<StructureSet> scopeHolder(
Holder<StructureSet> holder,
DatapackStructureScopeIndex scopeIndex,
Set<String> declaredSources,
Map<String, Holder<StructureSet>> holdersByKey,
Map<Holder<StructureSet>, Holder<StructureSet>> scopedByIdentity,
Set<Holder<StructureSet>> visiting,
StructureSetKeyIndex keyIndex
) {
if (scopedByIdentity.containsKey(holder)) {
return scopedByIdentity.get(holder);
}
if (!visiting.add(holder)) {
return null;
}
try {
String setKey = structureSetKey(holder, keyIndex);
if (setKey != null && scopeIndex.isManagedStructureSet(setKey)
&& !scopeIndex.allowsStructureSet(setKey, declaredSources)) {
scopedByIdentity.put(holder, null);
return null;
}
StructureSet originalSet = holder.value();
List<StructureSet.StructureSelectionEntry> entries = filterEntries(
originalSet.structures(), scopeIndex, declaredSources);
if (entries.isEmpty()) {
scopedByIdentity.put(holder, null);
return null;
}
StructurePlacement placement = scopePlacement(
originalSet.placement(),
scopeIndex,
declaredSources,
holdersByKey,
scopedByIdentity,
visiting,
keyIndex);
if (entries.size() == originalSet.structures().size()
&& placement == originalSet.placement()) {
scopedByIdentity.put(holder, holder);
return holder;
}
Holder<StructureSet> scoped = replacementHolder(
holder, new StructureSet(entries, placement), keyIndex);
scopedByIdentity.put(holder, scoped);
return scoped;
} finally {
visiting.remove(holder);
}
}
static Optional<AbstractSpreadingStructurePlacement.ExclusionZone> exclusionZone(StructurePlacement placement) {
if (!(placement instanceof AbstractSpreadingStructurePlacement)) {
return Optional.empty();
}
Object value = declaredFieldValue(AbstractSpreadingStructurePlacement.class, placement, Optional.class);
if (value instanceof Optional<?> optional && (optional.isEmpty()
|| optional.get() instanceof AbstractSpreadingStructurePlacement.ExclusionZone)) {
@SuppressWarnings("unchecked")
Optional<AbstractSpreadingStructurePlacement.ExclusionZone> resolved =
(Optional<AbstractSpreadingStructurePlacement.ExclusionZone>) optional;
return resolved;
}
throw new IllegalStateException("Could not read structure placement exclusion zone from "
+ placement.getClass().getName());
}
private static StructurePlacement scopePlacement(
StructurePlacement placement,
DatapackStructureScopeIndex scopeIndex,
Set<String> declaredSources,
Map<String, Holder<StructureSet>> holdersByKey,
Map<Holder<StructureSet>, Holder<StructureSet>> scopedByIdentity,
Set<Holder<StructureSet>> visiting,
StructureSetKeyIndex keyIndex
) {
Optional<AbstractSpreadingStructurePlacement.ExclusionZone> currentZone = exclusionZone(placement);
if (currentZone.isEmpty()) {
return placement;
}
Holder<StructureSet> target = currentZone.get().otherSet();
String targetKey = structureSetKey(target, keyIndex);
if (targetKey != null) {
target = holdersByKey.getOrDefault(targetKey, target);
}
Holder<StructureSet> scopedTarget = scopeHolder(
target,
scopeIndex,
declaredSources,
holdersByKey,
scopedByIdentity,
visiting,
keyIndex);
Optional<AbstractSpreadingStructurePlacement.ExclusionZone> scopedZone = Optional.empty();
if (scopedTarget != null) {
scopedZone = Optional.of(new AbstractSpreadingStructurePlacement.ExclusionZone(
scopedTarget,
currentZone.get().chunkCount()));
}
if (scopedTarget == currentZone.get().otherSet()) {
return placement;
}
return copyPlacement(placement, scopedZone, 1D);
}
private static List<Holder<StructureSet>> scaleFrequencyOverrides(
List<Holder<StructureSet>> structureSets,
IrisImportedStructureControl importedStructures,
StructureSetKeyIndex keyIndex
) {
if (!importedStructures.hasFrequencyOverrides()) {
return structureSets;
}
Map<String, Holder<StructureSet>> holdersByKey = new HashMap<>();
for (Holder<StructureSet> holder : structureSets) {
String key = structureSetKey(holder, keyIndex);
if (key != null) {
holdersByKey.putIfAbsent(key, holder);
}
}
Map<Holder<StructureSet>, Holder<StructureSet>> scaledByIdentity = new IdentityHashMap<>();
List<Holder<StructureSet>> scaledSets = new ArrayList<>(structureSets.size());
boolean changed = false;
for (Holder<StructureSet> holder : structureSets) {
Holder<StructureSet> scaled = scaleFrequencyHolder(
holder,
importedStructures,
holdersByKey,
scaledByIdentity,
keyIndex);
scaledSets.add(scaled);
changed |= scaled != holder;
}
return changed ? List.copyOf(scaledSets) : structureSets;
}
private static Holder<StructureSet> scaleFrequencyHolder(
Holder<StructureSet> holder,
IrisImportedStructureControl importedStructures,
Map<String, Holder<StructureSet>> holdersByKey,
Map<Holder<StructureSet>, Holder<StructureSet>> scaledByIdentity,
StructureSetKeyIndex keyIndex
) {
if (scaledByIdentity.containsKey(holder)) {
return scaledByIdentity.get(holder);
}
if (!dependsOnFrequencyOverride(
holder, importedStructures, holdersByKey, keyIndex)) {
scaledByIdentity.put(holder, holder);
return holder;
}
ResourceKey<StructureSet> holderKey = keyIndex.resolve(holder).orElseThrow(() ->
new IllegalStateException(
"An affected native structure-set exclusion graph has an unkeyed holder"));
ReboundStructureSetHolder scaledHolder = new ReboundStructureSetHolder(holderKey);
scaledByIdentity.put(holder, scaledHolder);
StructureSet originalSet = holder.value();
StructurePlacement originalPlacement = originalSet.placement();
Optional<AbstractSpreadingStructurePlacement.ExclusionZone> originalZone = exclusionZone(originalPlacement);
Optional<AbstractSpreadingStructurePlacement.ExclusionZone> scaledZone = originalZone;
if (originalZone.isPresent()) {
Holder<StructureSet> target = canonicalHolder(
originalZone.get().otherSet(), holdersByKey, keyIndex);
Holder<StructureSet> scaledTarget = scaleFrequencyHolder(
target,
importedStructures,
holdersByKey,
scaledByIdentity,
keyIndex);
if (scaledTarget != originalZone.get().otherSet()) {
scaledZone = Optional.of(new AbstractSpreadingStructurePlacement.ExclusionZone(
scaledTarget, originalZone.get().chunkCount()));
}
}
double multiplier = importedStructures.frequencyMultiplier(
structureSetKey(holder, keyIndex));
StructurePlacement scaledPlacement = multiplier == 1D && scaledZone.equals(originalZone)
? originalPlacement
: copyPlacement(originalPlacement, scaledZone, multiplier);
scaledHolder.bind(new StructureSet(originalSet.structures(), scaledPlacement));
return scaledHolder;
}
private static boolean dependsOnFrequencyOverride(
Holder<StructureSet> holder,
IrisImportedStructureControl importedStructures,
Map<String, Holder<StructureSet>> holdersByKey,
StructureSetKeyIndex keyIndex
) {
Set<Holder<StructureSet>> visited = Collections.newSetFromMap(new IdentityHashMap<>());
Holder<StructureSet> current = holder;
while (visited.add(current)) {
if (importedStructures.frequencyMultiplier(
structureSetKey(current, keyIndex)) != 1D) {
return true;
}
Optional<AbstractSpreadingStructurePlacement.ExclusionZone> zone =
exclusionZone(current.value().placement());
if (zone.isEmpty()) {
return false;
}
current = canonicalHolder(zone.get().otherSet(), holdersByKey, keyIndex);
}
return false;
}
private static Holder<StructureSet> canonicalHolder(
Holder<StructureSet> holder,
Map<String, Holder<StructureSet>> holdersByKey,
StructureSetKeyIndex keyIndex
) {
String key = structureSetKey(holder, keyIndex);
return key == null ? holder : holdersByKey.getOrDefault(key, holder);
}
private static Holder<StructureSet> replacementHolder(
Holder<StructureSet> original,
StructureSet replacement,
StructureSetKeyIndex keyIndex
) {
Optional<ResourceKey<StructureSet>> key = keyIndex.resolve(original);
if (key.isEmpty()) {
return Holder.direct(replacement);
}
ReboundStructureSetHolder holder = new ReboundStructureSetHolder(key.get());
holder.bind(replacement);
return holder;
}
private static StructurePlacement copyPlacement(
StructurePlacement placement,
Optional<AbstractSpreadingStructurePlacement.ExclusionZone> exclusionZone,
double frequencyMultiplier
) {
Vec3i locateOffset = (Vec3i) declaredFieldValue(
AbstractSpreadingStructurePlacement.class, placement, Vec3i.class);
AbstractSpreadingStructurePlacement.FrequencyReductionMethod frequencyReductionMethod =
(AbstractSpreadingStructurePlacement.FrequencyReductionMethod) declaredFieldValue(
AbstractSpreadingStructurePlacement.class,
placement,
AbstractSpreadingStructurePlacement.FrequencyReductionMethod.class);
float frequency = (float) declaredFieldValue(
AbstractSpreadingStructurePlacement.class, placement, float.class);
int salt = (int) declaredFieldValue(
AbstractSpreadingStructurePlacement.class, placement, int.class);
if (placement instanceof RandomSpreadStructurePlacement randomSpread) {
NativeStructureFrequencyScale scale = NativeStructureFrequencyScale.randomSpread(
frequency, randomSpread.spacing(), randomSpread.separation(), frequencyMultiplier);
return new RandomSpreadStructurePlacement(
locateOffset,
frequencyReductionMethod,
scale.frequency(),
salt,
exclusionZone,
scale.spacing(),
randomSpread.separation(),
randomSpread.spreadType());
}
if (placement.getClass() == ConcentricRingsStructurePlacement.class) {
ConcentricRingsStructurePlacement rings =
(ConcentricRingsStructurePlacement) placement;
float scaledFrequency = NativeStructureFrequencyScale.probability(
frequency, frequencyMultiplier);
return new ConcentricRingsStructurePlacement(
locateOffset,
frequencyReductionMethod,
scaledFrequency,
salt,
exclusionZone,
rings.distance(),
rings.spread(),
rings.count(),
rings.preferredBiomes());
}
throw new IllegalStateException("Unsupported native structure placement in an affected "
+ "frequency-override or datapack-scope graph: "
+ placement.getClass().getName());
}
private static Object declaredFieldValue(
Class<?> declaringType,
Object target,
Class<?> fieldType
) {
Field match = null;
for (Field field : declaringType.getDeclaredFields()) {
if (Modifier.isStatic(field.getModifiers()) || field.getType() != fieldType) {
continue;
}
if (match != null) {
throw new IllegalStateException("Ambiguous " + fieldType.getName() + " field on "
+ declaringType.getName());
}
match = field;
}
if (match == null) {
throw new IllegalStateException("Missing " + fieldType.getName() + " field on "
+ declaringType.getName());
}
try {
match.setAccessible(true);
return match.get(target);
} catch (IllegalAccessException e) {
throw new IllegalStateException("Could not read " + fieldType.getName() + " field on "
+ declaringType.getName(), e);
}
}
static final class StructureSetKeyIndex {
private final Map<List<StructureSet.StructureSelectionEntry>, ResourceKey<StructureSet>>
keysByEntries;
private StructureSetKeyIndex(
Map<List<StructureSet.StructureSelectionEntry>, ResourceKey<StructureSet>>
keysByEntries
) {
this.keysByEntries = keysByEntries;
}
private static StructureSetKeyIndex create(
Iterable<? extends Holder<StructureSet>> registeredStructureSets
) {
Map<List<StructureSet.StructureSelectionEntry>, ResourceKey<StructureSet>> keys =
new IdentityHashMap<>();
for (Holder<StructureSet> holder : registeredStructureSets) {
Optional<ResourceKey<StructureSet>> key = holder.unwrapKey();
if (key.isEmpty()) {
continue;
}
List<StructureSet.StructureSelectionEntry> entries = holder.value().structures();
ResourceKey<StructureSet> previous = keys.put(entries, key.get());
if (previous != null && !previous.equals(key.get())) {
throw new IllegalStateException(
"Multiple registered structure sets share the same entry list");
}
}
return new StructureSetKeyIndex(keys);
}
private Optional<ResourceKey<StructureSet>> resolve(Holder<StructureSet> holder) {
Optional<ResourceKey<StructureSet>> key = holder.unwrapKey();
if (key.isPresent()) {
return key;
}
return Optional.ofNullable(keysByEntries.get(holder.value().structures()));
}
}
record Selection(
List<Holder<StructureSet>> structureSets,
int retainedManagedSets,
int excludedManagedSets
) {
}
private static final class ReboundStructureSetHolder extends Holder.Reference<StructureSet> {
private ReboundStructureSetHolder(ResourceKey<StructureSet> key) {
super(Type.STAND_ALONE, new HolderOwner<>() {
}, key, null);
}
private void bind(StructureSet structureSet) {
bindValue(structureSet);
}
}
}
@@ -0,0 +1,441 @@
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
import net.minecraft.world.level.levelgen.placement.FeaturePlacer;
import art.arcane.iris.generation.runtime.DimensionStackContext;
import art.arcane.iris.generation.runtime.DimensionStackLayout;
import art.arcane.iris.nativegen.v26_3_R1.NativeGenerationWriteGuard;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.structure.nativegen.NativeFeatureGenerationPolicy;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.biome.IrisBiomeCustom;
import art.arcane.iris.generation.decoration.IrisDecorationStep;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.structure.nativegen.IrisImportedFeatureControl;
import art.arcane.iris.structure.object.IrisStaticObjectLayer;
import art.arcane.iris.spi.IrisLogging;
import it.unimi.dsi.fastutil.ints.IntArraySet;
import it.unimi.dsi.fastutil.ints.IntSet;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderSet;
import net.minecraft.core.Registry;
import net.minecraft.core.SectionPos;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeGenerationSettings;
import net.minecraft.world.level.biome.FeatureSorter;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.LevelChunkSection;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.RandomSupport;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.WorldgenRandom;
import net.minecraft.world.level.levelgen.XoroshiroRandomSource;
import net.minecraft.world.level.levelgen.placement.PlacedFeature;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.IntBinaryOperator;
/**
* Bukkit twin of the modded imported-feature stage. Same control, same semantics, same seeds: with
* {@code importedFeatures.enabled} the vanilla placed-feature pass runs over Iris terrain, and with the
* control off nothing here allocates or runs.
*
* <p>Iris does its own native structure pass and calls the delegate with {@code addVanillaDecorations=false},
* so the vanilla feature half never runs on its own. This reproduces that half only - structures are never
* placed twice.
*
* <p>Threading: called from {@code applyBiomeDecoration} on the worldgen thread that owns the chunk. The
* FEATURES chunk step is not parallel-safe.
*/
final class ImportedFeatureStage {
private static final String CYCLE_MARKER = "Feature order cycle found";
private final Engine engine;
private final ConcurrentHashMap<Integer, RuntimeFeatureState> runtimeStates = new ConcurrentHashMap<>();
ImportedFeatureStage(Engine engine) {
this.engine = engine;
}
/**
* Generation settings for one biome holder, mapping Iris custom biomes onto their vanilla derivative.
* Pass-through while the control is off, which is what keeps {@code BiomeFilter} behaving exactly as it
* does today.
*/
BiomeGenerationSettings generationSettings(Holder<Biome> biome) {
RuntimeFeatureState state = currentState();
FeatureTable table = state == null ? null : state.featureTable();
if (table == null) {
return null;
}
Holder<Biome> mapped = table.derivatives().get(holderKey(biome));
return mapped == null ? null : mapped.value().getGenerationSettings();
}
/**
* Builds the table on the first decorated chunk, or leaves the stage inert. A feature-order cycle is
* reported once here and degrades to features-off; it never reaches chunk generation as a crash.
*/
void prepare(WorldGenLevel level) {
IrisDimension dimension = engine.getDimension();
int runtimeId = engine.getCacheID();
if (settled(runtimeStates.get(runtimeId), dimension)) {
return;
}
synchronized (this) {
if (settled(runtimeStates.get(runtimeId), dimension)) {
return;
}
runtimeStates.put(runtimeId, build(level, dimension));
}
}
void evictRuntime(int runtimeId) {
runtimeStates.remove(runtimeId);
}
private RuntimeFeatureState currentState() {
IrisDimension dimension = engine.getDimension();
RuntimeFeatureState state = runtimeStates.get(engine.getCacheID());
return settled(state, dimension) ? state : null;
}
private static boolean settled(RuntimeFeatureState state, IrisDimension dimension) {
return state != null && state.dimension() == dimension;
}
private RuntimeFeatureState build(WorldGenLevel level, IrisDimension dimension) {
IrisImportedFeatureControl control;
try {
control = NativeFeatureGenerationPolicy.control(engine);
} catch (RuntimeException error) {
IrisLogging.error("Iris could not read importedFeatures for this dimension; features off: "
+ error);
return new RuntimeFeatureState(dimension, null);
}
if (!control.shouldGenerateFeatures()) {
return new RuntimeFeatureState(dimension, null);
}
FeatureTable built;
try {
built = buildTable(level, control, dimension);
} catch (Throwable error) {
IrisLogging.error("Iris importedFeatures is off for " + dimensionKey()
+ ": feature table construction failed: " + error);
return new RuntimeFeatureState(dimension, null);
}
if (built == null) {
return new RuntimeFeatureState(dimension, null);
}
IrisLogging.info("Iris importedFeatures on for " + dimensionKey() + ": " + built.biomes().size()
+ " biomes, " + built.steps().size() + " steps, " + built.derivatives().size()
+ " custom-biome derivative maps");
return new RuntimeFeatureState(dimension, built);
}
private FeatureTable buildTable(WorldGenLevel level, IrisImportedFeatureControl control,
IrisDimension dimension) {
Registry<Biome> registry = level.registryAccess().lookupOrThrow(Registries.BIOME);
Set<String> visibleKeys = visibleBiomeKeys();
// Registry-ordered walk, never a hash-ordered set: FeatureSorter's cycle detection walks this list and
// an unordered walk makes detection depend on JVM hash order.
List<Holder<Biome>> biomes = new ArrayList<>();
Map<String, Holder<Biome>> byKey = new HashMap<>();
registry.listElements().forEach((Holder.Reference<Biome> reference) -> {
String key = holderKey(reference);
if (key != null && visibleKeys.contains(key)) {
biomes.add(reference);
byKey.put(key, reference);
}
});
if (biomes.isEmpty()) {
IrisLogging.error("Iris importedFeatures is on but " + dimensionKey()
+ " exposes no registered biomes; features off");
return null;
}
Map<String, Holder<Biome>> derivatives = customBiomeDerivatives(registry, byKey);
List<FeatureSorter.StepFeatureData> steps;
try {
steps = FeatureSorter.buildFeaturesPerStep(biomes,
(Holder<Biome> biome) -> settingsFor(biome, derivatives).features(), true);
} catch (IllegalStateException error) {
String message = error.getMessage();
if (message == null || !message.contains(CYCLE_MARKER)) {
throw error;
}
IrisLogging.error("Iris importedFeatures is off for " + dimensionKey()
+ ": the registered placed features cannot be ordered. " + message
+ ". Remove or reorder the conflicting content, or leave importedFeatures.enabled false.");
return null;
}
boolean filtered = control.getDisabled() != null && !control.getDisabled().isEmpty();
return new FeatureTable(dimension, control, List.copyOf(biomes), Set.copyOf(biomes),
Map.copyOf(byKey), steps, Map.copyOf(derivatives), filtered);
}
/**
* Every biome key Iris can write into a chunk section: the structure derivative, the raw derivative, every
* scatter entry, and every generated custom biome.
*/
private Set<String> visibleBiomeKeys() {
Set<String> keys = new LinkedHashSet<>();
for (IrisBiome irisBiome : engine.getDimension().getReachableBiomes(engine)) {
addKey(keys, irisBiome.getStructureDerivativeKey());
addKey(keys, irisBiome.getDerivativeKey());
addKey(keys, irisBiome.getVanillaDerivativeKey());
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, engine.getData().customBiomeResourceKey(engine.getDimension(), customBiome));
}
}
return keys;
}
/**
* Maps every generated Iris custom biome key onto the registry holder of its Iris biome's vanilla
* derivative. The custom biome's own datapack JSON carries no features by design.
*/
private Map<String, Holder<Biome>> customBiomeDerivatives(Registry<Biome> registry,
Map<String, Holder<Biome>> byKey) {
Map<String, Holder<Biome>> derivatives = new HashMap<>();
for (IrisBiome irisBiome : engine.getDimension().getReachableBiomes(engine)) {
if (!irisBiome.isCustom()) {
continue;
}
String derivativeKey = normalizeKey(irisBiome.getVanillaDerivativeKey());
// Resolve from the registry, not only from the visible biome set: a sea or shore biome's structure
// derivative is rewritten away from its vanilla derivative, so the derivative whose features we
// want is not always a biome Iris can emit.
Holder<Biome> derivative = byKey.get(derivativeKey);
if (derivative == null) {
derivative = resolveHolder(registry, derivativeKey);
}
if (derivative == null) {
IrisLogging.warn("Iris importedFeatures: vanilla derivative " + derivativeKey + " of biome "
+ irisBiome.getLoadKey()
+ " is not registered; its custom biomes generate no imported features");
continue;
}
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
derivatives.put(
engine.getData().customBiomeResourceKey(engine.getDimension(), customBiome),
derivative
);
}
}
return derivatives;
}
private static Holder<Biome> resolveHolder(Registry<Biome> registry, String key) {
if (registry == null || key == null || key.isBlank()) {
return null;
}
Identifier identifier = Identifier.tryParse(key);
if (identifier == null) {
return null;
}
return registry.get(identifier).<Holder<Biome>>map((Holder.Reference<Biome> reference) -> reference)
.orElse(null);
}
private static BiomeGenerationSettings settingsFor(Holder<Biome> biome,
Map<String, Holder<Biome>> derivatives) {
Holder<Biome> mapped = derivatives.get(holderKey(biome));
return mapped == null
? biome.value().getGenerationSettings()
: mapped.value().getGenerationSettings();
}
/**
* Runs the vanilla placed-feature pass for one chunk on the calling worldgen thread. A no-op while the
* control is disabled or the table degraded.
*/
void run(WorldGenLevel level, ChunkAccess chunk, ChunkGenerator owner) {
RuntimeFeatureState state = currentState();
FeatureTable table = state == null ? null : state.featureTable();
if (table == null) {
return;
}
ChunkPos centerPos = chunk.getPos();
SectionPos sectionPos = SectionPos.of(centerPos, level.getMinSectionY());
BlockPos origin = sectionPos.origin();
Registry<PlacedFeature> featureRegistry = level.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE);
List<FeatureSorter.StepFeatureData> steps = table.steps();
WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed()));
long decorationSeed = random.setDecorationSeed(level.getSeed(), origin.getX(), origin.getZ());
IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData());
int staticMinY = engine.getMinHeight();
DimensionStackContext stackContext = engine.getDimensionStackContext();
Set<Holder<Biome>> chunkBiomes = chunkBiomes(level, sectionPos, table, stackContext);
IntBinaryOperator surfaceFirstFreeY = stackContext == null
? (x, z) -> level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z)
: (x, z) -> Engine.hostHeight(engine, x, z, false) + staticMinY + 1;
IntBinaryOperator floorFirstFreeY = stackContext == null
? (x, z) -> level.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, x, z)
: (x, z) -> Engine.hostHeight(engine, x, z, true) + staticMinY + 1;
WorldGenLevel placementLevel = staticObjects.isEmpty() && stackContext == null
? level
: NativeStructureWorldgenAccess.create(
level, centerPos,
surfaceFirstFreeY,
floorFirstFreeY,
stackContext != null,
NativeGenerationWriteGuard.protectedPositions(staticObjects, stackContext, staticMinY));
try {
for (int stepIndex = 0; stepIndex < steps.size(); stepIndex++) {
if (!table.control().shouldGenerateStep(IrisDecorationStep.byOrdinal(stepIndex))) {
continue;
}
placeStep(placementLevel, table, steps.get(stepIndex), featureRegistry, chunkBiomes, owner,
random, decorationSeed, origin, stepIndex);
}
} catch (Throwable error) {
throw new IllegalStateException("Iris imported feature placement failed for chunk "
+ centerPos.x() + "," + centerPos.z(), error);
} finally {
level.setCurrentlyGenerating(null);
}
}
private void placeStep(WorldGenLevel level, FeatureTable table, FeatureSorter.StepFeatureData stepData,
Registry<PlacedFeature> featureRegistry, Set<Holder<Biome>> chunkBiomes,
ChunkGenerator owner, WorldgenRandom random, long decorationSeed,
BlockPos origin, int stepIndex) {
IntSet stepFeatures = new IntArraySet();
for (Holder<Biome> biome : chunkBiomes) {
List<HolderSet<PlacedFeature>> biomeFeatures = settingsFor(biome, table.derivatives()).features();
if (stepIndex >= biomeFeatures.size()) {
continue;
}
for (Holder<PlacedFeature> feature : biomeFeatures.get(stepIndex)) {
stepFeatures.add(stepData.indexMapping().applyAsInt(feature.value()));
}
}
if (stepFeatures.isEmpty()) {
return;
}
// Sorted global indices: identical ordering to vanilla, and each feature's seed comes from its own
// global index, so denying one feature never shifts another.
int[] featureIndices = stepFeatures.toIntArray();
Arrays.sort(featureIndices);
for (int globalIndex : featureIndices) {
PlacedFeature feature = stepData.features().get(globalIndex);
if (table.filtered()) {
Identifier featureId = featureRegistry.getKey(feature);
if (featureId != null && !table.control().shouldGenerate(featureId.toString())) {
continue;
}
}
random.setFeatureSeed(decorationSeed, globalIndex, stepIndex);
level.setCurrentlyGenerating(() -> describeFeature(featureRegistry, feature));
new FeaturePlacer(level, owner).placeWithBiomeCheck(feature, random, origin);
}
}
private static String describeFeature(Registry<PlacedFeature> registry, PlacedFeature feature) {
Identifier id = registry.getKey(feature);
return id == null ? feature.toString() : id.toString();
}
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());
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<>();
for (Holder<Biome> biome : collected) {
if (table.biomeSet().contains(biome)) {
present.add(biome);
continue;
}
String key = holderKey(biome);
Holder<Biome> canonical = key == null ? null : table.byKey().get(key);
if (canonical != null) {
present.add(canonical);
}
}
return present;
}
private static void addKey(Set<String> keys, String key) {
String normalized = normalizeKey(key);
if (normalized != null) {
keys.add(normalized);
}
}
private String dimensionKey() {
IrisDimension dimension = engine.getDimension();
return dimension == null ? "<unbound>" : dimension.getLoadKey();
}
private static String holderKey(Holder<Biome> holder) {
return holder.unwrapKey()
.map(key -> key.identifier().toString().toLowerCase(Locale.ROOT))
.orElse(null);
}
private static String normalizeKey(String key) {
Identifier identifier = key == null ? null : Identifier.tryParse(key);
return identifier == null ? null : identifier.toString().toLowerCase(Locale.ROOT);
}
private record FeatureTable(IrisDimension dimension, IrisImportedFeatureControl control,
List<Holder<Biome>> biomes, Set<Holder<Biome>> biomeSet,
Map<String, Holder<Biome>> byKey,
List<FeatureSorter.StepFeatureData> steps,
Map<String, Holder<Biome>> derivatives, boolean filtered) {
}
private record RuntimeFeatureState(IrisDimension dimension, FeatureTable featureTable) {
}
}
@@ -0,0 +1,132 @@
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
import art.arcane.iris.spi.IrisLogging;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.Holder;
import net.minecraft.server.level.WorldGenRegion;
import net.minecraft.util.Mth;
import net.minecraft.util.RandomSource;
import net.minecraft.util.random.WeightedList;
import net.minecraft.world.attribute.EnvironmentAttributes;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.EntitySpawnReason;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.entity.MobCategory;
import net.minecraft.world.entity.SpawnGroupData;
import net.minecraft.world.entity.SpawnPlacements;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelReader;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.MobSpawnSettings;
import net.minecraft.world.level.gamerules.GameRules;
import org.bukkit.event.entity.CreatureSpawnEvent;
import java.util.Optional;
final class InitialMobSpawner {
private InitialMobSpawner() {
}
static void spawn(
WorldGenRegion level, Holder<Biome> biome, ChunkPos chunkPos, RandomSource random
) {
if (level.getLevel().getGameRules().get(GameRules.SPAWN_MOBS)) {
MobSpawnSettings mobSettings = biome.value().getAttributes().applyModifier(
EnvironmentAttributes.NATURAL_MOB_SPAWNS, EnvironmentAttributes.NATURAL_MOB_SPAWNS.defaultValue());
WeightedList<MobSpawnSettings.SpawnerData> mobs = mobSettings.getMobsToSpawn(MobCategory.CREATURE);
if (!mobs.isEmpty()) {
float creatureProbability = biome.value().getAttributes().applyModifier(
EnvironmentAttributes.CREATURE_WORLD_GEN_SPAWN_PROBABILITY,
EnvironmentAttributes.CREATURE_WORLD_GEN_SPAWN_PROBABILITY.defaultValue());
int xo = chunkPos.getMinBlockX();
int zo = chunkPos.getMinBlockZ();
while (random.nextFloat() < creatureProbability) {
Optional<MobSpawnSettings.SpawnerData> nextSpawnerData = mobs.getRandom(random);
if (!nextSpawnerData.isEmpty()) {
MobSpawnSettings.SpawnerData spawnerData = nextSpawnerData.get();
int count = spawnerData.count().sample(random);
SpawnGroupData groupSpawnData = null;
int x = xo + random.nextInt(16);
int z = zo + random.nextInt(16);
int startX = x;
int startZ = z;
for (int i = 0; i < count; i++) {
boolean success = false;
for (int attempts = 0; !success && attempts < 4; attempts++) {
BlockPos pos = getTopNonCollidingPos(level, spawnerData.type(), x, z);
if (spawnerData.type().canSummon() && SpawnPlacements.isSpawnPositionOk(spawnerData.type(), level, pos)) {
float width = spawnerData.type().getWidth();
double fx = Mth.clamp(x, (double)xo + width, xo + 16.0 - width);
double fz = Mth.clamp(z, (double)zo + width, zo + 16.0 - width);
if (!level.noCollision(spawnerData.type().getSpawnAABB(fx, pos.getY(), fz))
|| !SpawnPlacements.checkSpawnRules(
spawnerData.type(),
level,
EntitySpawnReason.CHUNK_GENERATION,
BlockPos.containing(fx, pos.getY(), fz),
level.getRandom()
)) {
continue;
}
Entity entity;
try {
entity = spawnerData.type().create(level.getLevel(), EntitySpawnReason.NATURAL);
} catch (Exception e) {
IrisLogging.reportError("Failed to create initial mob at chunk " + chunkPos, e);
continue;
}
if (entity == null) {
continue;
}
entity.snapTo(fx, pos.getY(), fz, random.nextFloat() * 360.0F, 0.0F);
if (entity instanceof Mob mob
&& mob.checkSpawnRules(level, EntitySpawnReason.CHUNK_GENERATION)
&& mob.checkSpawnObstruction(level)) {
groupSpawnData = mob.finalizeSpawn(
level, level.getCurrentDifficultyAt(mob.blockPosition()), EntitySpawnReason.CHUNK_GENERATION, groupSpawnData
);
level.addFreshEntityWithPassengers(mob, CreatureSpawnEvent.SpawnReason.CHUNK_GEN);
success = true;
}
}
x += random.nextInt(5) - random.nextInt(5);
for (z += random.nextInt(5) - random.nextInt(5);
x < xo || x >= xo + 16 || z < zo || z >= zo + 16;
z = startZ + random.nextInt(5) - random.nextInt(5)
) {
x = startX + random.nextInt(5) - random.nextInt(5);
}
}
}
}
}
}
}
}
private static BlockPos getTopNonCollidingPos(LevelReader level, EntityType<?> type, int x, int z) {
int levelHeight = level.getHeight(SpawnPlacements.getHeightmapType(type), x, z);
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(x, levelHeight, z);
if (level.dimensionType().hasCeiling()) {
do {
pos.move(Direction.DOWN);
} while (!level.getBlockState(pos).isAir());
do {
pos.move(Direction.DOWN);
} while (level.getBlockState(pos).isAir() && pos.getY() > level.getMinY());
}
return SpawnPlacements.getPlacementType(type).adjustSpawnPosition(level, pos.immutable());
}
}
@@ -0,0 +1,142 @@
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
import ca.spottedleaf.moonrise.common.util.TickThread;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.SectionPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.ai.village.poi.PoiManager;
import net.minecraft.world.entity.ai.village.poi.PoiRecord;
import net.minecraft.world.entity.ai.village.poi.PoiSection;
import net.minecraft.world.entity.ai.village.poi.PoiType;
import net.minecraft.world.entity.ai.village.poi.PoiTypes;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ImposterProtoChunk;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.level.chunk.LevelChunkSection;
import net.minecraft.world.level.chunk.ProtoChunk;
import org.bukkit.NamespacedKey;
import org.bukkit.persistence.PersistentDataType;
import java.util.Arrays;
import java.util.Optional;
final class NativeStructurePoiUpdates {
private static final NamespacedKey DIRTY_SECTIONS = new NamespacedKey("iris", "native_poi_sections");
private final int skipPoiFlag;
NativeStructurePoiUpdates(int skipPoiFlag) {
this.skipPoiFlag = skipPoiFlag;
}
static NativeStructurePoiUpdates create() {
try {
return new NativeStructurePoiUpdates(Block.class.getField("UPDATE_SKIP_POI").getInt(null));
} catch (NoSuchFieldException e) {
return null;
} catch (IllegalAccessException e) {
throw new ExceptionInInitializerError(e);
}
}
boolean setBlock(WorldGenLevel world, BlockPos position, BlockState state, int flags, int limit) {
if ((flags & skipPoiFlag) != 0) {
return world.setBlock(position, state, flags, limit);
}
ChunkAccess chunk = world.getChunk(position);
if (!(chunk instanceof ProtoChunk)) {
return world.setBlock(position, state, flags, limit);
}
if (!(chunk instanceof ImposterProtoChunk)
&& !PoiTypes.forState(chunk.getBlockState(position)).equals(PoiTypes.forState(state))) {
markSection(chunk, chunk.getSectionIndex(position.getY()));
}
return world.setBlock(position, state, flags | skipPoiFlag, limit);
}
static void reconcile(LevelChunk chunk) {
long[] sections = chunk.persistentDataContainer.get(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY);
if (sections == null) {
return;
}
validateSections(chunk, sections);
ServerLevel world = (ServerLevel) chunk.getLevel();
TickThread.ensureTickThread(world, chunk.getPos(), "Native structure POIs require the chunk owning thread");
PoiManager manager = world.getPoiManager();
for (int word = 0; word < sections.length; word++) {
long remaining = sections[word];
while (remaining != 0) {
int index = (word << 6) + Long.numberOfTrailingZeros(remaining);
reconcileSection(chunk, manager, index);
remaining &= remaining - 1;
}
}
chunk.persistentDataContainer.remove(DIRTY_SECTIONS);
chunk.markUnsaved();
}
private static void markSection(ChunkAccess chunk, int index) {
long[] sections = chunk.persistentDataContainer.get(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY);
int word = index >>> 6;
long bit = 1L << (index & 63);
if (sections == null) {
sections = new long[word + 1];
} else {
validateSections(chunk, sections);
if (word < sections.length && (sections[word] & bit) != 0) {
return;
}
if (word >= sections.length) {
sections = Arrays.copyOf(sections, word + 1);
}
}
sections[word] |= bit;
chunk.persistentDataContainer.set(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY, sections);
}
private static void validateSections(ChunkAccess chunk, long[] sections) {
int count = chunk.getSectionsCount();
int words = (count + 63) >>> 6;
if (sections.length > words
|| sections.length == words && (count & 63) != 0
&& (sections[words - 1] >>> (count & 63)) != 0) {
throw new IllegalArgumentException("Native POI repair sections exceed chunk height at " + chunk.getPos());
}
}
private static void reconcileSection(LevelChunk chunk, PoiManager manager, int index) {
SectionPos sectionPosition = SectionPos.of(chunk.getPos(), chunk.getSectionYFromSectionIndex(index));
Optional<PoiSection> existing = manager.getOrLoad(sectionPosition.asLong());
if (existing.isPresent()) {
for (PoiRecord record : existing.get().getRecords(type -> true, PoiManager.Occupancy.ANY).toList()) {
if (!PoiTypes.forState(chunk.getBlockState(record.getPos())).equals(Optional.of(record.getPoiType()))) {
manager.remove(record.getPos());
}
}
}
LevelChunkSection section = chunk.getSection(index);
if (!section.maybeHas(PoiTypes::hasPoi)) {
return;
}
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
for (int y = 0; y < 16; y++) {
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
Optional<Holder<PoiType>> type = PoiTypes.forState(section.getBlockState(x, y, z));
if (type.isEmpty()) {
continue;
}
position.set(sectionPosition.minBlockX() + x, sectionPosition.minBlockY() + y,
sectionPosition.minBlockZ() + z);
if (!manager.getType(position).equals(type)) {
manager.add(position.immutable(), type.get());
}
}
}
}
}
}
@@ -0,0 +1,606 @@
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
import art.arcane.iris.nativegen.v26_3_R1.WorldgenTerrainHeightmaps;
import it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.Holder;
import net.minecraft.core.QuartPos;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.particles.ParticleOptions;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.WorldGenRegion;
import net.minecraft.sounds.SoundEvent;
import net.minecraft.sounds.SoundSource;
import net.minecraft.util.Mth;
import net.minecraft.util.RandomSource;
import net.minecraft.world.DifficultyInstance;
import net.minecraft.world.attribute.EnvironmentAttributeReader;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.flag.FeatureFlagSet;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LightLayer;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeManager;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.border.WorldBorder;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkSource;
import net.minecraft.world.level.chunk.EmptyLevelChunk;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.entity.EntityTypeTest;
import net.minecraft.world.level.gameevent.GameEvent;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.lighting.LevelLightEngine;
import net.minecraft.world.level.material.Fluid;
import net.minecraft.world.level.material.FluidState;
import net.minecraft.world.level.storage.LevelData;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
import net.minecraft.world.ticks.LevelTickAccess;
import net.minecraft.world.ticks.ScheduledTick;
import org.bukkit.event.entity.CreatureSpawnEvent;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.IntBinaryOperator;
import java.util.function.Predicate;
import java.util.function.Supplier;
final class NativeStructureWorldgenAccess implements WorldGenLevel {
private static final int WRITE_RADIUS = 1;
private static final NativeStructurePoiUpdates POI_UPDATES = NativeStructurePoiUpdates.create();
private final WorldGenLevel delegate;
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;
private final AABB generationBounds;
private final LevelTickAccess<Block> blockTicks;
private final LevelTickAccess<Fluid> fluidTicks;
private final Long2LongOpenHashMap terrainHeights;
private final Long2ObjectOpenHashMap<ChunkAccess> outsideChunks;
private NativeStructureWorldgenAccess(WorldGenLevel delegate, Boundary boundary) {
this.delegate = Objects.requireNonNull(delegate, "Native structure world access requires a delegate");
this.generationCenter = Objects.requireNonNull(
boundary.generationCenter(), "Native structure world access requires a generation center");
this.surfaceFirstFreeY = Objects.requireNonNull(
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());
this.fallbackBiome = delegate.getBiome(biomeSample);
this.biomeManager = delegate.getBiomeManager().withDifferentSource(this);
this.generationBounds = new AABB(
generationCenter.getMinBlockX() - 16,
delegate.getMinY(),
generationCenter.getMinBlockZ() - 16,
generationCenter.getMaxBlockX() + 17,
delegate.getMinY() + delegate.getHeight(),
generationCenter.getMaxBlockZ() + 17);
this.blockTicks = new BoundedTickAccess<>(delegate.getBlockTicks(), this::isWritable);
this.fluidTicks = new BoundedTickAccess<>(delegate.getFluidTicks(), this::isWritable);
this.terrainHeights = new Long2LongOpenHashMap();
this.terrainHeights.defaultReturnValue(Long.MIN_VALUE);
this.outsideChunks = new Long2ObjectOpenHashMap<>();
}
static NativeStructureWorldgenAccess create(WorldGenLevel delegate, ChunkPos generationCenter,
IntBinaryOperator surfaceFirstFreeY,
IntBinaryOperator floorFirstFreeY,
boolean virtualizeHeights,
Predicate<BlockPos> protectedPosition) {
return new NativeStructureWorldgenAccess(delegate, new Boundary(
generationCenter, surfaceFirstFreeY, floorFirstFreeY, virtualizeHeights, protectedPosition));
}
@Override
public long getSeed() {
return delegate.getSeed();
}
@Override
public boolean ensureCanWrite(BlockPos position) {
return isWritable(position) && delegate.ensureCanWrite(position);
}
@Override
public void setCurrentlyGenerating(Supplier<String> description) {
delegate.setCurrentlyGenerating(description);
}
@Override
public ServerLevel getLevel() {
return delegate.getLevel();
}
@Override
public DifficultyInstance getCurrentDifficultyAt(BlockPos position) {
if (isReadable(position)) {
return delegate.getCurrentDifficultyAt(position);
}
return delegate.getCurrentDifficultyAt(generationCenter.getMiddleBlockPosition(
Mth.clamp(position.getY(), getMinY(), getMaxY() - 1)));
}
@Override
public long nextSubTickCount() {
return delegate.nextSubTickCount();
}
@Override
public LevelTickAccess<Block> getBlockTicks() {
return blockTicks;
}
@Override
public LevelTickAccess<Fluid> getFluidTicks() {
return fluidTicks;
}
@Override
public LevelData getLevelData() {
return delegate.getLevelData();
}
@Override
public MinecraftServer getServer() {
return delegate.getServer();
}
@Override
public ChunkSource getChunkSource() {
return delegate.getChunkSource();
}
@Override
public RandomSource getRandom() {
return delegate.getRandom();
}
@Override
public void updateNeighborsAt(BlockPos position, Block block) {
if (isWritableNeighbourhood(position)) {
delegate.updateNeighborsAt(position, block);
}
}
@Override
public void neighborShapeChanged(Direction direction, BlockPos position,
BlockPos neighbourPosition, BlockState neighbourState,
int updateFlags, int updateLimit) {
if (isWritable(position) && isWritable(neighbourPosition)) {
delegate.neighborShapeChanged(
direction, position, neighbourPosition, neighbourState, updateFlags, updateLimit);
}
}
@Override
public void playSound(Entity source, BlockPos position, SoundEvent sound,
SoundSource soundSource, float volume, float pitch) {
if (isWritable(position)) {
delegate.playSound(source, position, sound, soundSource, volume, pitch);
}
}
@Override
public void addParticle(ParticleOptions particle, double x, double y, double z,
double velocityX, double velocityY, double velocityZ) {
if (isWritable(BlockPos.containing(x, y, z))) {
delegate.addParticle(particle, x, y, z, velocityX, velocityY, velocityZ);
}
}
@Override
public void levelEvent(Entity source, int eventId, BlockPos position, int data) {
if (isWritable(position)) {
delegate.levelEvent(source, eventId, position, data);
}
}
@Override
public void gameEvent(Holder<GameEvent> event, Vec3 position, GameEvent.Context context) {
if (isWritable(BlockPos.containing(position))) {
delegate.gameEvent(event, position, context);
}
}
@Override
public ChunkAccess getChunk(int chunkX, int chunkZ) {
if (isInsideGenerationRegion(chunkX, chunkZ)) {
return delegate.getChunk(chunkX, chunkZ);
}
return outsideChunk(chunkX, chunkZ);
}
@Override
public ChunkAccess getChunk(int chunkX, int chunkZ, ChunkStatus status, boolean create) {
if (isInsideGenerationRegion(chunkX, chunkZ)) {
return delegate.getChunk(chunkX, chunkZ, status, create);
}
return create ? outsideChunk(chunkX, chunkZ) : null;
}
@Override
public ChunkAccess getChunkIfLoadedImmediately(int chunkX, int chunkZ) {
if (!isInsideGenerationRegion(chunkX, chunkZ)) {
return null;
}
return delegate.getChunkIfLoadedImmediately(chunkX, chunkZ);
}
@Override
public boolean hasChunk(int chunkX, int chunkZ) {
return isInsideGenerationRegion(chunkX, chunkZ) && delegate.hasChunk(chunkX, chunkZ);
}
@Override
public int getHeight(Heightmap.Types type, int x, int z) {
if (!virtualizeHeights && isInsideGenerationRegion(x >> 4, z >> 4)) {
return delegate.getHeight(type, x, z);
}
return height(type, x, z);
}
@Override
public int getSkyDarken() {
return delegate.getSkyDarken();
}
@Override
public BiomeManager getBiomeManager() {
return biomeManager;
}
@Override
public Holder<Biome> getNoiseBiome(int quartX, int quartY, int quartZ) {
return getUncachedNoiseBiome(quartX, quartY, quartZ);
}
@Override
public Holder<Biome> getUncachedNoiseBiome(int quartX, int quartY, int quartZ) {
if (isInsideGenerationRegion(QuartPos.toSection(quartX), QuartPos.toSection(quartZ))) {
return delegate.getUncachedNoiseBiome(quartX, quartY, quartZ);
}
return fallbackBiome;
}
@Override
public boolean isClientSide() {
return delegate.isClientSide();
}
@Override
public int getSeaLevel() {
return delegate.getSeaLevel();
}
@Override
public DimensionType dimensionType() {
return delegate.dimensionType();
}
@Override
public int getMinY() {
return delegate.getMinY();
}
@Override
public int getHeight() {
return delegate.getHeight();
}
@Override
public RegistryAccess registryAccess() {
return delegate.registryAccess();
}
@Override
public FeatureFlagSet enabledFeatures() {
return delegate.enabledFeatures();
}
@Override
public EnvironmentAttributeReader environmentAttributes() {
return delegate.environmentAttributes();
}
@Override
public LevelLightEngine getLightEngine() {
return delegate.getLightEngine();
}
@Override
public int getBrightness(LightLayer layer, BlockPos position) {
if (isReadable(position)) {
return delegate.getBrightness(layer, position);
}
return layer == LightLayer.SKY && canSeeSky(position) ? 15 : 0;
}
@Override
public int getRawBrightness(BlockPos position, int ambientDarkening) {
if (isReadable(position)) {
return delegate.getRawBrightness(position, ambientDarkening);
}
return Math.max(0, getBrightness(LightLayer.SKY, position) - ambientDarkening);
}
@Override
public boolean canSeeSky(BlockPos position) {
if (isReadable(position)) {
return delegate.canSeeSky(position);
}
return position.getY() >= height(Heightmap.Types.WORLD_SURFACE_WG, position.getX(), position.getZ());
}
@Override
public WorldBorder getWorldBorder() {
return delegate.getWorldBorder();
}
@Override
public BlockGetter getChunkForCollisions(int chunkX, int chunkZ) {
if (isInsideGenerationRegion(chunkX, chunkZ)) {
return delegate.getChunkForCollisions(chunkX, chunkZ);
}
return outsideChunk(chunkX, chunkZ);
}
@Override
public BlockEntity getBlockEntity(BlockPos position) {
return isWritable(position) ? delegate.getBlockEntity(position) : null;
}
@Override
public <T extends BlockEntity> Optional<T> getBlockEntity(
BlockPos position, BlockEntityType<T> type) {
return isWritable(position) ? delegate.getBlockEntity(position, type) : Optional.empty();
}
@Override
public BlockState getBlockState(BlockPos position) {
return isReadable(position) ? delegate.getBlockState(position) : terrainState(position);
}
@Override
public BlockState getBlockStateIfLoaded(BlockPos position) {
return isReadable(position) ? delegate.getBlockStateIfLoaded(position) : null;
}
@Override
public FluidState getFluidState(BlockPos position) {
return isReadable(position) ? delegate.getFluidState(position) : terrainState(position).getFluidState();
}
@Override
public FluidState getFluidIfLoaded(BlockPos position) {
return isReadable(position) ? delegate.getFluidIfLoaded(position) : null;
}
@Override
public List<Entity> getEntities(Entity source, AABB area, Predicate<? super Entity> predicate) {
AABB boundedArea = boundedArea(area);
return boundedArea == null ? List.of() : delegate.getEntities(source, boundedArea, predicate);
}
@Override
public <T extends Entity> List<T> getEntities(
EntityTypeTest<Entity, T> type,
AABB area, Predicate<? super T> predicate) {
AABB boundedArea = boundedArea(area);
return boundedArea == null ? List.of() : delegate.getEntities(type, boundedArea, predicate);
}
@Override
public List<? extends Player> players() {
return delegate.players();
}
@Override
public boolean isStateAtPosition(BlockPos position, Predicate<BlockState> predicate) {
return predicate.test(getBlockState(position));
}
@Override
public boolean isFluidAtPosition(BlockPos position, Predicate<FluidState> predicate) {
return predicate.test(getFluidState(position));
}
@Override
public BlockPos getHeightmapPos(Heightmap.Types type, BlockPos position) {
return position.atY(getHeight(type, position.getX(), position.getZ()));
}
@Override
public boolean setBlock(BlockPos position, BlockState state, int updateFlags, int updateLimit) {
return isWritable(position)
&& (POI_UPDATES != null
? POI_UPDATES.setBlock(delegate, position, state, updateFlags, updateLimit)
: delegate.setBlock(position, state, updateFlags, updateLimit));
}
@Override
public boolean removeBlock(BlockPos position, boolean move) {
if (!isWritable(position)) {
return false;
}
return POI_UPDATES != null && delegate instanceof WorldGenRegion
? setBlock(position, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL, Block.UPDATE_LIMIT)
: delegate.removeBlock(position, move);
}
@Override
public boolean destroyBlock(BlockPos position, boolean drop, Entity source, int updateLimit) {
if (!isWritable(position)) {
return false;
}
return POI_UPDATES != null && delegate instanceof WorldGenRegion
? !delegate.getBlockState(position).isAir()
&& setBlock(position, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL, updateLimit)
: delegate.destroyBlock(position, drop, source, updateLimit);
}
@Override
public boolean addFreshEntity(Entity entity) {
return isWritable(entity.blockPosition()) && delegate.addFreshEntity(entity);
}
@Override
public boolean addFreshEntity(Entity entity, CreatureSpawnEvent.SpawnReason reason) {
return isWritable(entity.blockPosition()) && delegate.addFreshEntity(entity, reason);
}
private boolean isReadable(BlockPos position) {
return isInsideGenerationRegion(position.getX() >> 4, position.getZ() >> 4)
&& !isOutsideBuildHeight(position);
}
private boolean isWritable(BlockPos position) {
return isReadable(position) && !protectedPosition.test(position);
}
private boolean isWritableNeighbourhood(BlockPos position) {
return isWritable(position)
&& isWritable(position.north())
&& isWritable(position.south())
&& isWritable(position.east())
&& isWritable(position.west())
&& isWritable(position.above())
&& isWritable(position.below());
}
private AABB boundedArea(AABB area) {
double minX = Math.max(area.minX, generationBounds.minX);
double minY = Math.max(area.minY, generationBounds.minY);
double minZ = Math.max(area.minZ, generationBounds.minZ);
double maxX = Math.min(area.maxX, generationBounds.maxX);
double maxY = Math.min(area.maxY, generationBounds.maxY);
double maxZ = Math.min(area.maxZ, generationBounds.maxZ);
if (minX >= maxX || minY >= maxY || minZ >= maxZ) {
return null;
}
if (minX == area.minX && minY == area.minY && minZ == area.minZ
&& maxX == area.maxX && maxY == area.maxY && maxZ == area.maxZ) {
return area;
}
return new AABB(minX, minY, minZ, maxX, maxY, maxZ);
}
boolean isInsideGenerationRegion(int chunkX, int chunkZ) {
return Math.abs(chunkX - generationCenter.x()) <= WRITE_RADIUS
&& Math.abs(chunkZ - generationCenter.z()) <= WRITE_RADIUS;
}
@Override
public boolean isOutsideBuildHeight(BlockPos position) {
return position.getY() < getMinY() || position.getY() >= getMaxY();
}
private BlockState terrainState(BlockPos position) {
if (isOutsideBuildHeight(position)) {
return Blocks.VOID_AIR.defaultBlockState();
}
long heights = heights(position.getX(), position.getZ());
int surface = (int) (heights >> 32);
int floor = (int) heights;
if (position.getY() < floor) {
return Blocks.STONE.defaultBlockState();
}
if (position.getY() < surface) {
return Blocks.WATER.defaultBlockState();
}
return Blocks.AIR.defaultBlockState();
}
private int height(Heightmap.Types type, int x, int z) {
long heights = heights(x, z);
if (type == Heightmap.Types.OCEAN_FLOOR || type == Heightmap.Types.OCEAN_FLOOR_WG) {
return (int) heights;
}
return (int) (heights >> 32);
}
private long heights(int x, int z) {
long key = ((long) x << 32) ^ (z & 0xffffffffL);
long cached = terrainHeights.get(key);
if (cached != Long.MIN_VALUE) {
return cached;
}
int floor = Mth.clamp(floorFirstFreeY.applyAsInt(x, z), getMinY(), getMaxY());
int surface = Mth.clamp(surfaceFirstFreeY.applyAsInt(x, z), floor, getMaxY());
long heights = ((long) surface << 32) | (floor & 0xffffffffL);
terrainHeights.put(key, heights);
return heights;
}
private ChunkAccess outsideChunk(int chunkX, int chunkZ) {
long key = ChunkPos.pack(chunkX, chunkZ);
ChunkAccess cached = outsideChunks.get(key);
if (cached != null) {
return cached;
}
EmptyLevelChunk chunk = new EmptyLevelChunk(getLevel(), new ChunkPos(chunkX, chunkZ), fallbackBiome);
WorldgenTerrainHeightmaps.primeTerrain(chunk, surfaceFirstFreeY, floorFirstFreeY);
outsideChunks.put(key, chunk);
return chunk;
}
private record Boundary(ChunkPos generationCenter,
IntBinaryOperator surfaceFirstFreeY,
IntBinaryOperator floorFirstFreeY,
boolean virtualizeHeights,
Predicate<BlockPos> protectedPosition) {
}
private static final class BoundedTickAccess<T> implements LevelTickAccess<T> {
private final LevelTickAccess<T> delegate;
private final Predicate<BlockPos> writable;
private BoundedTickAccess(LevelTickAccess<T> delegate, Predicate<BlockPos> writable) {
this.delegate = delegate;
this.writable = writable;
}
@Override
public void schedule(ScheduledTick<T> tick) {
if (writable.test(tick.pos())) {
delegate.schedule(tick);
}
}
@Override
public boolean hasScheduledTick(BlockPos position, T type) {
return writable.test(position) && delegate.hasScheduledTick(position, type);
}
@Override
public int count() {
return delegate.count();
}
@Override
public boolean willTickThisTick(BlockPos position, T type) {
return writable.test(position) && delegate.willTickThisTick(position, type);
}
}
}
@@ -0,0 +1,61 @@
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
import art.arcane.iris.platform.agent.Agent;
import it.unimi.dsi.fastutil.shorts.ShortList;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.matcher.ElementMatchers;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.server.level.WorldGenRegion;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.ChunkPos;
import java.util.function.LongPredicate;
import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import java.util.List;
final class NmsGenerationHooks {
private NmsGenerationHooks() {
}
static void install() {
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());
}
buddy.redefine(WorldGenRegion.class)
.visit(Advice.to(WorldGenerationWriteAdvice.class).on(ElementMatchers.named("ensureCanWrite")
.and(ElementMatchers.takesArguments(BlockPos.class))))
.make()
.load(WorldGenRegion.class.getClassLoader(), Agent.installed());
}
private static class WorldGenerationWriteAdvice {
@Advice.OnMethodExit
static void exit(@Advice.This WorldGenRegion region, @Advice.Argument(0) BlockPos position,
@Advice.Return(readOnly = false) boolean allowed) {
if (!allowed) {
return;
}
ChunkGenerator generator = region.getLevel().getChunkSource().getGenerator();
if (generator instanceof LongPredicate predicate
&& generator.getClass().getName().equals("art.arcane.iris.platform.bukkit.nms.v26_3_R1.IrisChunkGenerator")
&& region.getChunk(position.getX() >> 4, position.getZ() >> 4)
.getPersistedStatus().isOrAfter(ChunkStatus.FULL)) {
allowed = predicate.test(ChunkPos.pack(position.getX() >> 4, position.getZ() >> 4));
}
}
}
private static class ChunkAccessAdvice {
@Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class)
static boolean enter(@Advice.This ChunkAccess access, @Advice.Argument(1) int index) {
return index >= access.getPostProcessing().length;
}
}
}
@@ -0,0 +1,182 @@
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
import art.arcane.iris.spi.PlatformGenerationRegistry;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.mojang.serialization.Codec;
import com.mojang.serialization.JsonOps;
import net.minecraft.core.Registry;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.RegistryOps;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.dimension.DimensionType;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.function.Supplier;
final class NmsGenerationRegistry implements PlatformGenerationRegistry {
private static final String BIOME_REGISTRY = "minecraft:worldgen/biome";
private static final String STRUCTURE_REGISTRY = "minecraft:worldgen/structure";
private static final String DIMENSION_TYPE_REGISTRY = "minecraft:dimension_type";
private static final String BLOCK_REGISTRY = "minecraft:block";
private static final String ENTITY_TYPE_REGISTRY = "minecraft:entity_type";
private final Supplier<RegistryAccess> registryAccess;
private final String runtimeIdentity;
private final String rendererIdentity;
NmsGenerationRegistry(
Supplier<RegistryAccess> registryAccess,
String runtimeIdentity,
String rendererIdentity
) {
this.registryAccess = Objects.requireNonNull(registryAccess, "registryAccess");
this.runtimeIdentity = requireText(runtimeIdentity, "runtimeIdentity");
this.rendererIdentity = requireText(rendererIdentity, "rendererIdentity");
}
@Override
public String runtimeIdentity() {
return runtimeIdentity;
}
@Override
public String generatedDefinitionRendererIdentity() {
return rendererIdentity;
}
@Override
public String customBiomeResourceKey(String identitySha256) {
return PlatformGenerationRegistry.contentAddressedCustomBiomeResourceKey(identitySha256);
}
@Override
public List<String> legacyCustomBiomeResourceKeys(
String packName,
String dimensionKey,
String customBiomeId
) {
return List.of(requireText(dimensionKey, "dimensionKey").toLowerCase(Locale.ROOT)
+ ":" + requireText(customBiomeId, "customBiomeId").toLowerCase(Locale.ROOT));
}
@Override
public String dimensionTypeResourceKey(String packName, String dimensionKey, String dimensionTypeKey) {
return "iris:" + requireText(dimensionTypeKey, "dimensionTypeKey").toLowerCase(Locale.ROOT);
}
@Override
public Definition definition(String registryKey, String resourceKey) {
RegistryAccess access = Objects.requireNonNull(registryAccess.get(), "Minecraft registry access");
return switch (registryKey) {
case BIOME_REGISTRY -> presence(access, Registries.BIOME, registryKey, resourceKey);
case STRUCTURE_REGISTRY -> presence(access, Registries.STRUCTURE, registryKey, resourceKey);
case DIMENSION_TYPE_REGISTRY -> presence(
access,
Registries.DIMENSION_TYPE,
registryKey,
resourceKey
);
case BLOCK_REGISTRY -> presence(access, Registries.BLOCK, registryKey, resourceKey);
case ENTITY_TYPE_REGISTRY -> presence(access, Registries.ENTITY_TYPE, registryKey, resourceKey);
default -> throw new IllegalArgumentException("Unsupported generation registry: " + registryKey);
};
}
@Override
public Definition canonicalDefinition(String registryKey, String resourceKey, String sourceJson) {
RegistryAccess access = Objects.requireNonNull(registryAccess.get(), "Minecraft registry access");
return switch (registryKey) {
case BIOME_REGISTRY -> canonicalize(access, Biome.DIRECT_CODEC, sourceJson);
case DIMENSION_TYPE_REGISTRY -> canonicalize(access, DimensionType.DIRECT_CODEC, sourceJson);
default -> throw new IllegalArgumentException(
"Registry does not accept generated JSON definitions: " + registryKey
);
};
}
@Override
public Definition generatedDefinition(String registryKey, String resourceKey) {
RegistryAccess access = Objects.requireNonNull(registryAccess.get(), "Minecraft registry access");
return switch (registryKey) {
case BIOME_REGISTRY -> exactRegistered(
access,
Registries.BIOME,
Biome.DIRECT_CODEC,
resourceKey
);
case DIMENSION_TYPE_REGISTRY -> exactRegistered(
access,
Registries.DIMENSION_TYPE,
DimensionType.DIRECT_CODEC,
resourceKey
);
default -> throw new IllegalArgumentException(
"Registry does not contain Iris-generated definitions: " + registryKey
);
};
}
private static <T> Definition presence(
RegistryAccess access,
ResourceKey<? extends Registry<T>> registryKey,
String registryResourceKey,
String resourceKey
) {
Registry<T> registry = access.lookupOrThrow(registryKey);
Identifier identifier = requireIdentifier(resourceKey);
T value = registry.getValue(identifier);
if (value == null) {
throw new IllegalStateException("Missing registry definition " + registryKey.identifier()
+ " / " + resourceKey + ".");
}
return Definition.resourceIdentity(registryResourceKey, resourceKey);
}
private static <T> Definition canonicalize(RegistryAccess access, Codec<T> codec, String sourceJson) {
RegistryOps<JsonElement> ops = RegistryOps.create(JsonOps.INSTANCE, access);
JsonElement source = JsonParser.parseString(Objects.requireNonNull(sourceJson, "sourceJson"));
T decoded = codec.parse(ops, source).getOrThrow(IllegalArgumentException::new);
JsonElement encoded = codec.encodeStart(ops, decoded).getOrThrow(IllegalStateException::new);
return Definition.exactJson(encoded.toString());
}
private static <T> Definition exactRegistered(
RegistryAccess access,
ResourceKey<? extends Registry<T>> registryKey,
Codec<T> codec,
String resourceKey
) {
Registry<T> registry = access.lookupOrThrow(registryKey);
Identifier identifier = requireIdentifier(resourceKey);
T value = registry.getValue(identifier);
if (value == null) {
throw new IllegalStateException("Missing generated registry definition "
+ registryKey.identifier() + " / " + resourceKey + ".");
}
RegistryOps<JsonElement> ops = RegistryOps.create(JsonOps.INSTANCE, access);
JsonElement encoded = codec.encodeStart(ops, value).getOrThrow(IllegalStateException::new);
return Definition.exactJson(encoded.toString());
}
private static Identifier requireIdentifier(String resourceKey) {
Identifier identifier = Identifier.tryParse(resourceKey);
if (identifier == null) {
throw new IllegalArgumentException("Invalid registry resource key: " + resourceKey);
}
return identifier;
}
private static String requireText(String value, String label) {
String required = Objects.requireNonNull(value, label).trim();
if (required.isEmpty()) {
throw new IllegalArgumentException(label + " must not be blank.");
}
return required;
}
}
@@ -0,0 +1,266 @@
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
import art.arcane.iris.world.history.NativeTerrainReceipt;
import art.arcane.iris.world.history.SavedTerrainChunk;
import art.arcane.iris.world.task.J;
import ca.spottedleaf.concurrentutil.completable.Completable;
import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor;
import ca.spottedleaf.concurrentutil.util.Priority;
import ca.spottedleaf.moonrise.common.PlatformHooks;
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtIo;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.storage.SerializableChunkData;
import org.bukkit.World;
import org.bukkit.craftbukkit.CraftWorld;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.locks.LockSupport;
final class NmsSavedTerrainCapture {
private static final long WRITE_TIMEOUT_NANOS = TimeUnit.SECONDS.toNanos(30L);
private static final int SNAPSHOT_BATCH_SIZE = 64;
private NmsSavedTerrainCapture() {
}
static CompletableFuture<SavedTerrainChunk> capture(World world, int chunkX, int chunkZ, int minimumY, int height) {
ServerLevel level = ((CraftWorld) world).getHandle();
CompletableFuture<CapturedChunk> snapshot = new CompletableFuture<>();
J.runRegionFuture(world, chunkX, chunkZ, () -> {
try {
NewChunkHolder holder = level.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkX, chunkZ);
ChunkAccess chunk = holder == null ? null : holder.getCurrentChunk();
if (chunk == null) {
snapshot.complete(new CapturedChunk(new ChunkPos(chunkX, chunkZ), CompletableFuture.completedFuture(null)));
return;
}
String status = BuiltInRegistries.CHUNK_STATUS.getKey(chunk.getPersistedStatus()).toString();
if (!SavedTerrainChunk.hasTerrain(status)) {
throw new IOException("Native chunk " + chunkX + "," + chunkZ + " has no completed terrain at " + status);
}
snapshot.complete(scheduleSnapshot(level, chunk));
} catch (Throwable failure) {
snapshot.completeExceptionally(failure);
}
}).whenComplete((ignored, failure) -> {
if (failure != null) {
snapshot.completeExceptionally(failure);
}
});
return snapshot.thenApplyAsync(captured -> {
try {
long deadline = System.nanoTime() + WRITE_TIMEOUT_NANOS;
CompoundTag data = awaitSnapshot(level, captured, deadline);
Checkpoint checkpoint = checkpoint(chunkX, chunkZ, data);
awaitWrite(level, checkpoint.chunk(), deadline);
if (data == null) {
return SavedTerrainChunk.read(world.getWorldFolder().toPath(), chunkX, chunkZ, minimumY, height);
}
verifyCheckpoint(world.getWorldFolder().toPath(), checkpoint);
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (DataOutputStream output = new DataOutputStream(bytes)) {
NbtIo.write(data, output);
}
return SavedTerrainChunk.readNbt(bytes.toByteArray(), chunkX, chunkZ, minimumY, height);
} catch (IOException failure) {
throw new CompletionException(failure);
}
});
}
static CompletableFuture<Void> flush(World world) {
ServerLevel level = ((CraftWorld) world).getHandle();
List<NewChunkHolder> holders = level.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolders();
return CompletableFuture.runAsync(() -> {
try {
flushSnapshots(world, level, holders);
} catch (IOException failure) {
throw new CompletionException(failure);
}
});
}
static void flushSnapshots(World world, ServerLevel level, List<NewChunkHolder> holders) throws IOException {
long deadline = System.nanoTime() + WRITE_TIMEOUT_NANOS;
List<Checkpoint> checkpoints = new ArrayList<>(holders.size());
for (int offset = 0; offset < holders.size(); offset += SNAPSHOT_BATCH_SIZE) {
int end = Math.min(holders.size(), offset + SNAPSHOT_BATCH_SIZE);
List<CompletableFuture<CapturedChunk>> snapshots = requestSnapshots(world, level, holders.subList(offset, end));
awaitSnapshots(snapshots, deadline);
for (CompletableFuture<CapturedChunk> future : snapshots) {
CapturedChunk captured = future.join();
CompoundTag data = awaitSnapshot(level, captured, deadline);
checkpoints.add(checkpoint(captured.chunk().x(), captured.chunk().z(), data));
}
for (int index = offset; index < end; index++) {
awaitWrite(level, checkpoints.get(index).chunk(), deadline);
}
}
finishCheckpoint(level, world.getWorldFolder().toPath(), checkpoints);
}
private static List<CompletableFuture<CapturedChunk>> requestSnapshots(
World world, ServerLevel level, List<NewChunkHolder> holders) {
List<CompletableFuture<CapturedChunk>> snapshots = new ArrayList<>(holders.size());
for (NewChunkHolder original : holders) {
ChunkPos position = new ChunkPos(original.chunkX, original.chunkZ);
CompletableFuture<CapturedChunk> snapshot = new CompletableFuture<>();
snapshots.add(snapshot);
J.runRegionFuture(world, position.x(), position.z(), () -> {
try {
NewChunkHolder holder = level.moonrise$getChunkTaskScheduler().chunkHolderManager
.getChunkHolder(position.x(), position.z());
ChunkAccess chunk = holder == null ? null : holder.getCurrentChunk();
snapshot.complete(chunk != null && chunk.isUnsaved() ? scheduleSnapshot(level, chunk)
: new CapturedChunk(position, CompletableFuture.completedFuture(null)));
} catch (Throwable failure) {
snapshot.completeExceptionally(failure);
}
}).whenComplete((ignored, failure) -> {
if (failure != null) {
snapshot.completeExceptionally(failure);
}
});
}
return snapshots;
}
private static void awaitSnapshots(List<CompletableFuture<CapturedChunk>> snapshots, long deadlineNanos)
throws IOException {
try {
CompletableFuture.allOf(snapshots.toArray(CompletableFuture[]::new))
.get(Math.max(0L, deadlineNanos - System.nanoTime()), TimeUnit.NANOSECONDS);
} catch (InterruptedException failure) {
Thread.currentThread().interrupt();
throw new IOException("Interrupted while copying terrain checkpoint chunks.", failure);
} catch (ExecutionException | TimeoutException failure) {
throw new IOException("Unable to copy terrain checkpoint chunks.", failure);
}
}
static void finishCheckpoint(ServerLevel level, Path worldFolder, List<Checkpoint> checkpoints) throws IOException {
long deadline = System.nanoTime() + WRITE_TIMEOUT_NANOS;
for (Checkpoint checkpoint : checkpoints) {
awaitWrite(level, checkpoint.chunk(), deadline);
}
MoonriseRegionFileIO.flushRegionStorages(level, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA);
for (Checkpoint checkpoint : checkpoints) {
verifyCheckpoint(worldFolder, checkpoint);
}
}
static Checkpoint checkpoint(int chunkX, int chunkZ, CompoundTag data) {
if (data == null) {
return new Checkpoint(new ChunkPos(chunkX, chunkZ), null);
}
byte[] receipt = data.getCompound("ChunkBukkitValues")
.flatMap(values -> values.getByteArray(NativeTerrainReceipt.NBT_KEY)).orElse(null);
long structureActivation = data.getCompound("ChunkBukkitValues").map(values -> values.getLongOr(
NativeTerrainReceipt.STRUCTURE_ACTIVATION_KEY, 0)).orElse(0L);
return new Checkpoint(new ChunkPos(chunkX, chunkZ), new Verification(
data.getStringOr("Status", ""), receipt, structureActivation));
}
private static void verifyCheckpoint(Path worldFolder, Checkpoint checkpoint) throws IOException {
Verification expected = checkpoint.verification();
if (expected == null) {
return;
}
SavedTerrainChunk.verifyCheckpoint(worldFolder, checkpoint.chunk().x(), checkpoint.chunk().z(),
expected.status(), expected.receipt(), expected.structureActivation());
}
static void awaitWrite(ServerLevel level, ChunkPos chunk, long deadlineNanos) throws IOException {
ChunkTaskScheduler scheduler = level.moonrise$getChunkTaskScheduler();
while (true) {
checkWriteWait(chunk, deadlineNanos);
if (MoonriseRegionFileIO.getPriority(level, chunk.x(), chunk.z(),
MoonriseRegionFileIO.RegionFileType.CHUNK_DATA) == Priority.COMPLETING) {
return;
}
helpSaveTasks(scheduler);
}
}
static CapturedChunk scheduleSnapshot(ServerLevel level, ChunkAccess chunk) {
SerializableChunkData snapshot = SerializableChunkData.copyOf(level, chunk);
PlatformHooks.get().chunkSyncSave(level, chunk, snapshot);
Completable<CompoundTag> nativeData = new Completable<>();
CompletableFuture<CompoundTag> data = new CompletableFuture<>();
PrioritisedExecutor.PrioritisedTask task = level.moonrise$getChunkTaskScheduler().saveExecutor
.createTask(() -> serializeSnapshot(snapshot, nativeData, data));
task.queue();
ChunkPos position = chunk.getPos();
MoonriseRegionFileIO.scheduleSave(level, position.x(), position.z(), nativeData, task,
MoonriseRegionFileIO.RegionFileType.CHUNK_DATA, Priority.NORMAL);
return new CapturedChunk(position, data);
}
static CompoundTag awaitSnapshot(ServerLevel level, CapturedChunk captured, long deadlineNanos) throws IOException {
ChunkTaskScheduler scheduler = level.moonrise$getChunkTaskScheduler();
while (!captured.data().isDone()) {
checkWriteWait(captured.chunk(), deadlineNanos);
helpSaveTasks(scheduler);
}
try {
return captured.data().join();
} catch (CompletionException failure) {
throw new IOException("Unable to serialize terrain checkpoint chunk " + captured.chunk(), failure.getCause());
}
}
private static void serializeSnapshot(SerializableChunkData snapshot, Completable<CompoundTag> nativeData,
CompletableFuture<CompoundTag> data) {
try {
CompoundTag serialized = snapshot.write();
nativeData.complete(serialized);
data.complete(serialized);
} catch (Throwable failure) {
nativeData.completeExceptionally(failure);
data.completeExceptionally(failure);
}
}
private static void checkWriteWait(ChunkPos chunk, long deadlineNanos) throws IOException {
if (Thread.currentThread().isInterrupted()) {
throw new IOException("Interrupted while saving terrain checkpoint chunk " + chunk);
}
if (System.nanoTime() - deadlineNanos >= 0L) {
throw new IOException("Timed out saving terrain checkpoint chunk " + chunk);
}
}
private static void helpSaveTasks(ChunkTaskScheduler scheduler) {
boolean helped = scheduler.saveExecutor.executeTask();
helped |= scheduler.compressionExecutor.executeTask();
if (!helped) {
LockSupport.parkNanos(1_000_000L);
}
}
record CapturedChunk(ChunkPos chunk, CompletableFuture<CompoundTag> data) {
}
record Checkpoint(ChunkPos chunk, Verification verification) {
}
record Verification(String status, byte[] receipt, long structureActivation) {
}
}
@@ -0,0 +1,403 @@
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
import art.arcane.iris.platform.bukkit.nms.ServerShutdownBoundary;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.localization.C;
import art.arcane.iris.platform.agent.Agent;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.agent.builder.ResettableClassFileTransformer;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.matcher.ElementMatchers;
import net.bytebuddy.utility.JavaModule;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.dimension.LevelStem;
import net.minecraft.world.level.storage.LevelStorageSource;
import org.bukkit.Bukkit;
import org.bukkit.craftbukkit.CraftServer;
import org.bukkit.generator.ChunkGenerator;
import org.bukkit.plugin.Plugin;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.file.Path;
import java.util.concurrent.atomic.AtomicBoolean;
final class NmsWorldLifecycle {
private final AtomicBoolean injected = new AtomicBoolean();
private volatile ResettableClassFileTransformer levelStorageAccessTransformer;
private volatile ResettableClassFileTransformer serverLevelTransformer;
private volatile ResettableClassFileTransformer pluginClassLoaderTransformer;
private boolean pluginClassLoaderCloseDeferred;
public boolean injectBukkit() {
synchronized (injected) {
if (injected.get()) {
return true;
}
try {
IrisLogging.info("Injecting Bukkit");
Agent.requireClassLoaderCloseDeferral();
Class<?> loaderCloseType = getClass().getClassLoader().getClass()
.getMethod("close").getDeclaringClass();
PluginClassLoaderInjectionListener loaderListener = new PluginClassLoaderInjectionListener();
pluginClassLoaderTransformer = new AgentBuilder.Default()
.disableClassFormatChanges()
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
.with(AgentBuilder.RedefinitionStrategy.Listener.ErrorEscalating.FAIL_FAST)
.with(loaderListener)
.type(ElementMatchers.is(loaderCloseType))
.transform((builder, typeDescription, classLoader, module, protectionDomain) ->
builder.visit(Advice.to(PluginClassLoaderCloseAdvice.class)
.on(ElementMatchers.named("close")
.and(ElementMatchers.takesArguments(0))
.and(ElementMatchers.returns(void.class)))))
.installOn(Agent.getInstrumentation());
loaderListener.requireInstalled();
levelStorageAccessTransformer = new AgentBuilder.Default()
.disableClassFormatChanges()
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
.type(ElementMatchers.is(LevelStorageSource.LevelStorageAccess.class))
.transform((builder, typeDescription, classLoader, module, protectionDomain) ->
builder.visit(Advice.to(LevelStorageAccessAdvice.class).on(ElementMatchers.isConstructor()
.and(ElementMatchers.takesArguments(4))
.and(ElementMatchers.takesArgument(0, LevelStorageSource.class))
.and(ElementMatchers.takesArgument(1, String.class))
.and(ElementMatchers.takesArgument(2, Path.class))
.and(ElementMatchers.takesArgument(3, ResourceKey.class)))))
.installOn(Agent.getInstrumentation());
serverLevelTransformer = 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());
NmsGenerationHooks.install();
injected.set(true);
return true;
} catch (Throwable e) {
IrisLogging.reportError(C.RED + "Failed to inject Bukkit", e);
ResettableClassFileTransformer partialServerLevel = serverLevelTransformer;
ResettableClassFileTransformer partialStorageAccess = levelStorageAccessTransformer;
ResettableClassFileTransformer partialPluginClassLoader = pluginClassLoaderTransformer;
serverLevelTransformer = null;
levelStorageAccessTransformer = null;
pluginClassLoaderTransformer = null;
for (ResettableClassFileTransformer partial : new ResettableClassFileTransformer[]{
partialServerLevel,
partialStorageAccess,
partialPluginClassLoader
}) {
if (partial == null) {
continue;
}
try {
partial.reset(Agent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION);
} catch (Throwable cleanupFailure) {
IrisLogging.reportError("Failed to remove partial Bukkit lifecycle injection", cleanupFailure);
}
}
return false;
}
}
}
public void ensureServerLevelInjection() {
if (!injected.get()) {
throw new IllegalStateException("Iris world lifecycle injection is unavailable. Fix the Java Agent or Code Injection startup failure and restart before creating worlds.");
}
try {
Agent.getInstrumentation().retransformClasses(
LevelStorageSource.LevelStorageAccess.class,
ServerLevel.class
);
} catch (Throwable e) {
throw new IllegalStateException("Failed to re-apply Bukkit world lifecycle injection. World creation was stopped before loading the world.", e);
}
}
public void uninjectBukkit() {
synchronized (injected) {
ResettableClassFileTransformer activeServerLevel = serverLevelTransformer;
ResettableClassFileTransformer activeStorageAccess = levelStorageAccessTransformer;
serverLevelTransformer = null;
levelStorageAccessTransformer = null;
injected.set(false);
for (ResettableClassFileTransformer transformer : new ResettableClassFileTransformer[]{
activeServerLevel,
activeStorageAccess
}) {
if (transformer == null) {
continue;
}
try {
transformer.reset(Agent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION);
} catch (Throwable e) {
IrisLogging.reportError(C.RED + "Failed to remove Bukkit world lifecycle injection", e);
}
}
}
}
public boolean isServerStopping() {
return ((CraftServer) Bukkit.getServer()).getServer().hasStopped();
}
public ServerShutdownBoundary createServerShutdownBoundary() {
MinecraftServer server = ((CraftServer) Bukkit.getServer()).getServer();
return new ServerShutdownBoundary(
() -> server.hasFullyShutdown,
server.getRunningThread()
);
}
public void deferPluginClassLoaderClose() {
synchronized (injected) {
if (pluginClassLoaderTransformer == null || pluginClassLoaderCloseDeferred) {
return;
}
Agent.retainClassLoader(getClass().getClassLoader());
pluginClassLoaderCloseDeferred = true;
}
}
public void releasePluginClassLoaderClose() {
ResettableClassFileTransformer transformer;
boolean releaseLoader;
synchronized (injected) {
transformer = pluginClassLoaderTransformer;
pluginClassLoaderTransformer = null;
releaseLoader = pluginClassLoaderCloseDeferred;
pluginClassLoaderCloseDeferred = false;
}
try {
if (transformer != null
&& !transformer.reset(Agent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)) {
throw new IllegalStateException("Failed to remove Iris plugin class loader lifecycle injection");
}
} finally {
if (releaseLoader) {
Agent.releaseClassLoader(getClass().getClassLoader());
}
}
}
private static final class PluginClassLoaderInjectionListener extends AgentBuilder.Listener.Adapter {
private volatile boolean transformed;
private volatile Throwable failure;
@Override
public void onTransformation(TypeDescription type, ClassLoader loader, JavaModule module,
boolean loaded, DynamicType dynamicType) {
transformed = true;
}
@Override
public void onError(String typeName, ClassLoader loader, JavaModule module,
boolean loaded, Throwable throwable) {
failure = throwable;
}
private void requireInstalled() {
if (!transformed || failure != null) {
throw new IllegalStateException("Iris plugin class loader lifecycle injection failed", failure);
}
}
}
private static class PluginClassLoaderCloseAdvice {
@Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class)
static boolean enter(@Advice.This ClassLoader loader) throws ReflectiveOperationException {
Class<?> installer = Class.forName(
"art.arcane.iris.platform.agent.Installer", true, ClassLoader.getSystemClassLoader());
return Boolean.TRUE.equals(installer.getMethod("deferClassLoaderClose", ClassLoader.class)
.invoke(null, loader));
}
}
private static class LevelStorageAccessAdvice {
@Advice.OnMethodEnter
static void enter(
@Advice.Argument(1) String levelId,
@Advice.Argument(value = 3, readOnly = false) ResourceKey<LevelStem> dimensionType
) {
if (levelId == null || levelId.isBlank()) {
return;
}
ClassLoader pluginClassLoader;
Class<?> generatorType;
Class<?> stagingType;
try {
Plugin irisPlugin = Bukkit.getPluginManager().getPlugin("Iris");
if (irisPlugin == null) {
return;
}
pluginClassLoader = irisPlugin.getClass().getClassLoader();
generatorType = Class.forName("art.arcane.iris.platform.generation.PlatformChunkGenerator", true, pluginClassLoader);
stagingType = Class.forName("art.arcane.iris.world.lifecycle.WorldLifecycleStaging", true, pluginClassLoader);
} catch (Throwable ignored) {
return;
}
Object generator;
try {
generator = stagingType
.getDeclaredMethod("peekStemGenerator", String.class)
.invoke(null, levelId);
} catch (Throwable e) {
throw new RuntimeException("Iris failed to inspect the staged world generator",
e instanceof InvocationTargetException ex ? ex.getCause() : e);
}
if (generator == null || !generatorType.isInstance(generator)) {
return;
}
try {
Object target = generatorType.getMethod("getTarget").invoke(generator);
if (target == null) {
throw new IllegalStateException("Iris generator has no engine target.");
}
Object world = target.getClass().getMethod("getWorld").invoke(target);
if (world == null) {
throw new IllegalStateException("Iris generator target has no world identity.");
}
Object rawIdentity = world.getClass().getMethod("identity").invoke(world);
String worldIdentity = rawIdentity == null ? "" : rawIdentity.toString().trim();
Identifier worldIdentifier = Identifier.parse(worldIdentity);
if (!"iris".equals(worldIdentifier.getNamespace())
&& !"minecraft".equals(worldIdentifier.getNamespace())) {
throw new IllegalStateException(
"Iris generator has an unmanaged world identity: " + worldIdentity);
}
dimensionType = ResourceKey.create(Registries.LEVEL_STEM, worldIdentifier);
} catch (Throwable e) {
throw new RuntimeException("Iris failed to bind the staged world storage identity",
e instanceof InvocationTargetException ex ? ex.getCause() : e);
}
}
}
private static class ServerLevelAdvice {
@Advice.OnMethodEnter
static void enter(
@Advice.Argument(0) MinecraftServer server,
@Advice.Argument(value = 4, readOnly = false) ResourceKey<Level> dimensionKey,
@Advice.Argument(value = 5, readOnly = false) LevelStem levelStem,
@Advice.AllArguments Object[] constructorArguments
) {
if (dimensionKey == null)
return;
// This advice is inlined into every ServerLevel construction on the server. Until a
// world is proven Iris-owned, every failure must fail OPEN (keep the vanilla stem):
// Iris being unloaded or half-loaded must never break other plugins' world creation.
String levelId;
ClassLoader pluginClassLoader;
Class<?> generatorType;
Class<?> stagingType;
try {
levelId = dimensionKey.identifier().getPath();
if (levelId == null || levelId.isBlank()) {
return;
}
Plugin irisPlugin = Bukkit.getPluginManager().getPlugin("Iris");
if (irisPlugin == null) {
return;
}
pluginClassLoader = irisPlugin.getClass().getClassLoader();
generatorType = Class.forName("art.arcane.iris.platform.generation.PlatformChunkGenerator", true, pluginClassLoader);
stagingType = Class.forName("art.arcane.iris.world.lifecycle.WorldLifecycleStaging", true, pluginClassLoader);
} catch (Throwable ignored) {
// Iris absent or half-loaded: fail OPEN for a world we cannot prove ours.
return;
}
// From here Iris is present and its classes resolve; a failure resolving the
// staged generator must fail LOUD — silently handing a possibly-staged Iris world
// the vanilla stem corrupts generation.
ChunkGenerator gen = null;
try {
ChunkGenerator constructorGenerator = null;
for (Object argument : constructorArguments) {
if (argument instanceof ChunkGenerator candidate) {
constructorGenerator = candidate;
break;
}
}
Object generator = generatorType.isInstance(constructorGenerator) ? constructorGenerator : null;
if (generator == null) {
generator = stagingType
.getDeclaredMethod("consumeStemGenerator", String.class)
.invoke(null, levelId);
}
if (generator instanceof ChunkGenerator owned && owned.getClass().getPackageName().startsWith("art.arcane.iris")) {
gen = owned;
}
} catch (Throwable e) {
throw new RuntimeException("Iris failed to resolve the staged world generator",
e instanceof InvocationTargetException ex ? ex.getCause() : e);
}
if (gen == null) {
return;
}
// Past the ownership gate the world is Iris-owned; silently handing back the
// vanilla stem would corrupt generation, so failures from here rethrow.
try {
Object bindings = Class.forName("art.arcane.iris.platform.bukkit.nms.INMS", true, pluginClassLoader)
.getDeclaredMethod("get")
.invoke(null);
if (bindings == null) {
throw new IllegalStateException("Iris failed to resolve an INMSBinding instance.");
}
Method stemMethod = null;
for (Method candidate : bindings.getClass().getMethods()) {
if (candidate.getName().equals("createRuntimeLevelStem") && candidate.getParameterCount() == 2) {
stemMethod = candidate;
break;
}
}
if (stemMethod == null) {
throw new IllegalStateException("Iris binding is missing createRuntimeLevelStem.");
}
Object target = generatorType.getMethod("getTarget").invoke(gen);
if (target == null) {
throw new IllegalStateException("Iris generator has no engine target.");
}
Object world = target.getClass().getMethod("getWorld").invoke(target);
if (world == null) {
throw new IllegalStateException("Iris generator target has no world identity.");
}
Object rawIdentity = world.getClass().getMethod("identity").invoke(world);
String worldIdentity = rawIdentity == null ? "" : rawIdentity.toString().trim();
Identifier worldIdentifier = Identifier.parse(worldIdentity);
if (!"iris".equals(worldIdentifier.getNamespace())
&& !"minecraft".equals(worldIdentifier.getNamespace())) {
throw new IllegalStateException(
"Iris generator has an unmanaged world identity: " + worldIdentity);
}
Object resolvedStem = stemMethod.invoke(bindings, server.registryAccess(), gen);
if (!(resolvedStem instanceof LevelStem runtimeStem)) {
throw new IllegalStateException("Iris runtime LevelStem binding returned " + (resolvedStem == null ? "null" : resolvedStem.getClass().getName()) + ".");
}
dimensionKey = ResourceKey.create(Registries.DIMENSION, worldIdentifier);
levelStem = runtimeStem;
} catch (Throwable e) {
throw new RuntimeException("Iris failed to replace the levelStem", e instanceof InvocationTargetException ex ? ex.getCause() : e);
}
}
}
}
@@ -0,0 +1,80 @@
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
import art.arcane.iris.nativegen.v26_3_R1.NativeStructureGenerationKeys;
import net.minecraft.core.Holder;
import net.minecraft.core.Registry;
import net.minecraft.core.RegistryAccess;
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.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeSource;
import net.minecraft.world.level.levelgen.structure.Structure;
import java.util.LinkedHashSet;
import java.util.Optional;
import java.util.Set;
final class VanillaStructureBiomes {
private VanillaStructureBiomes() {
}
static Set<String> possibleBiomeKeys(BiomeSource source) {
if (source == null) {
throw new IllegalStateException("Minecraft chunk generator has no biome source");
}
Set<String> keys = new LinkedHashSet<>();
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());
}
}
if (keys.isEmpty()) {
throw new IllegalStateException("Minecraft biome source exposes no registered possible biomes");
}
return keys;
}
static Set<String> structureBiomeKeys(RegistryAccess access, String structureKey) {
if (access == null) {
throw new IllegalStateException("Minecraft registry access is unavailable");
}
if (structureKey == null || structureKey.isBlank()) {
throw new IllegalArgumentException("Registered structure key must not be blank");
}
Set<String> keys = new LinkedHashSet<>();
Registry<Structure> registry = access.lookupOrThrow(Registries.STRUCTURE);
Structure structure = registry.getValue(Identifier.parse(structureKey));
if (structure == null) {
throw new IllegalArgumentException("Registered structure does not exist: " + structureKey);
}
boolean hasFilterEntries = false;
for (Holder<Biome> holder : structure.biomes()) {
hasFilterEntries = true;
Optional<ResourceKey<Biome>> key = holder.unwrapKey();
if (key.isPresent()) {
keys.add(key.get().identifier().toString());
}
}
// An empty biome filter is legal datapack content (opt-in structures whose tags only
// reference absent modded biomes); the structure is unreachable, not an error state.
if (keys.isEmpty() && hasFilterEntries) {
throw new IllegalStateException("Registered structure '" + structureKey
+ "' has biome filter entries but none resolve to registered biome keys");
}
return keys;
}
static Set<String> reachableStructureKeys(ServerLevel level, BiomeSource source) {
if (level == null) {
throw new IllegalStateException("Minecraft server level is unavailable");
}
Set<String> possible = possibleBiomeKeys(source);
return NativeStructureGenerationKeys.reachable(level, possible);
}
}
@@ -0,0 +1,131 @@
package art.arcane.iris.nativegen.v26_3_R1;
import art.arcane.iris.platform.bukkit.nms.v26_3_R1.IrisChunkGenerator;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.GenerationSessionLease;
import art.arcane.iris.world.history.BoundaryColumnGeometry;
import art.arcane.iris.world.history.TerrainBoundarySignature;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.levelgen.Heightmap;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Optional;
import java.util.OptionalInt;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public final class IrisChunkGeneratorHeightCacheTest {
@Rule
public final PlatformLeakGuard leakGuard = PlatformLeakGuard.clean();
@BeforeClass
public static void bootstrap() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
NativeBlockTags.bindHeightmapFixtures();
}
@Test
public void repeatedNativeHeightSurvivesResolvedTerrainEvictionAndRuntimeChange() throws Exception {
Fixture fixture = fixture();
LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32);
assertEquals(-11, fixture.generator().getBaseHeight(-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
when(fixture.complex().resolvedTerrainColumn(-1, -33))
.thenThrow(new IllegalStateException("Full terrain was evicted"));
for (int index = 0; index < 100; index++) {
assertEquals(-11, fixture.generator().getBaseHeight(
-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
}
verify(fixture.complex(), times(1)).resolvedTerrainColumn(-1, -33);
when(fixture.engine().getCacheID()).thenReturn(2);
doReturn(Optional.of(signature())).when(fixture.complex()).resolvedTerrainColumn(-1, -33);
assertEquals(-11, fixture.generator().getBaseHeight(-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
verify(fixture.complex(), times(2)).resolvedTerrainColumn(-1, -33);
}
@Test
public void ordinaryHeightRemainsLiveUntilResolvedTransitionTerrainExists() throws Exception {
Fixture fixture = fixture();
LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32);
when(fixture.complex().resolvedTerrainColumn(0, 0))
.thenReturn(Optional.empty(), Optional.empty(), Optional.of(signature()));
when(fixture.engine().getHeight(0, 0, false)).thenReturn(20, 25);
assertEquals(5, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
assertEquals(10, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
assertEquals(-11, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
assertEquals(-11, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
verify(fixture.engine(), times(2)).getHeight(0, 0, false);
verify(fixture.complex(), times(3)).resolvedTerrainColumn(0, 0);
}
@Test
public void nativePredicatesAndAccessorBoundsRemainExact() throws Exception {
Fixture fixture = fixture();
LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32);
for (int repeat = 0; repeat < 2; repeat++) {
assertEquals(-11, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
assertEquals(-13, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.OCEAN_FLOOR, bounds, null));
assertEquals(-13, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING, bounds, null));
assertEquals(-14, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, bounds, null));
assertEquals(-14, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE,
LevelHeightAccessor.create(-16, 2), null));
assertEquals(-14, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING_NO_LEAVES,
LevelHeightAccessor.create(-14, 3), null));
}
verify(fixture.complex(), times(6)).resolvedTerrainColumn(0, 0);
}
private static Fixture fixture() throws Exception {
Engine engine = mock(Engine.class);
IrisComplex complex = mock(IrisComplex.class);
when(engine.getComplex()).thenReturn(complex);
when(engine.getCacheID()).thenReturn(1);
when(engine.acquireGenerationLease(anyString())).thenReturn(GenerationSessionLease.noop());
when(complex.resolvedTerrainColumn(anyInt(), anyInt())).thenReturn(Optional.of(signature()));
IrisChunkGenerator generator = mock(IrisChunkGenerator.class, CALLS_REAL_METHODS);
set(generator, "engine", engine);
set(generator, "terrainHeights", new NativeTerrainHeightCache());
return new Fixture(generator, engine, complex);
}
private static void set(IrisChunkGenerator generator, String name, Object value) throws Exception {
Field field = IrisChunkGenerator.class.getDeclaredField(name);
field.setAccessible(true);
field.set(generator, value);
}
private static TerrainBoundarySignature signature() {
BoundaryColumnGeometry geometry = BoundaryColumnGeometry.fromVoxels(-16, List.of(
new BoundaryColumnGeometry.Voxel("minecraft:stone", BoundaryColumnGeometry.Phase.SOLID, "", false),
new BoundaryColumnGeometry.Voxel("minecraft:water", BoundaryColumnGeometry.Phase.FLUID, "minecraft:water", false),
new BoundaryColumnGeometry.Voxel("minecraft:oak_leaves[persistent=true]", BoundaryColumnGeometry.Phase.SOLID, "", false),
new BoundaryColumnGeometry.Voxel("minecraft:short_grass", BoundaryColumnGeometry.Phase.SOLID, "", false),
new BoundaryColumnGeometry.Voxel("minecraft:light[level=15]", BoundaryColumnGeometry.Phase.SOLID, "", false)));
return new TerrainBoundarySignature(
new TerrainBoundarySignature.Column(0, 0, 4, 4, OptionalInt.of(1), OptionalInt.empty()),
new TerrainBoundarySignature.Samples(new TerrainBoundarySignature.VerticalLayout(-16, 5, 1),
new TerrainBoundarySignature.BiomeEncoding(List.of("minecraft:plains"), new short[]{0})), geometry);
}
private record Fixture(IrisChunkGenerator generator, Engine engine, IrisComplex complex) {
}
}
@@ -0,0 +1,125 @@
package art.arcane.iris.nativegen.v26_3_R1;
import art.arcane.iris.generation.runtime.DimensionStackContext;
import art.arcane.iris.generation.runtime.DimensionStackLayout;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.world.history.GenerationActivation;
import art.arcane.iris.world.history.GenerationHistory;
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.structure.object.IrisStaticObjectLayer;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.Optional;
import java.util.function.Predicate;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
public final class NativeGenerationWriteGuardTest {
@BeforeClass
public static void bootstrap() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void staticObjectsProtectRelativeHeightBeforeStackSampling() {
IrisStaticObjectLayer objects = mock(IrisStaticObjectLayer.class);
DimensionStackContext stack = mock(DimensionStackContext.class);
when(objects.contains(-3, 74, -7)).thenReturn(true);
Predicate<BlockPos> protectedPositions = NativeGenerationWriteGuard.protectedPositions(objects, stack, -64);
assertTrue(protectedPositions.test(new BlockPos(-3, 10, -7)));
verifyNoInteractions(stack);
assertFalse(NativeGenerationWriteGuard.protectedPositions(objects, null, -64)
.test(new BlockPos(-3, 11, -7)));
}
@Test
public void emptyStaticLayerSkipsContainsAndCachesEachSignedColumn() {
IrisStaticObjectLayer objects = mock(IrisStaticObjectLayer.class);
DimensionStackContext stack = mock(DimensionStackContext.class);
DimensionStackLayout negative = mock(DimensionStackLayout.class);
DimensionStackLayout positive = mock(DimensionStackLayout.class);
when(objects.isEmpty()).thenReturn(true);
when(stack.sample(-1, -1)).thenReturn(negative);
when(stack.sample(-1, 1)).thenReturn(positive);
when(negative.isHostFeatureProtectedY(64)).thenReturn(true);
when(positive.isHostFeatureProtectedY(65)).thenReturn(true);
Predicate<BlockPos> protectedPositions = NativeGenerationWriteGuard.protectedPositions(objects, stack, -64);
assertTrue(protectedPositions.test(new BlockPos(-1, 0, -1)));
assertFalse(protectedPositions.test(new BlockPos(-1, 1, -1)));
assertTrue(protectedPositions.test(new BlockPos(-1, 1, 1)));
verify(stack, times(1)).sample(-1, -1);
verify(stack, times(1)).sample(-1, 1);
verify(objects, never()).contains(anyInt(), anyInt(), anyInt());
}
@Test
public void preTerrainSavedStampDistinguishesEarlierNativeStarts() {
IrisEngine engine = mock(IrisEngine.class);
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
GenerationHistory history = mock(GenerationHistory.class);
GenerationActivation active = mock(GenerationActivation.class);
when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
when(router.history()).thenReturn(history);
when(history.activeActivation()).thenReturn(active);
when(active.activationId()).thenReturn(4L);
assertTrue(NativeGenerationWriteGuard.isHistoricalStructure(engine, 3));
assertFalse(NativeGenerationWriteGuard.isHistoricalStructure(engine, 0));
assertFalse(NativeGenerationWriteGuard.isHistoricalStructure(engine, 4));
assertFalse(NativeGenerationWriteGuard.isHistoricalStructure(engine, 5));
}
@Test
public void partialNeighborsMayFinishButCompletedHistoricalNeighborRejectsPass() {
Engine engine = mock(Engine.class);
IrisComplex complex = mock(IrisComplex.class);
when(engine.getComplex()).thenReturn(complex);
WorldGenLevel region = mock(WorldGenLevel.class);
ChunkAccess partial = mock(ChunkAccess.class);
when(partial.getPersistedStatus()).thenReturn(ChunkStatus.TERRAIN);
when(region.getChunk(anyInt(), anyInt())).thenReturn(partial);
assertTrue(NativeGenerationWriteGuard.allowsDecoration(engine, region, new ChunkPos(0, 0)));
ChunkAccess complete = mock(ChunkAccess.class);
when(complete.getPersistedStatus()).thenReturn(ChunkStatus.FULL);
when(region.getChunk(1, -1)).thenReturn(complete);
assertFalse(NativeGenerationWriteGuard.allowsDecoration(engine, region, new ChunkPos(0, 0)));
}
@Test
public void historicalNoiseMayFinishFeaturesButNeverReplaysNoiseOrCompletedStage() {
Engine engine = mock(Engine.class);
IrisComplex complex = mock(IrisComplex.class);
when(engine.getComplex()).thenReturn(complex);
ChunkAccess chunk = mock(ChunkAccess.class);
when(chunk.getPos()).thenReturn(new ChunkPos(-2, 3));
when(chunk.getPersistedStatus()).thenReturn(ChunkStatus.TERRAIN);
assertTrue(NativeGenerationWriteGuard.allowsPendingStage(engine, chunk, ChunkStatus.FEATURES));
assertFalse(NativeGenerationWriteGuard.allowsPendingStage(engine, chunk, ChunkStatus.TERRAIN));
when(chunk.getPersistedStatus()).thenReturn(ChunkStatus.FEATURES);
assertFalse(NativeGenerationWriteGuard.allowsPendingStage(engine, chunk, ChunkStatus.FEATURES));
when(chunk.getPersistedStatus()).thenReturn(ChunkStatus.FULL);
assertFalse(NativeGenerationWriteGuard.allowsPendingStage(engine, chunk, ChunkStatus.SPAWN));
}
}
@@ -0,0 +1,462 @@
package art.arcane.iris.nativegen.v26_3_R1;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
import java.util.Set;
import art.arcane.iris.structure.jigsaw.IrisJigsawConfiguration;
import art.arcane.iris.structure.placement.IrisStructureTerrain;
import com.mojang.datafixers.util.Either;
import com.mojang.datafixers.util.Pair;
import com.mojang.serialization.MapCodec;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderOwner;
import net.minecraft.core.HolderSet;
import net.minecraft.core.Vec3i;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.IntTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.Bootstrap;
import net.minecraft.server.level.WorldGenRegion;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.NoiseColumn;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeManager;
import net.minecraft.world.level.biome.FixedBiomeSource;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.RandomState;
import net.minecraft.world.level.levelgen.blending.Blender;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.TerrainAdjustment;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
import net.minecraft.world.level.levelgen.structure.pools.EmptyPoolElement;
import net.minecraft.world.level.levelgen.structure.pools.ListPoolElement;
import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElementType;
import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
import net.minecraft.world.level.levelgen.structure.pools.alias.PoolAliasBinding;
import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece;
import net.minecraft.world.level.levelgen.structure.structures.SwampHutStructure;
import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessorList;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class NativeStructureFactoryTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void genericSourceDoesNotRequireJigsawType() {
Structure source = new SwampHutStructure(
new Structure.StructureSettings(HolderSet.empty()));
Structure configured = NativeStructureFactory.configure(
null, source, null, false, 80);
assertSame(source, configured);
}
@Test
public void jigsawOptionsFailClosedForNonJigsawSource() {
Structure source = new SwampHutStructure(
new Structure.StructureSettings(HolderSet.empty()));
assertThrows(IllegalStateException.class, () -> NativeStructureFactory.configure(
null, source, new IrisJigsawConfiguration(), false, 80));
}
@Test
public void sourceMetadataReadsScalarAndAxisSpecificJigsawRanges() {
CompoundTag scalar = new CompoundTag();
scalar.putInt("max_distance_from_center", 80);
CompoundTag axisSpecific = new CompoundTag();
CompoundTag distance = new CompoundTag();
distance.putInt("horizontal", 96);
distance.putInt("vertical", 64);
axisSpecific.put("max_distance_from_center", distance);
assertEquals(80, NativeStructureFactory.sourceDistance(scalar, "horizontal"));
assertEquals(80, NativeStructureFactory.sourceDistance(scalar, "vertical"));
assertEquals(96, NativeStructureFactory.sourceDistance(axisSpecific, "horizontal"));
assertEquals(64, NativeStructureFactory.sourceDistance(axisSpecific, "vertical"));
}
@Test
public void sourceMetadataIncludesAdjustBoundingBoxExpansion() {
Structure none = new SwampHutStructure(new Structure.StructureSettings(HolderSet.empty()));
Structure beardBox = new SwampHutStructure(new Structure.StructureSettings.Builder(HolderSet.empty())
.terrainAdapation(TerrainAdjustment.BEARD_BOX)
.build());
assertEquals(0, NativeStructureFactory.horizontalReferenceExpansion(none));
assertEquals(12, NativeStructureFactory.horizontalReferenceExpansion(beardBox));
}
@Test
public void templatePoolSpanIncludesWeightedListsFallbacksAndRotations() {
StructurePoolElement narrow = inlineElement(30, 8, 70);
StructurePoolElement wide = inlineElement(96, 8, 12);
ListPoolElement list = new ListPoolElement(
List.of(narrow, wide, EmptyPoolElement.INSTANCE),
StructureTemplatePool.Projection.RIGID);
BoundPoolHolder fallbackHolder = new BoundPoolHolder("test:fallback");
StructureTemplatePool fallback = new StructureTemplatePool(
fallbackHolder,
List.of(Pair.of(inlineElement(110, 8, 20), 1)));
fallbackHolder.bind(fallback);
BoundPoolHolder startHolder = new BoundPoolHolder("test:start");
StructureTemplatePool start = new StructureTemplatePool(
fallbackHolder,
List.of(Pair.of(list, 5), Pair.of(EmptyPoolElement.INSTANCE, 1)));
startHolder.bind(start);
assertEquals(110, NativeStructureTemplatePoolBounds.horizontalSpan(start, null));
}
@Test
public void oversizedInlineStartTemplateReportsItsFullSpan() {
BoundPoolHolder holder = new BoundPoolHolder("test:oversized");
StructureTemplatePool pool = new StructureTemplatePool(
holder,
List.of(Pair.of(inlineElement(160, 8, 9), 1)));
holder.bind(pool);
assertEquals(160, NativeStructureTemplatePoolBounds.horizontalSpan(pool, null));
}
@Test
public void sourceSpanIncludesOnlyAliasesThatCanReplaceTheStartPool() {
BoundPoolHolder emptyHolder = new BoundPoolHolder("test:empty");
StructureTemplatePool empty = new StructureTemplatePool(emptyHolder, List.of());
emptyHolder.bind(empty);
BoundPoolHolder startHolder = new BoundPoolHolder("test:start");
StructureTemplatePool start = new StructureTemplatePool(
emptyHolder, List.of(Pair.of(inlineElement(20, 8, 12), 1)));
startHolder.bind(start);
BoundPoolHolder replacementHolder = new BoundPoolHolder("test:replacement");
StructureTemplatePool replacement = new StructureTemplatePool(
emptyHolder, List.of(Pair.of(inlineElement(30, 8, 12), 1)));
replacementHolder.bind(replacement);
BoundPoolHolder unrelatedHolder = new BoundPoolHolder("test:unrelated");
StructureTemplatePool unrelated = new StructureTemplatePool(
emptyHolder, List.of(Pair.of(inlineElement(200, 8, 12), 1)));
unrelatedHolder.bind(unrelated);
ResourceKey<StructureTemplatePool> startKey = startHolder.unwrapKey().orElseThrow();
ResourceKey<StructureTemplatePool> replacementKey = replacementHolder.unwrapKey().orElseThrow();
ResourceKey<StructureTemplatePool> unrelatedKey = unrelatedHolder.unwrapKey().orElseThrow();
ResourceKey<StructureTemplatePool> childAlias = ResourceKey.create(
Registries.TEMPLATE_POOL, Identifier.parse("test:child_alias"));
List<PoolAliasBinding> aliases = List.of(
PoolAliasBinding.direct(startKey, replacementKey),
PoolAliasBinding.direct(childAlias, unrelatedKey));
Map<ResourceKey<StructureTemplatePool>, StructureTemplatePool> pools = Map.of(
replacementKey, replacement,
unrelatedKey, unrelated);
assertEquals(30, NativeStructureTemplatePoolBounds.sourceHorizontalSpan(
null, start, startKey, aliases, pools::get));
}
@Test
public void sourceStartSpanDoesNotIncludeItsFallbackPool() {
BoundPoolHolder fallbackHolder = new BoundPoolHolder("test:large_fallback");
StructureTemplatePool fallback = new StructureTemplatePool(
fallbackHolder, List.of(Pair.of(inlineElement(180, 8, 10), 1)));
fallbackHolder.bind(fallback);
BoundPoolHolder startHolder = new BoundPoolHolder("test:small_start");
StructureTemplatePool start = new StructureTemplatePool(
fallbackHolder, List.of(Pair.of(inlineElement(24, 8, 10), 1)));
startHolder.bind(start);
assertEquals(180, NativeStructureTemplatePoolBounds.horizontalSpan(start, null));
assertEquals(24, NativeStructureTemplatePoolBounds.sourceHorizontalSpan(
null, start, startHolder.unwrapKey().orElseThrow(), List.of(), ignored -> null));
}
@Test
public void customPoolElementUsesItsBoundingBoxCapability() {
BoundPoolHolder fallbackHolder = new BoundPoolHolder("test:custom_fallback");
StructureTemplatePool fallback = new StructureTemplatePool(fallbackHolder, List.of());
fallbackHolder.bind(fallback);
BoundPoolHolder poolHolder = new BoundPoolHolder("test:custom");
StructureTemplatePool pool = new StructureTemplatePool(
fallbackHolder, List.of(Pair.of(new BoundingPoolElement(37, 11), 1)));
poolHolder.bind(pool);
assertEquals(37, NativeStructureTemplatePoolBounds.horizontalSpan(pool, null));
}
@Test
public void forcedGeneratorBypassesBiomeEligibilityWithoutFabricatingTerrain() {
ChunkGenerator delegate = new TestChunkGenerator();
Holder<Biome> biome = Holder.direct((Biome) null);
LevelHeightAccessor heightAccessor = LevelHeightAccessor.create(-64, 384);
ForcedStructureChunkGenerator generator = new ForcedStructureChunkGenerator(
delegate, biome);
assertEquals(63, generator.getSeaLevel());
assertEquals(64, generator.getBaseHeight(
0, 0, Heightmap.Types.WORLD_SURFACE_WG, heightAccessor, null));
NoiseColumn column = generator.getBaseColumn(
0, 0, heightAccessor, null);
assertSame(Blocks.STONE, column.getBlock(80).getBlock());
assertSame(Blocks.STONE, column.getBlock(81).getBlock());
assertNotSame(delegate.getBiomeSource(), generator.getBiomeSource());
}
@Test
public void undergroundRelocationMovesGenericPiecesAndReturnsFreshStart() {
Structure source = new SwampHutStructure(
new Structure.StructureSettings(HolderSet.empty()));
SwampHutPiece piece = new SwampHutPiece(RandomSource.create(17L), 0, 0);
StructureStart start = new StructureStart(
source,
new ChunkPos(0, 0),
0,
new PiecesContainer(List.of(piece))
);
StructureStart relocated = NativeStructureVerticalPlacer.relocateToMinY(
start, source, -20, LevelHeightAccessor.create(-64, 384));
assertNotSame(start, relocated);
assertEquals(-20, relocated.getPieces().getFirst().getBoundingBox().minY());
assertEquals(-20, relocated.getBoundingBox().minY());
}
@Test
public void invalidGeneratedStartIsANormalSkippedCandidate() {
Structure source = new SwampHutStructure(
new Structure.StructureSettings(HolderSet.empty()));
StructureStart valid = new StructureStart(
source,
new ChunkPos(0, 0),
0,
new PiecesContainer(List.of(
new SwampHutPiece(RandomSource.create(23L), 0, 0)))
);
assertFalse(NativeStructureStartInjector.isUsableGeneratedStart(
StructureStart.INVALID_START));
assertFalse(NativeStructureStartInjector.isUsableGeneratedStart(null));
assertFalse(NativeStructureStartInjector.isUsableGeneratedStart(
new StructureStart(source, new ChunkPos(0, 0), 0,
new PiecesContainer(List.of()))));
assertTrue(NativeStructureStartInjector.isUsableGeneratedStart(valid));
}
@Test
public void unrepresentableGeneratedContentBecomesAnUnpublishedInvalidStart() {
Structure source = new SwampHutStructure(
new Structure.StructureSettings(HolderSet.empty()));
SwampHutPiece boundaryPiece = new SwampHutPiece(RandomSource.create(29L), 0, 0);
boundaryPiece.move(143 - boundaryPiece.getBoundingBox().maxX(), 0, 0);
StructureStart boundary = new StructureStart(
source,
new ChunkPos(0, 0),
0,
new PiecesContainer(List.of(boundaryPiece))
);
SwampHutPiece oversizedPiece = new SwampHutPiece(RandomSource.create(31L), 0, 0);
oversizedPiece.move(144 - oversizedPiece.getBoundingBox().maxX(), 0, 0);
StructureStart oversized = new StructureStart(
source,
new ChunkPos(0, 0),
0,
new PiecesContainer(List.of(oversizedPiece))
);
StructureStart boundaryResult = NativeStructureReferenceEnvelope.wrapForPublication(
boundary, source, 0, new IrisStructureTerrain(), "test:boundary_content");
StructureStart result = NativeStructureReferenceEnvelope.wrapForPublication(
oversized, source, 0,
new IrisStructureTerrain(),
"test:oversized_content");
assertTrue(boundaryResult.isValid());
assertSame(StructureStart.INVALID_START, result);
assertFalse(NativeStructureStartInjector.isUsableGeneratedStart(result));
assertEquals(1, oversized.getPieces().size());
}
private static final class TestChunkGenerator extends ChunkGenerator {
private TestChunkGenerator() {
super(new FixedBiomeSource(Holder.direct((Biome) null)));
}
@Override
protected MapCodec<? extends ChunkGenerator> codec() {
return MapCodec.unit(this);
}
@Override
public void spawnOriginalMobs(WorldGenRegion worldGenRegion) {
}
@Override
public int getGenDepth() {
return 384;
}
@Override
public CompletableFuture<ChunkAccess> buildTerrain(
ChunkAccess centerChunk, Blender blender, RandomState randomState,
StructureManager structureManager, BiomeManager biomeManager, WorldGenRegion region, Set<Holder<Biome>> possibleBiomes) {
return CompletableFuture.completedFuture(centerChunk);
}
@Override
public int getSeaLevel() {
return 63;
}
@Override
public int getMinY() {
return -64;
}
@Override
public int getBaseHeight(int x, int z, Heightmap.Types type,
LevelHeightAccessor heightAccessor, RandomState randomState) {
return 64;
}
@Override
public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor heightAccessor,
RandomState randomState) {
BlockState[] states = new BlockState[heightAccessor.getHeight()];
for (int index = 0; index < states.length; index++) {
states[index] = Blocks.STONE.defaultBlockState();
}
return new NoiseColumn(heightAccessor.getMinY(), states);
}
@Override
public void addDebugScreenInfo(List<String> result, RandomState randomState,
BlockPos feetPos, SamplerContext samplerContext) {
}
}
private static StructurePoolElement inlineElement(int sizeX, int sizeY, int sizeZ) {
StructureTemplate template = new StructureTemplate();
CompoundTag tag = new CompoundTag();
ListTag size = new ListTag();
size.add(IntTag.valueOf(sizeX));
size.add(IntTag.valueOf(sizeY));
size.add(IntTag.valueOf(sizeZ));
tag.put("size", size);
template.load(BuiltInRegistries.BLOCK, tag);
return new InlinePoolElement(template);
}
private static final class InlinePoolElement extends SinglePoolElement {
private final StructureTemplate inlineTemplate;
private InlinePoolElement(StructureTemplate template) {
super(Either.right(template), Holder.direct(new StructureProcessorList(List.of())),
StructureTemplatePool.Projection.RIGID, Optional.<LiquidSettings>empty());
inlineTemplate = template;
}
@Override
public BoundingBox getBoundingBox(StructureTemplateManager templateManager,
BlockPos position, Rotation rotation) {
return inlineTemplate.getBoundingBox(
new StructurePlaceSettings().setRotation(rotation), position);
}
}
private static final class BoundingPoolElement extends StructurePoolElement {
private final int width;
private final int depth;
private BoundingPoolElement(int width, int depth) {
super(StructureTemplatePool.Projection.RIGID);
this.width = width;
this.depth = depth;
}
@Override
public Vec3i getSize(StructureTemplateManager templateManager, Rotation rotation) {
return new Vec3i(width, 1, depth);
}
@Override
public List<StructureTemplate.JigsawBlockInfo> getShuffledJigsawBlocks(
StructureTemplateManager templateManager, BlockPos position,
Rotation rotation, RandomSource random) {
return List.of();
}
@Override
public BoundingBox getBoundingBox(StructureTemplateManager templateManager,
BlockPos position, Rotation rotation) {
return new BoundingBox(
position.getX(), position.getY(), position.getZ(),
position.getX() + width - 1, position.getY(),
position.getZ() + depth - 1);
}
@Override
public boolean place(StructureTemplateManager templateManager,
WorldGenLevel world,
StructureManager structureManager, ChunkGenerator chunkGenerator,
BlockPos position, BlockPos pivot, Rotation rotation,
BoundingBox area, RandomSource random,
LiquidSettings liquidSettings, boolean keepJigsaws) {
return true;
}
@Override
public StructurePoolElementType<?> getType() {
return StructurePoolElementType.EMPTY;
}
}
private static final class BoundPoolHolder extends Holder.Reference<StructureTemplatePool> {
private BoundPoolHolder(String key) {
super(Type.STAND_ALONE, new HolderOwner<>() {
}, ResourceKey.create(Registries.TEMPLATE_POOL, Identifier.parse(key)), null);
}
private void bind(StructureTemplatePool pool) {
bindValue(pool);
}
}
}
@@ -0,0 +1,44 @@
package art.arcane.iris.nativegen.v26_3_R1;
import net.minecraft.world.level.levelgen.structure.placement.AbstractSpreadingStructurePlacement;
import net.minecraft.SharedConstants;
import net.minecraft.core.Vec3i;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement;
import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadType;
import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.Optional;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class NativeStructureGenerationKeysContractTest {
@BeforeClass
public static void bootstrapMinecraft() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void frequencyGateUsesMappedAccessorAndRejectsNonPositivePlacements() {
assertFalse(NativeStructureGenerationKeys.isEnabledPlacement(placement(0.0F)));
assertFalse(NativeStructureGenerationKeys.isEnabledPlacement(placement(-0.1F)));
assertFalse(NativeStructureGenerationKeys.isEnabledPlacement(placement(Float.NaN)));
assertTrue(NativeStructureGenerationKeys.isEnabledPlacement(placement(0.25F)));
}
private static RandomSpreadStructurePlacement placement(float frequency) {
return new RandomSpreadStructurePlacement(
Vec3i.ZERO,
AbstractSpreadingStructurePlacement.FrequencyReductionMethod.DEFAULT,
frequency,
1,
Optional.empty(),
2,
1,
RandomSpreadType.LINEAR);
}
}
@@ -0,0 +1,197 @@
package art.arcane.iris.nativegen.v26_3_R1;
import net.minecraft.SharedConstants;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.ChunkPos;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class NativeStructureLocatePersistenceTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void storageProbeScansEachCandidateOnlyOnce() {
AtomicInteger reads = new AtomicInteger();
CompoundTag chunk = new CompoundTag();
chunk.putInt("DataVersion", SharedConstants.getCurrentVersion().dataVersion().version());
chunk.putString("Status", "full");
CompoundTag start = new CompoundTag();
start.putString("id", "minecraft:village_plains");
start.putInt("references", 0);
CompoundTag starts = new CompoundTag();
starts.put("minecraft:village_plains", start);
CompoundTag structures = new CompoundTag();
structures.put("starts", starts);
chunk.put("structures", structures);
NativeStructureLocatePersistence.ProbeBudget budget =
new NativeStructureLocatePersistence.ProbeBudget(
512,
(chunkPos, visitor) -> {
reads.incrementAndGet();
chunk.acceptAsRoot(visitor);
return CompletableFuture.completedFuture(null);
});
ChunkPos chunkPos = new ChunkPos(7, -11);
assertTrue(budget.acceptsStored(
null, chunkPos, "minecraft:village_plains", true));
assertTrue(budget.acceptsStored(
null, chunkPos, "minecraft:village_plains", true));
assertEquals(1, reads.get());
assertEquals(1, budget.used());
}
@Test
public void legacyStoredChunkIsDatafixedBeforeItsStartIsClassified() {
AtomicBoolean datafixed = new AtomicBoolean();
CompoundTag legacy = new CompoundTag();
legacy.putInt("DataVersion", 100);
legacy.putString("Status", "full");
NativeStructureLocatePersistence.ProbeBudget budget =
new NativeStructureLocatePersistence.ProbeBudget(
512,
(chunkPos, visitor) -> {
legacy.acceptAsRoot(visitor);
return CompletableFuture.completedFuture(null);
},
(level, storedChunk) -> {
datafixed.set(true);
CompoundTag fixed = storedChunk.copy();
fixed.putInt("DataVersion",
SharedConstants.getCurrentVersion().dataVersion().version());
CompoundTag start = new CompoundTag();
start.putString("id", "minecraft:village_plains");
start.putInt("references", 1);
CompoundTag starts = new CompoundTag();
starts.put("minecraft:village_plains", start);
CompoundTag structures = new CompoundTag();
structures.put("starts", starts);
fixed.put("structures", structures);
return fixed;
});
assertFalse(budget.acceptsStored(
null, new ChunkPos(3, 4), "minecraft:village_plains", true));
assertTrue(datafixed.get());
}
@Test
public void partialDatafixFailureFallsBackToSelectedChunkVerification() {
CompoundTag legacy = new CompoundTag();
legacy.putInt("DataVersion", 100);
NativeStructureLocatePersistence.ProbeBudget budget =
new NativeStructureLocatePersistence.ProbeBudget(
512,
(chunkPos, visitor) -> {
legacy.acceptAsRoot(visitor);
return CompletableFuture.completedFuture(null);
},
(level, storedChunk) -> {
throw new IllegalStateException("broken partial datafix");
});
assertTrue(budget.acceptsStored(
null, new ChunkPos(9, -2), "minecraft:village_plains", true));
}
@Test
public void missingStartsContainerFallsBackToSelectedChunkVerification() {
CompoundTag chunk = currentChunk();
NativeStructureLocatePersistence.ProbeBudget budget = budget(chunk);
assertTrue(budget.acceptsStored(
null, new ChunkPos(5, 8), "minecraft:village_plains", false));
assertTrue(budget.acceptsStored(
null, new ChunkPos(5, 8), "minecraft:village_plains", true));
}
@Test
public void presentEmptyStartsContainerRejectsTheCandidateWithoutLoading() {
CompoundTag chunk = currentChunk();
CompoundTag structures = new CompoundTag();
structures.put("starts", new CompoundTag());
chunk.put("structures", structures);
NativeStructureLocatePersistence.ProbeBudget budget = budget(chunk);
assertFalse(budget.acceptsStored(
null, new ChunkPos(-3, 2), "minecraft:village_plains", false));
}
@Test
public void collisionTombstoneIsRejectedForNormalAndUnexploredLocate() {
CompoundTag chunk = currentChunk();
CompoundTag tombstone = new CompoundTag();
tombstone.putString("id", "INVALID");
CompoundTag starts = new CompoundTag();
starts.put("minecraft:village_plains", tombstone);
CompoundTag structures = new CompoundTag();
structures.put("starts", starts);
chunk.put("structures", structures);
ChunkPos chunkPos = new ChunkPos(12, -9);
assertFalse(budget(chunk).acceptsStored(
null, chunkPos, "minecraft:village_plains", false));
assertFalse(budget(chunk).acceptsStored(
null, chunkPos, "minecraft:village_plains", true));
}
@Test
public void malformedNonCompoundStartIsRejectedWithoutLoading() {
CompoundTag chunk = currentChunk();
CompoundTag starts = new CompoundTag();
starts.putInt("minecraft:village_plains", 1);
CompoundTag structures = new CompoundTag();
structures.put("starts", starts);
chunk.put("structures", structures);
assertFalse(budget(chunk).acceptsStored(
null, new ChunkPos(1, 1), "minecraft:village_plains", false));
}
@Test
public void referencedStoredStartIsOnlyRejectedForUnexploredLocate() {
CompoundTag chunk = currentChunk();
CompoundTag start = new CompoundTag();
start.putString("id", "minecraft:village_plains");
start.putInt("references", 1);
CompoundTag starts = new CompoundTag();
starts.put("minecraft:village_plains", start);
CompoundTag structures = new CompoundTag();
structures.put("starts", starts);
chunk.put("structures", structures);
ChunkPos chunkPos = new ChunkPos(-4, -7);
assertTrue(budget(chunk).acceptsStored(
null, chunkPos, "minecraft:village_plains", false));
assertFalse(budget(chunk).acceptsStored(
null, chunkPos, "minecraft:village_plains", true));
}
private static CompoundTag currentChunk() {
CompoundTag chunk = new CompoundTag();
chunk.putInt("DataVersion", SharedConstants.getCurrentVersion().dataVersion().version());
return chunk;
}
private static NativeStructureLocatePersistence.ProbeBudget budget(CompoundTag chunk) {
return new NativeStructureLocatePersistence.ProbeBudget(
512,
(chunkPos, visitor) -> {
chunk.acceptAsRoot(visitor);
return CompletableFuture.completedFuture(null);
});
}
}
@@ -0,0 +1,346 @@
package art.arcane.iris.nativegen.v26_3_R1;
import art.arcane.iris.structure.nativegen.NativeStructureOwnershipRecord;
import art.arcane.iris.structure.nativegen.NativeStructureStartPlan;
import art.arcane.iris.structure.nativegen.IrisNativeStructure;
import art.arcane.iris.structure.placement.IrisStructurePlacement;
import art.arcane.iris.structure.placement.IrisStructureTerrain;
import art.arcane.iris.structure.placement.IrisStructureTerrainMode;
import art.arcane.volmlib.util.collection.KList;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.HolderSet;
import net.minecraft.core.Vec3i;
import net.minecraft.nbt.ListTag;
import net.minecraft.server.Bootstrap;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.LegacyRandomSource;
import net.minecraft.world.level.levelgen.RandomSupport;
import net.minecraft.world.level.levelgen.WorldgenRandom;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElementType;
import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentPieces;
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentStructure;
import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
public class NativeStructureOwnershipFingerprintTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void monumentFingerprintSurvivesSourceYRegenerationWithoutSyntheticPieces() {
long seed = 8372619L;
ChunkPos origin = new ChunkPos(9, -4);
OceanMonumentStructure structure = new OceanMonumentStructure(
new OceanMonumentStructure.StructureSettings(HolderSet.empty()));
StructureStart generated = monumentStart(structure, origin, seed);
NativeStructureVerticalPlacer.alignOceanMonumentToSeaLevel(
generated, 0, 80, -64, 320);
int alignedLocatorY = NativeStructureOwnershipFingerprint.locatorY(generated);
IrisStructureTerrain terrain = new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.FORCE_CARVE)
.setHorizontalPadding(24);
StructureStart wrapped = NativeStructureReferenceEnvelope.wrap(
generated,
structure,
0,
terrain);
String beforeReload = NativeStructureOwnershipFingerprint.fingerprint(
"minecraft:monument", wrapped);
ListTag serializedPieces = (ListTag) new PiecesContainer(wrapped.getPieces()).save(null);
PiecesContainer loadedPieces = PiecesContainer.load(serializedPieces, null);
assertEquals(1, loadedPieces.pieces().size());
assertTrue(loadedPieces.pieces().getFirst()
instanceof OceanMonumentPieces.MonumentBuilding);
PiecesContainer regeneratedPieces = OceanMonumentStructure.regeneratePiecesAfterLoad(
origin, seed, loadedPieces);
StructureStart reloaded = new StructureStart(structure, origin, 0, regeneratedPieces);
String afterReload = NativeStructureOwnershipFingerprint.fingerprint(
"minecraft:monument", reloaded);
NativeStructureOwnershipRecord ownership = NativeStructureOwnershipFingerprint.capture(
"minecraft:monument",
wrapped,
plan(origin, NativeStructureReferenceEnvelope.contentBounds(wrapped).minY()),
NativeStructureReferenceEnvelope.referenceBounds(wrapped, structure, terrain));
assertEquals(beforeReload, afterReload);
assertTrue(NativeStructureOwnershipFingerprint.matches(ownership, reloaded));
assertNotEquals(alignedLocatorY, NativeStructureOwnershipFingerprint.locatorY(reloaded));
assertEquals(alignedLocatorY, ownership.locatorY());
assertEquals(1, reloaded.getPieces().size());
assertEquals(1, wrapped.getPieces().size());
}
@Test
public void sameKeyAndOriginStillRequiresMatchingContentIdentity() {
long seed = 18273L;
ChunkPos origin = new ChunkPos(2, 3);
OceanMonumentStructure structure = new OceanMonumentStructure(
new OceanMonumentStructure.StructureSettings(HolderSet.empty()));
StructureStart expected = monumentStart(structure, origin, seed);
StructureStart moved = monumentStart(structure, origin, seed);
for (StructurePiece piece : moved.getPieces()) {
piece.move(1, 0, 0);
}
NativeStructureOwnershipRecord ownership = NativeStructureOwnershipFingerprint.capture(
"minecraft:monument",
expected,
plan(origin, NativeStructureReferenceEnvelope.contentBounds(expected).minY()),
expected.getBoundingBox());
assertFalse(NativeStructureOwnershipFingerprint.matches(ownership, moved));
}
@Test
public void persistedEnvelopeRepairsReloadedMonumentReferenceOutsideLiveBounds() {
long seed = 772931L;
ChunkPos origin = new ChunkPos(4, -7);
OceanMonumentStructure structure = new OceanMonumentStructure(
new OceanMonumentStructure.StructureSettings(HolderSet.empty()));
StructureStart generated = monumentStart(structure, origin, seed);
NativeStructureVerticalPlacer.alignOceanMonumentToSeaLevel(
generated, 0, 80, -64, 320);
IrisStructureTerrain terrain = new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.FORCE_CARVE)
.setHorizontalPadding(24);
StructureStart wrapped = NativeStructureReferenceEnvelope.wrap(
generated,
structure,
0,
terrain);
NativeStructureOwnershipRecord ownership = NativeStructureOwnershipFingerprint.capture(
"minecraft:monument",
wrapped,
plan(origin, NativeStructureReferenceEnvelope.contentBounds(wrapped).minY()),
NativeStructureReferenceEnvelope.referenceBounds(wrapped, structure, terrain));
PiecesContainer regeneratedPieces = OceanMonumentStructure.regeneratePiecesAfterLoad(
origin, seed, new PiecesContainer(wrapped.getPieces()));
StructureStart reloaded = new StructureStart(structure, origin, 0, regeneratedPieces);
ChunkPos target = outsideLiveBounds(ownership, reloaded);
assertNotNull(target);
assertTrue(NativeStructureReferenceRepair.requiresReference(
target, "minecraft:monument", reloaded, ownership));
assertFalse(NativeStructureReferenceRepair.requiresReference(
target, "minecraft:monument", reloaded, null));
}
@Test
public void overlappingManualStartsSixteenChunksApartRemainIndependentlyReferenced() {
OceanMonumentStructure structure = new OceanMonumentStructure(
new OceanMonumentStructure.StructureSettings(HolderSet.empty()));
ChunkPos westOrigin = new ChunkPos(0, 0);
ChunkPos eastOrigin = new ChunkPos(16, 0);
StructureStart west = monumentStart(structure, westOrigin, 317L);
StructureStart east = monumentStart(structure, eastOrigin, 941L);
west.getPieces().getFirst().move(112, 0, 0);
east.getPieces().getFirst().move(-96, 0, 0);
NativeStructureOwnershipRecord westOwnership = ownership(westOrigin, west, structure);
NativeStructureOwnershipRecord eastOwnership = ownership(eastOrigin, east, structure);
ChunkPos shared = new ChunkPos(8, 0);
assertTrue(NativeStructureReferenceEnvelope.contentBounds(west).intersects(
NativeStructureReferenceEnvelope.contentBounds(east)));
assertTrue(NativeStructureReferenceRepair.requiresReference(
shared, "minecraft:monument", west, westOwnership));
assertTrue(NativeStructureReferenceRepair.requiresReference(
shared, "minecraft:monument", east, eastOwnership));
}
@Test
public void denseAdjacentManualStartsDoNotSuppressEachOther() {
OceanMonumentStructure structure = new OceanMonumentStructure(
new OceanMonumentStructure.StructureSettings(HolderSet.empty()));
ChunkPos firstOrigin = new ChunkPos(0, 0);
ChunkPos secondOrigin = new ChunkPos(1, 0);
StructureStart first = monumentStart(structure, firstOrigin, 17L);
StructureStart second = monumentStart(structure, secondOrigin, 23L);
NativeStructureOwnershipRecord firstOwnership = ownership(firstOrigin, first, structure);
NativeStructureOwnershipRecord secondOwnership = ownership(secondOrigin, second, structure);
ChunkPos shared = new ChunkPos(0, 0);
assertTrue(NativeStructureReferenceEnvelope.contentBounds(first).intersects(
NativeStructureReferenceEnvelope.contentBounds(second)));
assertTrue(NativeStructureReferenceRepair.requiresReference(
shared, "minecraft:monument", first, firstOwnership));
assertTrue(NativeStructureReferenceRepair.requiresReference(
shared, "minecraft:monument", second, secondOwnership));
}
@Test
public void customPoolElementIdentityDoesNotDependOnProcessLocalToString() {
OceanMonumentStructure structure = new OceanMonumentStructure(
new OceanMonumentStructure.StructureSettings(HolderSet.empty()));
ChunkPos origin = new ChunkPos(3, -2);
StructureStart first = poolStart(structure, origin, new IdentityStringPoolElement());
StructureStart reloaded = poolStart(structure, origin, new IdentityStringPoolElement());
assertEquals(
NativeStructureOwnershipFingerprint.fingerprint("test:custom_pool", first),
NativeStructureOwnershipFingerprint.fingerprint("test:custom_pool", reloaded));
}
@Test
public void namedPoolTemplateRemainsPartOfTheStableFingerprint() {
OceanMonumentStructure structure = new OceanMonumentStructure(
new OceanMonumentStructure.StructureSettings(HolderSet.empty()));
ChunkPos origin = new ChunkPos(3, -2);
StructurePoolElement firstElement = StructurePoolElement.single("test:first")
.apply(StructureTemplatePool.Projection.RIGID);
StructurePoolElement secondElement = StructurePoolElement.single("test:second")
.apply(StructureTemplatePool.Projection.RIGID);
assertNotEquals(
NativeStructureOwnershipFingerprint.fingerprint(
"test:named_pool", poolStart(structure, origin, firstElement)),
NativeStructureOwnershipFingerprint.fingerprint(
"test:named_pool", poolStart(structure, origin, secondElement)));
}
private static StructureStart monumentStart(OceanMonumentStructure structure,
ChunkPos origin, long seed) {
WorldgenRandom random = new WorldgenRandom(
new LegacyRandomSource(RandomSupport.generateUniqueSeed()));
random.setLargeFeatureSeed(seed, origin.x(), origin.z());
Direction orientation = Direction.Plane.HORIZONTAL.getRandomDirection(random);
OceanMonumentPieces.MonumentBuilding building = new OceanMonumentPieces.MonumentBuilding(
random,
origin.getMinBlockX() - 29,
origin.getMinBlockZ() - 29,
orientation
);
return new StructureStart(
structure,
origin,
0,
new PiecesContainer(List.of(building))
);
}
private static NativeStructureStartPlan plan(ChunkPos origin, int baseY) {
IrisNativeStructure source = new IrisNativeStructure()
.setStructure("minecraft:monument")
.setWeight(1);
IrisStructurePlacement placement = new IrisStructurePlacement()
.setPlacementId("monument-test")
.setNativeStructures(new KList<IrisNativeStructure>().qadd(source));
return new NativeStructureStartPlan(
placement,
source,
origin.x(),
origin.z(),
baseY
);
}
private static NativeStructureOwnershipRecord ownership(
ChunkPos origin, StructureStart start, OceanMonumentStructure structure) {
return NativeStructureOwnershipFingerprint.capture(
"minecraft:monument",
start,
plan(origin, NativeStructureReferenceEnvelope.contentBounds(start).minY()),
NativeStructureReferenceEnvelope.referenceBounds(
start, structure, new IrisStructureTerrain())
);
}
private static StructureStart poolStart(
OceanMonumentStructure structure, ChunkPos origin, StructurePoolElement element) {
BlockPos position = new BlockPos(
origin.getMiddleBlockX(), 64, origin.getMiddleBlockZ());
PoolElementStructurePiece piece = new PoolElementStructurePiece(
null, element, position, 0, Rotation.NONE,
new BoundingBox(position), LiquidSettings.APPLY_WATERLOGGING);
return new StructureStart(
structure, origin, 0, new PiecesContainer(List.of(piece)));
}
private static final class IdentityStringPoolElement extends StructurePoolElement {
private IdentityStringPoolElement() {
super(StructureTemplatePool.Projection.RIGID);
}
@Override
public Vec3i getSize(StructureTemplateManager templateManager, Rotation rotation) {
return new Vec3i(1, 1, 1);
}
@Override
public List<StructureTemplate.JigsawBlockInfo> getShuffledJigsawBlocks(
StructureTemplateManager templateManager, BlockPos position,
Rotation rotation, RandomSource random) {
return List.of();
}
@Override
public BoundingBox getBoundingBox(StructureTemplateManager templateManager,
BlockPos position, Rotation rotation) {
return new BoundingBox(position);
}
@Override
public boolean place(StructureTemplateManager templateManager,
WorldGenLevel world, StructureManager structureManager,
ChunkGenerator chunkGenerator, BlockPos position, BlockPos pivot,
Rotation rotation, BoundingBox area, RandomSource random,
LiquidSettings liquidSettings, boolean keepJigsaws) {
return true;
}
@Override
public StructurePoolElementType<?> getType() {
return StructurePoolElementType.EMPTY;
}
@Override
public String toString() {
return "IdentityStringPoolElement@"
+ Integer.toHexString(System.identityHashCode(this));
}
}
private static ChunkPos outsideLiveBounds(NativeStructureOwnershipRecord ownership,
StructureStart start) {
for (int chunkX = ownership.referenceMinChunkX();
chunkX <= ownership.referenceMaxChunkX(); chunkX++) {
for (int chunkZ = ownership.referenceMinChunkZ();
chunkZ <= ownership.referenceMaxChunkZ(); chunkZ++) {
ChunkPos candidate = new ChunkPos(chunkX, chunkZ);
if (!start.getBoundingBox().intersects(
candidate.getMinBlockX(), candidate.getMinBlockZ(),
candidate.getMaxBlockX(), candidate.getMaxBlockZ())) {
return candidate;
}
}
}
return null;
}
}
@@ -0,0 +1,314 @@
package art.arcane.iris.nativegen.v26_3_R1;
import art.arcane.iris.structure.nativegen.NativeStructureOwnershipRecord;
import art.arcane.iris.structure.nativegen.NativeStructureStartPlan;
import art.arcane.iris.structure.placement.StructurePlacementGrid;
import art.arcane.iris.structure.nativegen.IrisNativeStructure;
import art.arcane.iris.structure.placement.IrisStructurePlacement;
import art.arcane.iris.structure.placement.IrisStructureTerrain;
import art.arcane.iris.structure.placement.IrisStructureTerrainMode;
import art.arcane.volmlib.util.collection.KList;
import net.minecraft.SharedConstants;
import net.minecraft.core.Direction;
import net.minecraft.core.HolderSet;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.levelgen.LegacyRandomSource;
import net.minecraft.world.level.levelgen.RandomSupport;
import net.minecraft.world.level.levelgen.WorldgenRandom;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentPieces;
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentStructure;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public class NativeStructureOwnershipRecoveryTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void missingSidecarRecoversExactPolicyEnvelopeAndLocatorAfterReload() {
long seed = 5519231L;
ChunkPos origin = new ChunkPos(7, -5);
OceanMonumentStructure structure = structure();
StructureStart expected = monumentStart(structure, origin, seed);
NativeStructureVerticalPlacer.alignOceanMonumentToSeaLevel(
expected, 0, 80, -64, 320);
int expectedLocatorY = NativeStructureOwnershipFingerprint.locatorY(expected);
NativeStructureStartPlan plan = plan(origin, "recovery-exact", 24);
StructureStart reloaded = new StructureStart(
structure,
origin,
expected.getReferences(),
OceanMonumentStructure.regeneratePiecesAfterLoad(
origin, seed, new PiecesContainer(expected.getPieces()))
);
NativeStructureOwnershipRecord recovered =
NativeStructureOwnershipRecovery.proveCandidate(
"minecraft:monument", structure, reloaded,
plan, expected, false);
assertNotNull(recovered);
assertEquals(StructurePlacementGrid.placementIdentity(plan.placement()),
recovered.placementIdentity());
assertEquals(expectedLocatorY, recovered.locatorY());
assertNotEquals(NativeStructureOwnershipFingerprint.locatorY(reloaded),
recovered.locatorY());
assertEquals(IrisStructureTerrainMode.FORCE_CARVE,
recovered.restoredDecision().terrain().resolvedMode());
assertEquals(24, recovered.restoredDecision().terrain().getHorizontalPadding());
assertTrue(recovered.referenceMinChunkX()
< NativeStructureReferenceEnvelope.contentBounds(reloaded).minX() >> 4);
}
@Test
public void naturalSameKeyStartRemainsUnownedWhenOriginIsAmbiguous() {
long seed = 22817L;
ChunkPos origin = new ChunkPos(-3, 6);
OceanMonumentStructure structure = structure();
StructureStart natural = monumentStart(structure, origin, seed);
NativeStructureStartPlan plan = plan(origin, "natural-ambiguous", 0);
NativeStructureOwnershipRecord recovered =
NativeStructureOwnershipRecovery.proveCandidate(
"minecraft:monument", structure, natural,
plan, natural, true);
assertNull(recovered);
}
@Test
public void changedPlacementIdentityOrGeometryCannotRecoverOwnership() {
long seed = 991723L;
ChunkPos origin = new ChunkPos(2, 4);
OceanMonumentStructure structure = structure();
StructureStart expected = monumentStart(structure, origin, seed);
StructureStart changedGeometry = monumentStart(structure, origin, seed);
for (StructurePiece piece : changedGeometry.getPieces()) {
piece.move(1, 0, 0);
}
NativeStructureStartPlan originalIdentity = plan(
origin, "original-identity", 0);
NativeStructureStartPlan changedIdentity = plan(
new ChunkPos(origin.x() + 1, origin.z()), "changed-identity", 0);
assertNotEquals(
StructurePlacementGrid.placementIdentity(originalIdentity.placement()),
StructurePlacementGrid.placementIdentity(changedIdentity.placement()));
assertNull(NativeStructureOwnershipRecovery.proveCandidate(
"minecraft:monument", structure, expected,
changedIdentity, expected, false));
assertNull(NativeStructureOwnershipRecovery.proveCandidate(
"minecraft:monument", structure, changedGeometry,
plan(origin, "geometry-check", 0), expected, false));
}
@Test
public void recoveredEnvelopeStillRequiresExactFingerprint() {
long seed = 81337L;
ChunkPos origin = new ChunkPos(-8, -9);
OceanMonumentStructure structure = structure();
StructureStart expected = monumentStart(structure, origin, seed);
NativeStructureOwnershipRecord recovered =
NativeStructureOwnershipRecovery.proveCandidate(
"minecraft:monument", structure, expected,
plan(origin, "fingerprint-check", 16), expected, false);
assertNotNull(recovered);
StructureStart moved = monumentStart(structure, origin, seed);
for (StructurePiece piece : moved.getPieces()) {
piece.move(0, 0, 1);
}
assertFalse(NativeStructureOwnershipFingerprint.matches(recovered, moved));
}
@Test
public void staleVacuumEnvelopeRefreshesWithoutReplacingOwnershipIdentity() {
String structureKey = "minecraft:monument";
long seed = 648231L;
ChunkPos origin = new ChunkPos(5, -6);
OceanMonumentStructure structure = structure();
StructureStart start = monumentStart(structure, origin, seed);
NativeStructureStartPlan plan = plan(
origin, "vacuum-envelope-refresh", IrisStructureTerrainMode.VACUUM, 0);
BoundingBox content = NativeStructureReferenceEnvelope.contentBounds(start);
NativeStructureOwnershipRecord stale = NativeStructureOwnershipFingerprint.capture(
structureKey, start, plan, content);
NativeStructureOwnershipRecord refreshed =
NativeStructureOwnershipRecovery.refreshReferenceEnvelope(
structureKey, structure, start, stale);
assertNotNull(refreshed);
assertNotEquals(stale, refreshed);
assertEquals(stale.schema(), refreshed.schema());
assertEquals(stale.ownershipKey(), refreshed.ownershipKey());
assertEquals(stale.placementIdentity(), refreshed.placementIdentity());
assertEquals(stale.baseY(), refreshed.baseY());
assertEquals(stale.locatorY(), refreshed.locatorY());
assertEquals(stale.contentFingerprint(), refreshed.contentFingerprint());
assertEquals(stale.decision(), refreshed.decision());
assertEquals(IrisStructureTerrainMode.VACUUM,
refreshed.restoredDecision().terrain().resolvedMode());
BoundingBox expected = NativeStructureReferenceEnvelope.referenceBounds(
start, structure, plan.placement().resolvedTerrain(), structureKey);
assertEquals(expected.minX() >> 4, refreshed.referenceMinChunkX());
assertEquals(expected.maxX() >> 4, refreshed.referenceMaxChunkX());
assertEquals(expected.minZ() >> 4, refreshed.referenceMinChunkZ());
assertEquals(expected.maxZ() >> 4, refreshed.referenceMaxChunkZ());
assertTrue(hasExpandedCoverage(stale, refreshed));
assertSame(refreshed, NativeStructureOwnershipRecovery.refreshReferenceEnvelope(
structureKey, structure, start, refreshed));
}
@Test
public void staleEnvelopeCannotRefreshAgainstDifferentContent() {
String structureKey = "minecraft:monument";
long seed = 412987L;
ChunkPos origin = new ChunkPos(-2, 7);
OceanMonumentStructure structure = structure();
StructureStart expected = monumentStart(structure, origin, seed);
NativeStructureStartPlan plan = plan(
origin, "vacuum-content-check", IrisStructureTerrainMode.VACUUM, 0);
NativeStructureOwnershipRecord stale = NativeStructureOwnershipFingerprint.capture(
structureKey, expected, plan,
NativeStructureReferenceEnvelope.contentBounds(expected));
StructureStart moved = monumentStart(structure, origin, seed);
for (StructurePiece piece : moved.getPieces()) {
piece.move(1, 0, 0);
}
assertNull(NativeStructureOwnershipRecovery.refreshReferenceEnvelope(
structureKey, structure, moved, stale));
}
@Test
public void currentNonVacuumEnvelopeRemainsThePersistedAuthority() {
String structureKey = "minecraft:monument";
long seed = 927451L;
ChunkPos origin = new ChunkPos(3, 8);
OceanMonumentStructure structure = structure();
StructureStart start = monumentStart(structure, origin, seed);
NativeStructureStartPlan plan = plan(origin, "current-force-carve", 24);
BoundingBox envelope = NativeStructureReferenceEnvelope.referenceBounds(
start, structure, plan.placement().resolvedTerrain(), structureKey);
NativeStructureOwnershipRecord ownership = NativeStructureOwnershipFingerprint.capture(
structureKey, start, plan, envelope);
assertSame(ownership, NativeStructureOwnershipRecovery.refreshReferenceEnvelope(
structureKey, structure, start, ownership));
}
@Test
public void clippedVacuumEnvelopeRemainsStableAtTheReferenceLimit() {
String structureKey = "minecraft:monument";
long seed = 381729L;
ChunkPos origin = new ChunkPos(0, 0);
OceanMonumentStructure structure = structure();
StructureStart start = monumentStart(structure, origin, seed);
BoundingBox initial = NativeStructureReferenceEnvelope.contentBounds(start);
int maximumReferenceBlockX = ((origin.x()
+ NativeStructureOwnershipRecord.MAX_REFERENCE_DISTANCE_CHUNKS) << 4) + 15;
int shiftX = maximumReferenceBlockX - initial.maxX();
for (StructurePiece piece : start.getPieces()) {
piece.move(shiftX, 0, 0);
}
NativeStructureStartPlan plan = plan(
origin, "clipped-vacuum", IrisStructureTerrainMode.VACUUM, 0);
BoundingBox envelope = NativeStructureReferenceEnvelope.referenceBounds(
start, structure, plan.placement().resolvedTerrain(), structureKey);
NativeStructureOwnershipRecord ownership = NativeStructureOwnershipFingerprint.capture(
structureKey, start, plan, envelope);
assertEquals(origin.x() + NativeStructureOwnershipRecord.MAX_REFERENCE_DISTANCE_CHUNKS,
ownership.referenceMaxChunkX());
assertSame(ownership, NativeStructureOwnershipRecovery.refreshReferenceEnvelope(
structureKey, structure, start, ownership));
}
private static OceanMonumentStructure structure() {
return new OceanMonumentStructure(
new OceanMonumentStructure.StructureSettings(HolderSet.empty()));
}
private static StructureStart monumentStart(OceanMonumentStructure structure,
ChunkPos origin, long seed) {
WorldgenRandom random = new WorldgenRandom(
new LegacyRandomSource(RandomSupport.generateUniqueSeed()));
random.setLargeFeatureSeed(seed, origin.x(), origin.z());
Direction orientation = Direction.Plane.HORIZONTAL.getRandomDirection(random);
OceanMonumentPieces.MonumentBuilding building =
new OceanMonumentPieces.MonumentBuilding(
random,
origin.getMinBlockX() - 29,
origin.getMinBlockZ() - 29,
orientation
);
return new StructureStart(
structure, origin, 0, new PiecesContainer(List.of(building)));
}
private static NativeStructureStartPlan plan(ChunkPos origin,
String placementId,
int horizontalPadding) {
return plan(origin, placementId,
IrisStructureTerrainMode.FORCE_CARVE, horizontalPadding);
}
private static NativeStructureStartPlan plan(ChunkPos origin,
String placementId,
IrisStructureTerrainMode terrainMode,
int horizontalPadding) {
IrisNativeStructure source = new IrisNativeStructure()
.setStructure("minecraft:monument")
.setWeight(1);
IrisStructureTerrain terrain = new IrisStructureTerrain()
.setMode(terrainMode)
.setHorizontalPadding(horizontalPadding);
IrisStructurePlacement placement = new IrisStructurePlacement()
.setPlacementId(placementId)
.setTerrain(terrain)
.setNativeStructures(new KList<IrisNativeStructure>().qadd(source));
return new NativeStructureStartPlan(
placement,
source,
origin.x(),
origin.z(),
NativeStructureReferenceEnvelope.contentBounds(
monumentStart(structure(), origin, 1L)).minY()
);
}
private static boolean hasExpandedCoverage(
NativeStructureOwnershipRecord stale,
NativeStructureOwnershipRecord refreshed) {
for (int chunkX = refreshed.referenceMinChunkX();
chunkX <= refreshed.referenceMaxChunkX(); chunkX++) {
for (int chunkZ = refreshed.referenceMinChunkZ();
chunkZ <= refreshed.referenceMaxChunkZ(); chunkZ++) {
if (!stale.covers(chunkX, chunkZ)) {
return true;
}
}
}
return false;
}
}
@@ -0,0 +1,532 @@
package art.arcane.iris.nativegen.v26_3_R1;
import art.arcane.iris.generation.terrain.IrisMaterialPalette;
import art.arcane.iris.structure.placement.IrisStructureTerrain;
import art.arcane.iris.structure.placement.IrisStructureTerrainMode;
import art.arcane.iris.structure.placement.IrisStructureYBand;
import art.arcane.volmlib.util.math.RNG;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderSet;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.Bootstrap;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.dimension.BuiltinDimensionTypes;
import net.minecraft.world.level.levelgen.GenerationStep;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.TerrainAdjustment;
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidPiece;
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidStructure;
import net.minecraft.world.level.levelgen.structure.templatesystem.BlockIgnoreProcessor;
import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessorList;
import org.junit.BeforeClass;
import org.junit.Test;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.BitSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class NativeStructurePostProcessorEncaseTest {
private static final long TEST_SEED = 1234L;
@BeforeClass
public static void bootstrapMinecraft() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void encaseFillsAirAndLiquidWithoutOverwritingExistingTerrain() {
StructureStart start = start(TerrainAdjustment.BURY, 64);
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
BoundingBox area = new BoundingBox(
bounds.minX() - 1, bounds.minY() - 1, bounds.minZ() - 1,
bounds.maxX() + 1, bounds.maxY() + 1, bounds.maxZ() + 1);
Map<BlockPos, BlockState> blocks = new HashMap<>();
put(blocks, bounds.minX(), bounds.minY(), bounds.minZ(), Blocks.DIRT.defaultBlockState());
put(blocks, bounds.maxX(), bounds.minY(), bounds.maxZ(), Blocks.WATER.defaultBlockState());
NativeStructureTerrainIntegrator.integrateTerrain(
world(blocks), area, "minecraft:stronghold", start,
new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.ENCASE)
.setHorizontalPadding(1)
.setCeilingPadding(1)
.setFloorPadding(1),
null);
assertEquals(Blocks.DIRT.defaultBlockState(),
state(blocks, bounds.minX(), bounds.minY(), bounds.minZ()));
assertEquals(Blocks.STONE.defaultBlockState(),
state(blocks, bounds.maxX(), bounds.minY(), bounds.maxZ()));
assertEquals(Blocks.STONE.defaultBlockState(),
state(blocks, bounds.minX() - 1, bounds.minY() - 1, bounds.minZ() - 1));
assertEquals(Blocks.STONE.defaultBlockState(),
state(blocks, bounds.maxX() + 1, bounds.maxY() + 1, bounds.maxZ() + 1));
}
@Test
public void defaultEncasePaletteSplitsStoneAboveZeroAndDeepslateBelow() {
StructureStart start = start(TerrainAdjustment.BURY, -1);
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
Map<BlockPos, BlockState> blocks = new HashMap<>();
NativeStructureTerrainIntegrator.integrateTerrain(
world(blocks), bounds, "minecraft:stronghold", start,
new IrisStructureTerrain().setMode(IrisStructureTerrainMode.ENCASE),
null);
assertEquals(-1, bounds.minY());
assertEquals(Blocks.DEEPSLATE.defaultBlockState(),
state(blocks, bounds.minX(), -1, bounds.minZ()));
assertEquals(Blocks.STONE.defaultBlockState(),
state(blocks, bounds.minX(), 0, bounds.minZ()));
}
@Test
public void defaultEncaseMaterialFollowsVanillaDimensionTerrain() {
assertEquals(Blocks.NETHERRACK.defaultBlockState(),
NativeStructureTerrainIntegrator.defaultEncaseBlock(Level.NETHER, 64));
assertEquals(Blocks.END_STONE.defaultBlockState(),
NativeStructureTerrainIntegrator.defaultEncaseBlock(Level.END, 64));
assertEquals(Blocks.STONE.defaultBlockState(),
NativeStructureTerrainIntegrator.defaultEncaseBlock(Level.OVERWORLD, 64));
}
@Test
public void sourceEncaseMaterialFollowsNearbyIrisTerrain() {
Map<BlockPos, BlockState> blocks = new HashMap<>();
BlockPos fill = new BlockPos(4, 64, 8);
put(blocks, fill.getX() + 3, fill.getY(), fill.getZ(), Blocks.TUFF.defaultBlockState());
assertEquals(Blocks.TUFF.defaultBlockState(),
NativeStructureTerrainIntegrator.sourceEncaseBlock(world(blocks), fill));
}
@Test
public void sourceMaterialSnapshotIgnoresPreviouslyDecoratedAdjacentChunks() {
Map<BlockPos, BlockState> blocks = new HashMap<>();
BlockPos fill = new BlockPos(0, 64, 8);
put(blocks, -1, 64, 8, Blocks.OAK_PLANKS.defaultBlockState());
put(blocks, 3, 64, 8, Blocks.TUFF.defaultBlockState());
WorldGenLevel world = world(blocks);
NativeStructureTerrainIntegrator.SourceTerrainSnapshot sourceTerrain =
sourceSnapshot(world, new BoundingBox(0, 64, 0, 15, 64, 15));
assertEquals(Blocks.TUFF.defaultBlockState(),
NativeStructureTerrainIntegrator.sourceEncaseBlock(
world, fill, sourceTerrain));
}
@Test
public void sourceMaterialSnapshotIsStableAcrossEarlierFillsInTheSameChunk() {
Map<BlockPos, BlockState> blocks = new HashMap<>();
BlockPos fill = new BlockPos(0, 64, 8);
put(blocks, 3, 64, 8, Blocks.TUFF.defaultBlockState());
WorldGenLevel world = world(blocks);
NativeStructureTerrainIntegrator.SourceTerrainSnapshot sourceTerrain =
sourceSnapshot(world, new BoundingBox(0, 64, 0, 15, 64, 15));
put(blocks, 1, 64, 8, Blocks.OAK_PLANKS.defaultBlockState());
assertEquals(Blocks.TUFF.defaultBlockState(),
NativeStructureTerrainIntegrator.sourceEncaseBlock(
world, fill, sourceTerrain));
}
@Test
public void sourceMaterialRejectsStructureBlocksAndOresBeforeDimensionFallback() {
Map<BlockPos, BlockState> blocks = new HashMap<>();
BlockPos fill = new BlockPos(4, -20, 8);
put(blocks, 3, -20, 8, Blocks.OAK_PLANKS.defaultBlockState());
put(blocks, 5, -20, 8, Blocks.STONE_BRICKS.defaultBlockState());
put(blocks, 4, -20, 7, Blocks.IRON_ORE.defaultBlockState());
assertEquals(Blocks.DEEPSLATE.defaultBlockState(),
NativeStructureTerrainIntegrator.sourceEncaseBlock(world(blocks), fill));
}
@Test
public void sourceMaterialSnapshotOnlyAllocatesActualFillLayers() {
StructureStart start = start(TerrainAdjustment.BURY, 64);
BoundingBox area = new BoundingBox(0, -2032, 0, 15, 2030, 15);
NativeStructureTerrainIntegrator.SourceTerrainSnapshot sourceTerrain =
NativeStructureTerrainIntegrator.captureSourceTerrain(
world(new HashMap<>()), area,
List.of(new NativeStructureTerrainIntegrator.TerrainTarget(
"test:bounded-snapshot", start,
new IrisStructureTerrain().setMode(IrisStructureTerrainMode.SOURCE))));
assertNotNull(sourceTerrain);
assertEquals(23 * 16 * 16, sourceTerrain.sampledCells());
}
@Test
public void customDimensionKeyUsesRegisteredVanillaDimensionType() {
ResourceKey<Level> customDimension = ResourceKey.create(
Registries.DIMENSION, Identifier.parse("iris:custom_nether"));
assertEquals(Blocks.NETHERRACK.defaultBlockState(),
NativeStructureTerrainIntegrator.defaultEncaseBlock(
customDimension, BuiltinDimensionTypes.NETHER, null, 64));
assertEquals(Blocks.END_STONE.defaultBlockState(),
NativeStructureTerrainIntegrator.defaultEncaseBlock(
customDimension, BuiltinDimensionTypes.END, null, 64));
}
@Test
public void configuredEncasePaletteReplacesTheDefaultMaterial() {
StructureStart start = start(TerrainAdjustment.BURY, 64);
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
Map<BlockPos, BlockState> blocks = new HashMap<>();
IrisMaterialPalette palette = new IrisMaterialPalette().qclear().qadd("minecraft:tuff");
NativeStructureTerrainIntegrator.integrateTerrain(
world(blocks), bounds, "minecraft:stronghold", start,
new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.ENCASE)
.setEncasePalette(palette),
NativeStructurePostProcessorEncaseTest::tuffBlock);
assertEquals(Blocks.TUFF.defaultBlockState(),
state(blocks, bounds.minX(), bounds.minY(), bounds.minZ()));
}
@Test
public void omittedTerrainConfigurationRetainsSourceSemantics() {
for (TerrainAdjustment adjustment : TerrainAdjustment.values()) {
IrisStructureTerrain resolved = NativeStructureTerrainIntegrator.resolveNativeTerrain(
start(adjustment, 64), null);
assertEquals(IrisStructureTerrainMode.SOURCE, resolved.resolvedMode());
}
}
@Test
public void sourceBuryUsesTheVanillaSixByTwelveInfluenceEnvelope() {
StructureStart start = start(TerrainAdjustment.BURY, 64);
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
int groundY = bounds.minY();
BoundingBox area = new BoundingBox(
bounds.minX() - 7, groundY - 13, bounds.minZ(),
bounds.maxX(), groundY + 13, bounds.maxZ());
Map<BlockPos, BlockState> blocks = new HashMap<>();
NativeStructureTerrainIntegrator.integrateTerrain(
world(blocks), area, "test:bury", start,
new IrisStructureTerrain().setMode(IrisStructureTerrainMode.SOURCE), null);
assertEquals(Blocks.STONE.defaultBlockState(),
state(blocks, bounds.minX() - 5, groundY, bounds.minZ()));
assertEquals(Blocks.STONE.defaultBlockState(),
state(blocks, bounds.minX(), groundY + 11, bounds.minZ()));
assertEquals(Blocks.AIR.defaultBlockState(),
state(blocks, bounds.minX() - 6, groundY, bounds.minZ()));
assertEquals(Blocks.AIR.defaultBlockState(),
state(blocks, bounds.minX(), groundY + 12, bounds.minZ()));
assertEquals(Blocks.AIR.defaultBlockState(),
state(blocks, bounds.minX() - 7, groundY, bounds.minZ()));
assertEquals(Blocks.AIR.defaultBlockState(),
state(blocks, bounds.minX(), groundY + 13, bounds.minZ()));
}
@Test
public void sourceEncapsulateWrapsTheFullPieceBounds() {
StructureStart start = start(TerrainAdjustment.ENCAPSULATE, 64);
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
BoundingBox area = new BoundingBox(
bounds.minX() - 13, bounds.minY(), bounds.minZ(),
bounds.maxX(), bounds.maxY() + 13, bounds.maxZ());
Map<BlockPos, BlockState> blocks = new HashMap<>();
NativeStructureTerrainIntegrator.integrateTerrain(
world(blocks), area, "test:encapsulate", start,
new IrisStructureTerrain().setMode(IrisStructureTerrainMode.SOURCE), null);
assertEquals(Blocks.STONE.defaultBlockState(),
state(blocks, bounds.minX() - 11, bounds.minY(), bounds.minZ()));
assertEquals(Blocks.STONE.defaultBlockState(),
state(blocks, bounds.minX(), bounds.maxY() + 11, bounds.minZ()));
assertEquals(Blocks.AIR.defaultBlockState(),
state(blocks, bounds.minX() - 12, bounds.minY(), bounds.minZ()));
assertEquals(Blocks.AIR.defaultBlockState(),
state(blocks, bounds.minX(), bounds.maxY() + 12, bounds.minZ()));
assertEquals(Blocks.AIR.defaultBlockState(),
state(blocks, bounds.minX() - 13, bounds.minY(), bounds.minZ()));
assertEquals(Blocks.AIR.defaultBlockState(),
state(blocks, bounds.minX(), bounds.maxY() + 13, bounds.minZ()));
}
@Test
public void sourceTerrainDensityOnlyUsesRigidPoolPieces() {
PoolElementStructurePiece terrainMatching = poolPiece(StructureTemplatePool.Projection.TERRAIN_MATCHING);
PoolElementStructurePiece rigid = poolPiece(StructureTemplatePool.Projection.RIGID);
assertFalse(NativeStructureTerrainIntegrator.isSourceRigidPiece(terrainMatching));
assertTrue(NativeStructureTerrainIntegrator.isSourceRigidPiece(rigid));
}
@Test
public void sourceJunctionTerrainUsesTheStrictLowerTwelveBlockEnvelope() {
assertTrue(NativeStructureTerrainIntegrator.insideSourceJunctionEnvelope(0, -1, 0));
assertTrue(NativeStructureTerrainIntegrator.insideSourceJunctionEnvelope(0, -11, 0));
assertFalse(NativeStructureTerrainIntegrator.insideSourceJunctionEnvelope(0, 0, 0));
assertFalse(NativeStructureTerrainIntegrator.insideSourceJunctionEnvelope(0, -12, 0));
assertFalse(NativeStructureTerrainIntegrator.insideSourceJunctionEnvelope(12, -1, 0));
}
@Test
public void explicitTerrainConfigurationWinsOverAutoEncase() {
IrisStructureTerrain configured = new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.PRESERVE);
assertSame(configured, NativeStructureTerrainIntegrator.resolveNativeTerrain(
start(TerrainAdjustment.BURY, 64), configured));
}
@Test
public void legacyTemplateAirIsClearedAfterEveryTerrainFillPath() {
IrisStructureTerrain source = new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.SOURCE);
IrisStructureTerrain preserve = new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.PRESERVE);
IrisStructureTerrain encase = new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.ENCASE);
assertTrue(NativeStructureTerrainIntegrator.clearsLegacyTemplateAir(
start(TerrainAdjustment.BURY, 64), source));
assertTrue(NativeStructureTerrainIntegrator.clearsLegacyTemplateAir(
start(TerrainAdjustment.BEARD_BOX, 64), source));
assertTrue(NativeStructureTerrainIntegrator.clearsLegacyTemplateAir(
start(TerrainAdjustment.NONE, 64), encase));
assertFalse(NativeStructureTerrainIntegrator.clearsLegacyTemplateAir(
start(TerrainAdjustment.NONE, 64), source));
assertFalse(NativeStructureTerrainIntegrator.clearsLegacyTemplateAir(
start(TerrainAdjustment.BURY, 64), preserve));
}
@Test
public void encaseReservesTheNeighborChunkEnvelope() {
StructureStart generated = start(TerrainAdjustment.BURY, 64);
BoundingBox sourceBounds = generated.getBoundingBox();
BoundingBox content = NativeStructureReferenceEnvelope.contentBounds(generated);
IrisStructureTerrain terrain = new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.ENCASE)
.setHorizontalPadding(4);
StructureStart wrapped = NativeStructureReferenceEnvelope.wrap(
generated,
generated.getStructure(),
0,
terrain);
BoundingBox referenceBounds = NativeStructureReferenceEnvelope.referenceBounds(
wrapped, wrapped.getStructure(), terrain);
assertEquals(sourceBounds, wrapped.getBoundingBox());
assertEquals(content.minX() - 4, referenceBounds.minX());
assertEquals(content.maxX() + 4, referenceBounds.maxX());
assertEquals(content.minZ() - 4, referenceBounds.minZ());
assertEquals(content.maxZ() + 4, referenceBounds.maxZ());
assertEquals(generated.getPieces().size(), wrapped.getPieces().size());
}
@Test
public void referenceEnvelopeRejectsChunkCoordinateOverflow() {
StructureStart generated = start(TerrainAdjustment.NONE, 64);
StructureStart extreme = new StructureStart(
generated.getStructure(), new ChunkPos(Integer.MAX_VALUE, 0), 0,
new PiecesContainer(generated.getPieces()));
assertThrows(IllegalStateException.class, () ->
NativeStructureReferenceEnvelope.referenceBounds(
extreme, extreme.getStructure(), new IrisStructureTerrain()));
}
@Test
public void explicitTerrainEnvelopeDoesNotInheritSourceAdjustment() {
StructureStart generated = start(TerrainAdjustment.BURY, 64);
BoundingBox content = NativeStructureReferenceEnvelope.contentBounds(generated);
IrisStructureTerrain terrain = new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.ENCASE)
.setHorizontalPadding(112);
StructureStart wrapped = NativeStructureReferenceEnvelope.wrap(
generated, generated.getStructure(), 0, terrain);
BoundingBox referenceBounds = NativeStructureReferenceEnvelope.referenceBounds(
wrapped, wrapped.getStructure(), terrain);
assertEquals(content.minX() - 112, referenceBounds.minX());
assertEquals(content.maxX() + 112, referenceBounds.maxX());
}
@Test
public void yBandRelocatesTheStartMidpointDeterministically() {
IrisStructureYBand band = new IrisStructureYBand().setMin(-120).setMax(-20);
StructureStart first = start(TerrainAdjustment.BURY, 64);
StructureStart second = start(TerrainAdjustment.BURY, 64);
NativeStructureVerticalPlacer.applyVerticalShift(
first, -64, -256, 320, true, false, band, (x, z) -> 40);
NativeStructureVerticalPlacer.applyVerticalShift(
second, -64, -256, 320, true, false, band, (x, z) -> 40);
BoundingBox bounds = first.getBoundingBox();
assertEquals(bounds.minY(), second.getBoundingBox().minY());
assertTrue(bounds.minY() >= -120);
assertTrue(bounds.maxY() <= -20);
int repeated = NativeStructureVerticalPlacer.applyVerticalShift(
first, -64, -256, 320, true, false, band, (x, z) -> 40);
assertEquals(0, repeated);
}
@Test
public void yBandClampsWhenTheBandCannotContainTheStructure() {
IrisStructureYBand band = new IrisStructureYBand().setMin(-50).setMax(-45);
StructureStart start = start(TerrainAdjustment.BURY, 64);
NativeStructureVerticalPlacer.applyVerticalShift(
start, 0, -256, 320, true, false, band, (x, z) -> 40);
BoundingBox bounds = start.getBoundingBox();
int midpoint = bounds.minY() + (bounds.maxY() - bounds.minY()) / 2;
assertTrue(midpoint >= -50);
assertTrue(midpoint <= -45);
}
@Test
public void yBandReplacesBothTheBlindShiftAndBurial() {
IrisStructureYBand band = new IrisStructureYBand().setMin(-64).setMax(-64);
StructureStart start = start(TerrainAdjustment.BURY, 64);
BoundingBox bounds = start.getBoundingBox();
int height = bounds.maxY() - bounds.minY();
NativeStructureVerticalPlacer.applyVerticalShift(
start, -200, -256, 320, true, false, band, (x, z) -> 40);
assertEquals(-64 - height / 2, start.getBoundingBox().minY());
}
@Test
public void preserveSourceYWinsOverTheConfiguredBand() {
IrisStructureYBand band = new IrisStructureYBand().setMin(-120).setMax(-20);
StructureStart start = start(TerrainAdjustment.BURY, 64);
int minY = start.getBoundingBox().minY();
int offset = NativeStructureVerticalPlacer.applyVerticalShift(
start, 0, -256, 320, true, true, band, (x, z) -> 40);
assertEquals(0, offset);
assertEquals(minY, start.getBoundingBox().minY());
}
private static BlockState tuffBlock(IrisMaterialPalette palette, RNG rng, int x, int y, int z) {
assertNotNull(palette);
assertNotNull(rng);
return Blocks.TUFF.defaultBlockState();
}
private static StructureStart start(TerrainAdjustment adjustment, int minY) {
Structure structure = new DesertPyramidStructure(new Structure.StructureSettings(
HolderSet.empty(), Map.of(), GenerationStep.Decoration.STRONGHOLDS, adjustment));
StructurePiece piece = new DesertPyramidPiece(RandomSource.create(7L), 0, 0);
piece.move(0, minY - piece.getBoundingBox().minY(), 0);
return new StructureStart(
structure, new ChunkPos(0, 0), 0, new PiecesContainer(List.of(piece)));
}
private static PoolElementStructurePiece poolPiece(StructureTemplatePool.Projection projection) {
BlockPos position = new BlockPos(0, 64, 0);
StructurePoolElement element = StructurePoolElement.single(
"minecraft:empty",
Holder.direct(new StructureProcessorList(List.of(
BlockIgnoreProcessor.STRUCTURE_AND_AIR))),
LiquidSettings.APPLY_WATERLOGGING
).apply(projection);
return new PoolElementStructurePiece(
null,
element,
position,
0,
Rotation.NONE,
new BoundingBox(position),
LiquidSettings.APPLY_WATERLOGGING);
}
private static WorldGenLevel world(Map<BlockPos, BlockState> blocks) {
InvocationHandler handler = (proxy, method, arguments) -> {
String methodName = method.getName();
if (methodName.equals("getBlockState")) {
BlockPos position = (BlockPos) arguments[0];
return state(blocks, position.getX(), position.getY(), position.getZ());
}
if (methodName.equals("setBlock")) {
BlockPos position = (BlockPos) arguments[0];
BlockState blockState = (BlockState) arguments[1];
put(blocks, position.getX(), position.getY(), position.getZ(), blockState);
return true;
}
if (methodName.equals("getSeed")) {
return TEST_SEED;
}
if (methodName.equals("getLevel")) {
return null;
}
if (methodName.equals("hashCode")) {
return System.identityHashCode(proxy);
}
if (methodName.equals("equals")) {
return proxy == arguments[0];
}
if (methodName.equals("toString")) {
return "encase-test-world";
}
throw new UnsupportedOperationException(method.toString());
};
return (WorldGenLevel) Proxy.newProxyInstance(
WorldGenLevel.class.getClassLoader(), new Class<?>[]{WorldGenLevel.class}, handler);
}
private static NativeStructureTerrainIntegrator.SourceTerrainSnapshot sourceSnapshot(
WorldGenLevel world, BoundingBox area) {
BitSet requiredLayers = new BitSet(area.getYSpan());
requiredLayers.set(0, area.getYSpan());
return NativeStructureTerrainIntegrator.SourceTerrainSnapshot.capture(
world, area, requiredLayers);
}
private static void put(Map<BlockPos, BlockState> blocks,
int x, int y, int z, BlockState state) {
blocks.put(new BlockPos(x, y, z), state);
}
private static BlockState state(Map<BlockPos, BlockState> blocks, int x, int y, int z) {
return blocks.getOrDefault(new BlockPos(x, y, z), Blocks.AIR.defaultBlockState());
}
}
@@ -0,0 +1,202 @@
package art.arcane.iris.nativegen.v26_3_R1;
import art.arcane.iris.structure.placement.IrisStructureTerrain;
import art.arcane.iris.structure.placement.IrisStructureTerrainMode;
import net.minecraft.SharedConstants;
import net.minecraft.core.Direction;
import net.minecraft.core.HolderSet;
import net.minecraft.server.Bootstrap;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentPieces;
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentStructure;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public class NativeStructurePostProcessorMonumentTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void vanillaSeaLevelKeepsTheVanillaMonumentHeight() {
StructureStart start = monumentStart(1337L);
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:monument", 0, 63, -64, 320, false, false, null, (x, z) -> 0);
assertEquals(0, offset);
assertEquals(39, start.getBoundingBox().minY());
assertEquals(61, start.getBoundingBox().maxY());
}
@Test
public void shiftedSeaLevelMovesTheShellAndEveryRoomTogether() {
StructureStart start = monumentStart(1337L);
OceanMonumentPieces.MonumentBuilding building = monumentBuilding(start);
List<StructurePiece> children = NativeStructureVerticalPlacer.monumentChildPieces(building);
int[] childMinY = minimumYs(children);
BoundingBox cachedBounds = start.getBoundingBox();
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:monument", 0, 50, -256, 512, false, false, null, (x, z) -> 0);
assertEquals(-13, offset);
assertSame(cachedBounds, start.getBoundingBox());
assertEquals(26, building.getBoundingBox().minY());
assertEquals(48, building.getBoundingBox().maxY());
assertEquals(26, start.getBoundingBox().minY());
assertEquals(48, start.getBoundingBox().maxY());
assertTrue(children.size() > 20);
for (int i = 0; i < children.size(); i++) {
assertEquals(childMinY[i] - 13, children.get(i).getBoundingBox().minY());
}
int repeatedOffset = NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:monument", 0, 50, -256, 512, false, false, null, (x, z) -> 0);
assertEquals(0, repeatedOffset);
}
@Test
public void configuredOffsetIsRelativeToTheActualSeaLevel() {
StructureStart start = monumentStart(1337L);
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:monument", 3, 50, -256, 512, false, false, null, (x, z) -> 0);
assertEquals(-10, offset);
assertEquals(29, start.getBoundingBox().minY());
assertEquals(51, start.getBoundingBox().maxY());
}
@Test
public void referenceEnvelopeDoesNotAddMonumentPieces() {
StructureStart start = monumentStart(1337L);
StructureStart wrapped = NativeStructureReferenceEnvelope.wrap(
start,
start.getStructure(),
0,
new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.FORCE_CARVE)
.setHorizontalPadding(4));
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
wrapped, "minecraft:monument", 0, 63, -64, 320,
false, false, null, (x, z) -> 0);
assertEquals(0, offset);
assertEquals(1, wrapped.getPieces().size());
}
@Test
public void vanillaReloadRegenerationIsRealignedBeforePlacement() {
long seed = 1337L;
ChunkPos chunkPos = new ChunkPos(0, 0);
StructureStart initial = monumentStart(seed);
NativeStructureVerticalPlacer.applyVerticalPlacement(
initial, "minecraft:monument", 0, 50, -256, 512, false, false, null, (x, z) -> 0);
PiecesContainer regenerated = OceanMonumentStructure.regeneratePiecesAfterLoad(
chunkPos, seed, new PiecesContainer(initial.getPieces()));
StructureStart reloaded = new StructureStart(
monumentStructure(), chunkPos, 0, regenerated);
assertEquals(39, reloaded.getBoundingBox().minY());
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
reloaded, "minecraft:monument", 0, 50, -256, 512, false, false, null, (x, z) -> 0);
assertEquals(-13, offset);
assertEquals(26, reloaded.getBoundingBox().minY());
assertEquals(48, reloaded.getBoundingBox().maxY());
}
@Test
public void neighboringChunkPlacementRealignsReloadedMonumentOnce() throws Exception {
long seed = 1337L;
ChunkPos chunkPos = new ChunkPos(0, 0);
StructureStart initial = monumentStart(seed);
NativeStructureVerticalPlacer.applyVerticalPlacement(
initial, "minecraft:monument", 0, 50, -256, 512,
false, false, null, (x, z) -> 0);
PiecesContainer regenerated = OceanMonumentStructure.regeneratePiecesAfterLoad(
chunkPos, seed, new PiecesContainer(initial.getPieces()));
StructureStart reloaded = new StructureStart(
monumentStructure(), chunkPos, 0, regenerated);
ExecutorService executor = Executors.newFixedThreadPool(8);
CountDownLatch release = new CountDownLatch(1);
List<Future<?>> futures = new ArrayList<>();
try {
for (int task = 0; task < 16; task++) {
futures.add(executor.submit(() -> {
release.await();
NativeStructureVerticalPlacer.ensureMonumentSeaLevelAlignment(
reloaded, "minecraft:monument", 0, 50, -256, 512);
return null;
}));
}
release.countDown();
for (Future<?> future : futures) {
future.get();
}
} finally {
executor.shutdownNow();
}
assertEquals(26, reloaded.getBoundingBox().minY());
assertEquals(48, reloaded.getBoundingBox().maxY());
}
@Test
public void impossibleSeaLevelAlignmentFailsInsteadOfClippingTheMonument() {
StructureStart start = monumentStart(1337L);
try {
NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:monument", 0, -50, -64, 320, false, false, null, (x, z) -> 0);
} catch (IllegalStateException error) {
assertTrue(error.getMessage().contains("cannot align"));
return;
}
throw new AssertionError("Expected an out-of-bounds monument alignment to fail");
}
private static StructureStart monumentStart(long seed) {
ChunkPos chunkPos = new ChunkPos(0, 0);
OceanMonumentPieces.MonumentBuilding building = new OceanMonumentPieces.MonumentBuilding(
RandomSource.create(seed), -29, -29, Direction.NORTH);
return new StructureStart(
monumentStructure(), chunkPos, 0, new PiecesContainer(List.of(building)));
}
private static OceanMonumentStructure monumentStructure() {
return new OceanMonumentStructure(new Structure.StructureSettings(HolderSet.empty()));
}
private static OceanMonumentPieces.MonumentBuilding monumentBuilding(StructureStart start) {
return (OceanMonumentPieces.MonumentBuilding) start.getPieces().get(0);
}
private static int[] minimumYs(List<StructurePiece> pieces) {
int[] ys = new int[pieces.size()];
for (int i = 0; i < pieces.size(); i++) {
ys[i] = pieces.get(i).getBoundingBox().minY();
}
return ys;
}
}
@@ -0,0 +1,198 @@
package art.arcane.iris.nativegen.v26_3_R1;
import net.minecraft.SharedConstants;
import net.minecraft.core.HolderSet;
import net.minecraft.server.Bootstrap;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelAccessor;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.ScatteredFeaturePiece;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidPiece;
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidStructure;
import net.minecraft.world.level.levelgen.structure.structures.JungleTemplePiece;
import net.minecraft.world.level.levelgen.structure.structures.JungleTempleStructure;
import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece;
import net.minecraft.world.level.levelgen.structure.structures.SwampHutStructure;
import org.junit.BeforeClass;
import org.junit.Test;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public class NativeStructurePostProcessorScatteredFeatureTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void desertPyramidUsesTheLowestFootprintSurfaceAndLocksVanillaReanchor() {
TestDesertPyramidPiece piece = new TestDesertPyramidPiece(RandomSource.create(11L), 0, 0);
StructureStart start = desertStart(piece);
BoundingBox cachedBounds = start.getBoundingBox();
BoundingBox footprint = piece.getBoundingBox();
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, false, null,
(x, z) -> x == footprint.maxX() && z == footprint.maxZ() ? 64 : 92);
assertEquals(1, offset);
assertSame(cachedBounds, start.getBoundingBox());
assertEquals(65, piece.getBoundingBox().minY());
assertEquals(65, start.getBoundingBox().minY());
assertTrue(piece.attemptVanillaAlignment(-2));
assertEquals(65, piece.getBoundingBox().minY());
}
@Test
public void jungleTempleUsesAverageFootprintSurfaceAndHonorsConfiguredShift() {
TestJungleTemplePiece piece = new TestJungleTemplePiece(RandomSource.create(19L), 32, -16);
StructureStart start = jungleStart(piece);
BoundingBox footprint = piece.getBoundingBox();
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:jungle_pyramid", 4, 63, -64, 320, false, false, null,
(x, z) -> x == footprint.minX() && z == footprint.minZ() ? 88 : 70);
assertEquals(11, offset);
assertEquals(75, piece.getBoundingBox().minY());
assertTrue(piece.attemptVanillaAlignment());
assertEquals(75, piece.getBoundingBox().minY());
}
@Test
public void repeatedAlignmentIsIdempotent() {
TestDesertPyramidPiece piece = new TestDesertPyramidPiece(RandomSource.create(23L), 0, 0);
StructureStart start = desertStart(piece);
int initialOffset = NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, false, null, (x, z) -> 80);
int repeatedOffset = NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, false, null, (x, z) -> 80);
assertEquals(17, initialOffset);
assertEquals(0, repeatedOffset);
assertEquals(81, piece.getBoundingBox().minY());
}
@Test
public void scatteredAlignmentStaysInsideWorldBounds() {
TestDesertPyramidPiece piece = new TestDesertPyramidPiece(RandomSource.create(29L), 0, 0);
StructureStart start = desertStart(piece);
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, false, null, (x, z) -> 318);
assertEquals(241, offset);
assertEquals(305, piece.getBoundingBox().minY());
assertEquals(319, start.getBoundingBox().maxY());
}
@Test
public void scatteredStructuresDoNotOptIntoFullFootprintSurfaceFitting() {
TestDesertPyramidPiece desert = new TestDesertPyramidPiece(RandomSource.create(31L), 0, 0);
TestJungleTemplePiece jungle = new TestJungleTemplePiece(RandomSource.create(37L), 0, 0);
SwampHutPiece swamp = new SwampHutPiece(RandomSource.create(41L), 0, 0);
StructureStart desertStart = desertStart(desert);
StructureStart jungleStart = jungleStart(jungle);
StructureStart swampStart = swampStart(swamp);
assertFalse(NativeStructureSurfaceFitter.requiresSurfaceTerrain(desertStart));
assertFalse(NativeStructureSurfaceFitter.requiresSurfaceTerrain(jungleStart));
assertFalse(NativeStructureSurfaceFitter.requiresSurfaceTerrain(swampStart));
AtomicInteger terrainQueries = new AtomicInteger();
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
swampStart, "minecraft:swamp_hut", 0, 63, -64, 320, false, false, null,
(x, z) -> terrainQueries.incrementAndGet());
assertEquals(0, offset);
assertEquals(0, terrainQueries.get());
assertEquals(64, swamp.getBoundingBox().minY());
}
@Test
public void scatteredHeightFieldMatchesTheRuntimeContract() {
Field field = NativeStructureReflection.resolveScatteredHeightPositionField();
assertEquals(ScatteredFeaturePiece.class, field.getDeclaringClass());
assertEquals(int.class, field.getType());
assertTrue(Modifier.isProtected(field.getModifiers()));
assertFalse(Modifier.isFinal(field.getModifiers()));
assertFalse(Modifier.isStatic(field.getModifiers()));
assertTrue(field.trySetAccessible());
}
@Test
public void datapackOverridesOfSpecialVanillaIdsUseGenericPlacement() {
StructureStart pyramidOverride = swampStart(
new SwampHutPiece(RandomSource.create(43L), 0, 0));
int pyramidMinY = pyramidOverride.getBoundingBox().minY();
int pyramidOffset = NativeStructureVerticalPlacer.applyVerticalPlacement(
pyramidOverride, "minecraft:desert_pyramid", 5,
63, -64, 320, false, false, null, (x, z) -> 200);
assertEquals(5, pyramidOffset);
assertEquals(pyramidMinY + 5, pyramidOverride.getBoundingBox().minY());
StructureStart monumentOverride = swampStart(
new SwampHutPiece(RandomSource.create(47L), 0, 0));
int monumentMinY = monumentOverride.getBoundingBox().minY();
NativeStructureVerticalPlacer.ensureMonumentSeaLevelAlignment(
monumentOverride, "minecraft:monument", 0, 40, -64, 320);
assertEquals(monumentMinY, monumentOverride.getBoundingBox().minY());
}
private static StructureStart desertStart(StructurePiece piece) {
Structure structure = new DesertPyramidStructure(new Structure.StructureSettings(HolderSet.empty()));
return start(structure, piece);
}
private static StructureStart jungleStart(StructurePiece piece) {
Structure structure = new JungleTempleStructure(new Structure.StructureSettings(HolderSet.empty()));
return start(structure, piece);
}
private static StructureStart swampStart(StructurePiece piece) {
Structure structure = new SwampHutStructure(new Structure.StructureSettings(HolderSet.empty()));
return start(structure, piece);
}
private static StructureStart start(Structure structure, StructurePiece piece) {
return new StructureStart(
structure, new ChunkPos(0, 0), 0, new PiecesContainer(List.of(piece)));
}
private static final class TestDesertPyramidPiece extends DesertPyramidPiece {
private TestDesertPyramidPiece(RandomSource random, int west, int north) {
super(random, west, north);
}
private boolean attemptVanillaAlignment(int offset) {
return updateHeightPositionToLowestGroundHeight((LevelAccessor) null, offset);
}
}
private static final class TestJungleTemplePiece extends JungleTemplePiece {
private TestJungleTemplePiece(RandomSource random, int west, int north) {
super(random, west, north);
}
private boolean attemptVanillaAlignment() {
return updateAverageGroundHeight((LevelAccessor) null, null, 0);
}
}
}
@@ -0,0 +1,354 @@
package art.arcane.iris.nativegen.v26_3_R1;
import com.mojang.serialization.Codec;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderSet;
import net.minecraft.core.IdMapper;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.server.Bootstrap;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.LevelChunkSection;
import net.minecraft.world.level.chunk.PalettedContainer;
import net.minecraft.world.level.chunk.PalettedContainerFactory;
import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.chunk.Strategy;
import net.minecraft.world.level.chunk.UpgradeData;
import net.minecraft.world.level.levelgen.GenerationStep;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.TerrainAdjustment;
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidPiece;
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidStructure;
import org.junit.BeforeClass;
import org.junit.Test;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class NativeStructurePostProcessorVegetationTest {
private static final long TEST_SEED = 1234L;
private static final int MIN_Y = -64;
private static final int MAX_Y = 319;
private static final int GROUND_Y = 64;
private static final BoundingBox AREA = new BoundingBox(0, MIN_Y, 0, 15, MAX_Y, 15);
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void canopyAboveTheStructureRoofSurvivesVegetationClearing() {
ProtoChunk chunk = chunk();
StructureStart start = desertStart(0, 0, GROUND_Y + 15);
BoundingBox bounds = start.getBoundingBox();
for (int y = bounds.maxY() - 1; y <= GROUND_Y + 40; y++) {
fillLayer(chunk, y, leaves());
}
write(chunk, 4, bounds.minY() - 1, 4, log());
clearVegetation(chunk, start);
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
assertEquals(leaves(), chunk.getBlockState(new BlockPos(x, GROUND_Y + 40, z)));
assertEquals(leaves(), chunk.getBlockState(new BlockPos(x, bounds.maxY() + 1, z)));
assertEquals(air(), chunk.getBlockState(new BlockPos(x, bounds.maxY(), z)));
assertEquals(air(), chunk.getBlockState(new BlockPos(x, bounds.maxY() - 1, z)));
}
}
assertEquals(air(), chunk.getBlockState(new BlockPos(4, bounds.minY() - 1, 4)));
}
@Test
public void buriedStructuresPreserveTheForestAboveThem() {
ProtoChunk chunk = chunk();
StructureStart start = desertStart(0, 0, -20);
write(chunk, 8, GROUND_Y + 1, 8, log());
write(chunk, 8, GROUND_Y + 2, 8, log());
write(chunk, 8, GROUND_Y + 3, 8, log());
write(chunk, 8, GROUND_Y + 4, 8, leaves());
clearVegetation(chunk, start);
assertEquals(log(), chunk.getBlockState(new BlockPos(8, GROUND_Y + 1, 8)));
assertEquals(log(), chunk.getBlockState(new BlockPos(8, GROUND_Y + 2, 8)));
assertEquals(log(), chunk.getBlockState(new BlockPos(8, GROUND_Y + 3, 8)));
assertEquals(leaves(), chunk.getBlockState(new BlockPos(8, GROUND_Y + 4, 8)));
}
@Test
public void columnsOutsideThePieceFootprintKeepEveryTreeBlock() {
ProtoChunk chunk = chunk();
StructureStart start = desertStart(8, 8, GROUND_Y + 15);
BoundingBox bounds = start.getBoundingBox();
int inside = bounds.minY() + 4;
write(chunk, 0, inside, 0, log());
write(chunk, 0, GROUND_Y + 40, 0, leaves());
write(chunk, 12, inside, 12, log());
write(chunk, 12, GROUND_Y + 40, 12, leaves());
clearVegetation(chunk, start);
assertEquals(log(), chunk.getBlockState(new BlockPos(0, inside, 0)));
assertEquals(leaves(), chunk.getBlockState(new BlockPos(0, GROUND_Y + 40, 0)));
assertEquals(air(), chunk.getBlockState(new BlockPos(12, inside, 12)));
assertEquals(leaves(), chunk.getBlockState(new BlockPos(12, GROUND_Y + 40, 12)));
}
@Test
public void clearingStopsAtThePieceEnvelopeBoundaries() {
ProtoChunk chunk = chunk();
StructureStart start = desertStart(0, 0, GROUND_Y + 15);
BoundingBox bounds = start.getBoundingBox();
write(chunk, 4, bounds.maxY() + 1, 4, log());
write(chunk, 4, bounds.maxY(), 4, log());
write(chunk, 4, bounds.minY() - 1, 4, log());
write(chunk, 4, bounds.minY() - 2, 4, log());
clearVegetation(chunk, start);
assertEquals(log(), chunk.getBlockState(new BlockPos(4, bounds.maxY() + 1, 4)));
assertEquals(air(), chunk.getBlockState(new BlockPos(4, bounds.maxY(), 4)));
assertEquals(air(), chunk.getBlockState(new BlockPos(4, bounds.minY() - 1, 4)));
assertEquals(log(), chunk.getBlockState(new BlockPos(4, bounds.minY() - 2, 4)));
}
@Test
public void overlappingPieceEnvelopesMergeIntoOneClearedRange() {
ProtoChunk chunk = chunk();
StructureStart low = desertStart(0, 0, GROUND_Y);
StructureStart high = desertStart(0, 0, GROUND_Y + 40);
int between = GROUND_Y + 20;
write(chunk, 4, between, 4, log());
write(chunk, 4, high.getBoundingBox().maxY() + 1, 4, log());
write(chunk, 4, low.getBoundingBox().minY() - 2, 4, log());
NativeStructureVegetationClearer.clearIntersectingVegetation(
world(chunk), chunk, AREA, List.of(low, high));
assertEquals(air(), chunk.getBlockState(new BlockPos(4, between, 4)));
assertEquals(log(), chunk.getBlockState(
new BlockPos(4, high.getBoundingBox().maxY() + 1, 4)));
assertEquals(log(), chunk.getBlockState(
new BlockPos(4, low.getBoundingBox().minY() - 2, 4)));
}
@Test
public void validStartsReachIntersectionProcessing() {
assertTrue(NativeStructureVegetationClearer.shouldProcessStart(
desertStart(0, 0, GROUND_Y + 15)));
assertFalse(NativeStructureVegetationClearer.shouldProcessStart(
StructureStart.INVALID_START));
}
@Test
public void allUndergroundGenerationStepsShareOneClassification() {
assertTrue(NativeStructureVegetationClearer.isUndergroundStep(
GenerationStep.Decoration.UNDERGROUND_STRUCTURES));
assertTrue(NativeStructureVegetationClearer.isUndergroundStep(
GenerationStep.Decoration.UNDERGROUND_DECORATION));
assertTrue(NativeStructureVegetationClearer.isUndergroundStep(
GenerationStep.Decoration.STRONGHOLDS));
assertFalse(NativeStructureVegetationClearer.isUndergroundStep(
GenerationStep.Decoration.SURFACE_STRUCTURES));
}
@Test
public void undergroundStructuresUseTheLowestTerrainColumn() {
BoundingBox bounds = new BoundingBox(0, 60, 0, 1, 80, 0);
int offset = NativeStructureVerticalPlacer.resolveBuriedOffset(
bounds, 0, -64, 320, (x, z) -> x == 1 ? 76 : 100);
assertEquals(-5, offset);
}
@Test
public void undergroundBurialClampsToTheWorldFloorInsteadOfFailing() {
BoundingBox bounds = new BoundingBox(0, 60, 0, 1, 80, 0);
assertEquals(-2, NativeStructureVerticalPlacer.resolveBuriedOffset(
bounds, 0, 58, 320, (x, z) -> x == 1 ? 76 : 100));
}
@Test
public void loweringSurfaceRemovesCactusStacksAndTheirFlowers() {
ProtoChunk chunk = chunk();
for (int y = 80; y <= 102; y++) {
write(chunk, 4, y, 4, Blocks.SAND.defaultBlockState());
}
for (int y = 103; y <= 105; y++) {
write(chunk, 4, y, 4, Blocks.CACTUS.defaultBlockState());
}
write(chunk, 4, 106, 4, Blocks.CACTUS_FLOWER.defaultBlockState());
NativeStructureSurfaceFitter.applySurfaceColumn(
world(chunk), new BlockPos.MutableBlockPos(), 4, 4, 102, 93, MIN_Y, MAX_Y);
assertEquals(Blocks.SAND.defaultBlockState(), chunk.getBlockState(new BlockPos(4, 93, 4)));
for (int y = 94; y <= 106; y++) {
assertTrue("Unsupported decoration at " + y, chunk.getBlockState(new BlockPos(4, y, 4)).isAir());
}
}
@Test
public void loweringSurfacePreservesCactusOnUnchangedColumns() {
ProtoChunk chunk = chunk();
for (int x : List.of(4, 5)) {
for (int y = 60; y <= 64; y++) {
write(chunk, x, y, 4, Blocks.SAND.defaultBlockState());
}
write(chunk, x, 65, 4, Blocks.CACTUS.defaultBlockState());
write(chunk, x, 66, 4, Blocks.CACTUS_FLOWER.defaultBlockState());
}
WorldGenLevel world = world(chunk);
NativeStructureSurfaceFitter.applySurfaceColumn(
world, new BlockPos.MutableBlockPos(), 4, 4, 64, 60, MIN_Y, MAX_Y);
NativeStructureSurfaceFitter.applySurfaceColumn(
world, new BlockPos.MutableBlockPos(), 5, 4, 64, 64, MIN_Y, MAX_Y);
assertTrue(chunk.getBlockState(new BlockPos(4, 65, 4)).isAir());
assertTrue(chunk.getBlockState(new BlockPos(4, 66, 4)).isAir());
assertEquals(Blocks.SAND.defaultBlockState(), chunk.getBlockState(new BlockPos(5, 64, 4)));
assertEquals(Blocks.CACTUS.defaultBlockState(), chunk.getBlockState(new BlockPos(5, 65, 4)));
assertEquals(Blocks.CACTUS_FLOWER.defaultBlockState(), chunk.getBlockState(new BlockPos(5, 66, 4)));
}
@Test
public void loweringSurfaceKeepsTreeAndSolidObjectBoundaries() {
for (BlockState boundary : List.of(Blocks.OAK_LOG.defaultBlockState(), Blocks.OAK_PLANKS.defaultBlockState())) {
ProtoChunk chunk = chunk();
write(chunk, 4, 63, 4, Blocks.SAND.defaultBlockState());
write(chunk, 4, 64, 4, Blocks.SAND.defaultBlockState());
write(chunk, 4, 65, 4, boundary);
write(chunk, 4, 66, 4, Blocks.CACTUS.defaultBlockState());
write(chunk, 4, 67, 4, Blocks.CACTUS_FLOWER.defaultBlockState());
NativeStructureSurfaceFitter.applySurfaceColumn(
world(chunk), new BlockPos.MutableBlockPos(), 4, 4, 64, 60, MIN_Y, MAX_Y);
assertEquals(boundary, chunk.getBlockState(new BlockPos(4, 65, 4)));
assertEquals(Blocks.CACTUS.defaultBlockState(), chunk.getBlockState(new BlockPos(4, 66, 4)));
assertEquals(Blocks.CACTUS_FLOWER.defaultBlockState(), chunk.getBlockState(new BlockPos(4, 67, 4)));
}
}
private static void clearVegetation(ProtoChunk chunk, StructureStart start) {
NativeStructureVegetationClearer.clearIntersectingVegetation(
world(chunk), chunk, AREA, List.of(start));
}
private static StructureStart desertStart(int originX, int originZ, int topY) {
Structure structure = new DesertPyramidStructure(
new Structure.StructureSettings(
HolderSet.empty(), Map.of(),
GenerationStep.Decoration.SURFACE_STRUCTURES, TerrainAdjustment.NONE));
DesertPyramidPiece piece = new DesertPyramidPiece(RandomSource.create(7L), originX, originZ);
piece.move(0, topY - piece.getBoundingBox().maxY(), 0);
return new StructureStart(
structure, new ChunkPos(0, 0), 0, new PiecesContainer(List.of(piece)));
}
private static ProtoChunk chunk() {
return new ProtoChunk(
new ChunkPos(0, 0), UpgradeData.EMPTY,
LevelHeightAccessor.create(MIN_Y, MAX_Y - MIN_Y + 1), containerFactory(), null);
}
private static void fillLayer(ProtoChunk chunk, int y, BlockState state) {
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
write(chunk, x, y, z, state);
}
}
}
private static void write(ProtoChunk chunk, int x, int y, int z, BlockState state) {
LevelChunkSection section = chunk.getSection(chunk.getSectionIndex(y));
section.setBlockState(x, y & 15, z, state, false);
}
private static BlockState log() {
return Blocks.OAK_LOG.defaultBlockState();
}
private static BlockState leaves() {
return Blocks.OAK_LEAVES.defaultBlockState();
}
private static BlockState air() {
return Blocks.AIR.defaultBlockState();
}
private static WorldGenLevel world(ChunkAccess chunk) {
InvocationHandler handler = (proxy, method, arguments) -> {
String methodName = method.getName();
if (methodName.equals("getBlockState")) {
return chunk.getBlockState((BlockPos) arguments[0]);
}
if (methodName.equals("setBlock")) {
return chunk.setBlockState(
((BlockPos) arguments[0]).immutable(),
(BlockState) arguments[1], (int) arguments[2]) != null;
}
if (methodName.equals("getSeed")) {
return TEST_SEED;
}
if (methodName.equals("getLevel")) {
return null;
}
if (methodName.equals("holderLookup")) {
return BuiltInRegistries.BLOCK;
}
if (methodName.equals("hashCode")) {
return System.identityHashCode(proxy);
}
if (methodName.equals("equals")) {
return proxy == arguments[0];
}
if (methodName.equals("toString")) {
return "vegetation-test-world";
}
throw new UnsupportedOperationException(method.toString());
};
return (WorldGenLevel) Proxy.newProxyInstance(
WorldGenLevel.class.getClassLoader(), new Class<?>[]{WorldGenLevel.class}, handler);
}
private static PalettedContainerFactory containerFactory() {
Strategy<BlockState> blockStates = Strategy.createForBlockStates(Block.BLOCK_STATE_REGISTRY);
BlockState air = Blocks.AIR.defaultBlockState();
IdMapper<Holder<Biome>> biomeIds = new IdMapper<>();
Holder<Biome> defaultBiome = Holder.direct((Biome) null);
biomeIds.add(defaultBiome);
Strategy<Holder<Biome>> biomes = Strategy.createForBiomes(biomeIds);
Codec<Holder<Biome>> biomeCodec = Codec.STRING.xmap(
name -> defaultBiome, holder -> "iris-test-biome");
return new PalettedContainerFactory(
blockStates,
air,
PalettedContainer.codecRW(BlockState.CODEC, blockStates, air),
biomes,
defaultBiome,
PalettedContainer.codecRO(biomeCodec, biomes, defaultBiome),
PalettedContainer.codecRW(biomeCodec, biomes, defaultBiome));
}
}
@@ -0,0 +1,236 @@
package art.arcane.iris.nativegen.v26_3_R1;
import com.mojang.datafixers.util.Either;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderSet;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.server.Bootstrap;
import net.minecraft.tags.TagKey;
import net.minecraft.util.valueproviders.ConstantInt;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
import net.minecraft.world.level.levelgen.structure.templatesystem.BlackstoneReplaceProcessor;
import net.minecraft.world.level.levelgen.structure.templatesystem.BlockAgeProcessor;
import net.minecraft.world.level.levelgen.structure.templatesystem.BlockIgnoreProcessor;
import net.minecraft.world.level.levelgen.structure.templatesystem.CappedProcessor;
import net.minecraft.world.level.levelgen.structure.templatesystem.GravityProcessor;
import net.minecraft.world.level.levelgen.structure.templatesystem.JigsawReplacementProcessor;
import net.minecraft.world.level.levelgen.structure.templatesystem.LavaSubmergedBlockProcessor;
import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings;
import net.minecraft.world.level.levelgen.structure.templatesystem.NopProcessor;
import net.minecraft.world.level.levelgen.structure.templatesystem.ProtectedBlockProcessor;
import net.minecraft.world.level.levelgen.structure.templatesystem.RuleProcessor;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessor;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessorList;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import org.junit.Assume;
import org.junit.BeforeClass;
import org.junit.Test;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class NativeStructureTemplateOccupancyTest {
private static final BoundingBox PROCESSING_AREA = new BoundingBox(0, 56, 0, 15, 80, 0);
private static final int TEMPLATE_WIDTH = 32;
private static final int TERRAIN_HEIGHT = 64;
@BeforeClass
public static void bootstrapMinecraft() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void straddlingGravityPieceHeightReadsHonorTheVanillaClipContract() throws Exception {
ColumnRecorder recorder = new ColumnRecorder();
resolve(straddlingPiece(
List.of(new GravityProcessor(Heightmap.Types.WORLD_SURFACE_WG, 0))), recorder);
// 26.2 vanilla clips processor evaluation to the placement bounding box; 26.1.2 vanilla
// runs processors across the whole piece. Iris mirrors the pinned version's semantics.
assertEquals(expectedClippedColumns(), recorder.heightColumns);
}
@Test
public void cappedStraddlingPieceRunsProcessorsAcrossTheWholePiece() throws Exception {
ColumnRecorder recorder = new ColumnRecorder();
resolve(straddlingPiece(List.of(
new GravityProcessor(Heightmap.Types.WORLD_SURFACE_WG, 0),
new CappedProcessor(NopProcessor.INSTANCE, ConstantInt.of(0)))), recorder);
assertEquals(columns(0, TEMPLATE_WIDTH - 1), recorder.heightColumns);
}
@Test
public void cappedProcessorIsTheOnlyProcessorThatDisablesTheProcessingAreaClip() throws Exception {
Method contract = clipContractMethod();
Assume.assumeTrue("evaluatesEntirePieceState only exists on 26.2+", contract != null);
for (StructureProcessor processor : List.of(
BlockIgnoreProcessor.STRUCTURE_BLOCK,
JigsawReplacementProcessor.INSTANCE,
LavaSubmergedBlockProcessor.INSTANCE,
NopProcessor.INSTANCE,
BlackstoneReplaceProcessor.INSTANCE,
new GravityProcessor(Heightmap.Types.WORLD_SURFACE_WG, 0),
new RuleProcessor(List.of()),
new BlockAgeProcessor(0.5F),
protectedBlockProcessor())) {
assertFalse(processor.getClass().getName(),
(Boolean) contract.invoke(processor));
}
assertTrue((Boolean) contract.invoke(
new CappedProcessor(NopProcessor.INSTANCE, ConstantInt.of(4))));
}
@Test
public void placementSettingsCarryTheProcessingAreaAsTheirClipBox() throws Exception {
PoolElementStructurePiece piece = straddlingPiece(List.of());
StructurePlaceSettings settings = NativeStructureReflection.resolvePlacementSettings(
(SinglePoolElement) piece.getElement(), piece, PROCESSING_AREA);
assertEquals(PROCESSING_AREA, settings.getBoundingBox());
}
private static void resolve(PoolElementStructurePiece piece, ColumnRecorder recorder) {
NativeStructureTemplateOccupancy.resolve(
world(recorder), piece, piece.getPosition(), PROCESSING_AREA,
NativeStructureTemplateOccupancyTest::forbiddenTemplateManager,
PROCESSING_AREA::isInside, amount -> {
});
}
private static PoolElementStructurePiece straddlingPiece(
List<StructureProcessor> processors) throws Exception {
BoundingBox bounds = new BoundingBox(0, 70, 0, TEMPLATE_WIDTH - 1, 76, 0);
return new PoolElementStructurePiece(
null, new InlineSinglePoolElement(template(), processors),
new BlockPos(bounds.minX(), bounds.minY(), bounds.minZ()),
1, Rotation.NONE, bounds, LiquidSettings.APPLY_WATERLOGGING);
}
private static Set<Integer> columns(int minimumX, int maximumX) {
Set<Integer> columns = new TreeSet<>();
for (int x = minimumX; x <= maximumX; x++) {
columns.add(x);
}
return columns;
}
private static Method clipContractMethod() {
try {
return StructureProcessor.class.getMethod("evaluatesEntirePieceState");
} catch (NoSuchMethodException absent) {
return null;
}
}
private static Set<Integer> expectedClippedColumns() {
return clipContractMethod() != null
? columns(PROCESSING_AREA.minX(), PROCESSING_AREA.maxX())
: columns(0, TEMPLATE_WIDTH - 1);
}
private static StructureProcessor protectedBlockProcessor() throws Exception {
// Constructed reflectively: the constructor takes HolderSet on 26.2 and TagKey on 26.1.2.
Constructor<?> constructor = ProtectedBlockProcessor.class.getConstructors()[0];
Class<?> parameter = constructor.getParameterTypes()[0];
Object argument = parameter == HolderSet.class
? HolderSet.empty()
: TagKey.create(Registries.BLOCK, Identifier.fromNamespaceAndPath("minecraft", "air"));
return (StructureProcessor) constructor.newInstance(argument);
}
private static StructureTemplateManager forbiddenTemplateManager() {
throw new AssertionError("Inline templates must not resolve a template manager");
}
private static StructureTemplate template() throws Exception {
List<StructureTemplate.StructureBlockInfo> blocks = new ArrayList<>();
for (int x = 0; x < TEMPLATE_WIDTH; x++) {
blocks.add(new StructureTemplate.StructureBlockInfo(
new BlockPos(x, 0, 0), Blocks.COBBLESTONE.defaultBlockState(), null));
}
Constructor<StructureTemplate.Palette> constructor =
StructureTemplate.Palette.class.getDeclaredConstructor(List.class);
assertTrue(constructor.trySetAccessible());
StructureTemplate template = new StructureTemplate();
template.palettes.add(constructor.newInstance(blocks));
return template;
}
private static WorldGenLevel world(ColumnRecorder recorder) {
InvocationHandler handler = (proxy, method, arguments) -> {
String methodName = method.getName();
if (methodName.equals("getHeight") && arguments.length == 3) {
recorder.heightColumns.add((Integer) arguments[1]);
return TERRAIN_HEIGHT;
}
if (methodName.equals("getBlockState")) {
recorder.stateColumns.add(((BlockPos) arguments[0]).getX());
return Blocks.STONE.defaultBlockState();
}
if (methodName.equals("getLevel")) {
return null;
}
if (methodName.equals("holderLookup")) {
return BuiltInRegistries.BLOCK;
}
if (methodName.equals("hashCode")) {
return System.identityHashCode(proxy);
}
if (methodName.equals("equals")) {
return proxy == arguments[0];
}
if (methodName.equals("toString")) {
return "occupancy-test-world";
}
throw new UnsupportedOperationException(method.toString());
};
return (WorldGenLevel) Proxy.newProxyInstance(
WorldGenLevel.class.getClassLoader(),
new Class<?>[]{WorldGenLevel.class}, handler);
}
private static final class ColumnRecorder {
private final Set<Integer> heightColumns = new TreeSet<>();
private final Set<Integer> stateColumns = new TreeSet<>();
}
private static final class InlineSinglePoolElement extends SinglePoolElement {
private InlineSinglePoolElement(
StructureTemplate template, List<StructureProcessor> processors) {
super(Either.right(template),
Holder.direct(new StructureProcessorList(processors)),
StructureTemplatePool.Projection.RIGID,
Optional.<LiquidSettings>empty());
}
}
}
@@ -0,0 +1,561 @@
package art.arcane.iris.nativegen.v26_3_R1;
import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.structure.nativegen.NativeStructureVolume;
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
import art.arcane.volmlib.util.collection.KList;
import net.minecraft.SharedConstants;
import net.minecraft.core.HolderSet;
import net.minecraft.server.Bootstrap;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece;
import net.minecraft.world.level.levelgen.structure.structures.SwampHutStructure;
import org.junit.BeforeClass;
import org.junit.Test;
import java.lang.reflect.Proxy;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.LockSupport;
import java.util.function.IntConsumer;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class NativeStructureVolumeIndexTest {
private static final String STRUCTURE_KEY = "minecraft:swamp_hut";
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void assembledPiecesBecomeWorldSpacePieceVolumes() {
StructureStart start = swampHut(0, 0, 4, 6);
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
KList<NativeStructureVolume> volumes = NativeStructureVolumeIndex.appendPieces(null, STRUCTURE_KEY, start);
assertEquals(1, volumes.size());
NativeStructureVolume volume = volumes.getFirst();
assertEquals(STRUCTURE_KEY, volume.structure());
assertEquals(bounds.minX(), volume.minX());
assertEquals(bounds.minY(), volume.minY());
assertEquals(bounds.minZ(), volume.minZ());
assertEquals(bounds.maxX(), volume.maxX());
assertEquals(bounds.maxY(), volume.maxY());
assertEquals(bounds.maxZ(), volume.maxZ());
}
@Test
public void invalidStartsContributeNoVolumes() {
assertNull(NativeStructureVolumeIndex.appendPieces(null, STRUCTURE_KEY, StructureStart.INVALID_START));
assertNull(NativeStructureVolumeIndex.appendPieces(null, STRUCTURE_KEY, null));
}
@Test
public void coldAndCachedQueriesResolveIdenticalVolumes() {
CountingResolver resolver = new CountingResolver(0, 0);
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver);
KList<NativeStructureVolume> cold = index.resolve(null, 0, 0, 15, 15);
int coldResolutions = resolver.resolutions();
KList<NativeStructureVolume> cached = index.resolve(null, 0, 0, 15, 15);
assertTrue(coldResolutions > 0);
assertFalse(cold.isEmpty());
assertEquals(cold, cached);
assertEquals(coldResolutions, resolver.resolutions());
}
@Test
public void runtimeBucketsStayIsolatedAndRetirementEvictsOnlyTheirEntries() {
AtomicInteger runtimeId = new AtomicInteger(1);
Engine engine = runtimeEngine(runtimeId);
CountingResolver resolver = new CountingResolver(0, 0);
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver);
index.resolve(engine, 0, 0, 15, 15);
assertEquals(289, resolver.resolutions());
index.resolve(engine, 0, 0, 15, 15);
assertEquals(289, resolver.resolutions());
runtimeId.set(2);
index.resolve(engine, 0, 0, 15, 15);
assertEquals(578, resolver.resolutions());
index.resolve(engine, 0, 0, 15, 15);
assertEquals(578, resolver.resolutions());
index.evictRuntime(1);
runtimeId.set(1);
index.resolve(engine, 0, 0, 15, 15);
assertEquals(867, resolver.resolutions());
}
@Test
public void pinnedRuntimeReusesCachedOriginsWithoutOpeningCoordinateScopes() throws Exception {
ScopedEngine scoped = scopedEngine(true, 1);
CountingResolver resolver = new CountingResolver(0, 0);
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver);
KList<NativeStructureVolume> first = index.resolve(scoped.engine(), 0, 0, 15, 15);
assertEquals(289, scoped.scopes().get());
index.resolve(scoped.engine(), 16, 0, 31, 15);
assertEquals(306, resolver.resolutions());
assertEquals(306, scoped.scopes().get());
assertEquals(first, index.resolve(scoped.engine(), 0, 0, 15, 15));
assertEquals(306, scoped.scopes().get());
}
@Test
public void absentOrMismatchedRuntimeOwnershipKeepsCoordinateRouting() throws Exception {
ScopedEngine detached = scopedEngine(true, 1);
when(detached.engine().getGenerationHistoryRuntimeRouter()).thenReturn(Optional.empty());
for (ScopedEngine scoped : List.of(scopedEngine(false, 1), scopedEngine(true, 0),
scopedEngine(true, 2), detached)) {
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(new CountingResolver(0, 0));
index.resolve(scoped.engine(), 0, 0, 15, 15);
index.resolve(scoped.engine(), 16, 0, 31, 15);
assertEquals(578, scoped.scopes().get());
}
}
@Test
public void retiredPinnedOriginsRebuildThroughCoordinateScopes() throws Exception {
ScopedEngine scoped = scopedEngine(true, 1);
CountingResolver resolver = new CountingResolver(0, 0);
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver);
KList<NativeStructureVolume> first = index.resolve(scoped.engine(), 0, 0, 15, 15);
index.evictRuntime(1);
assertEquals(first, index.resolve(scoped.engine(), 0, 0, 15, 15));
assertEquals(578, resolver.resolutions());
assertEquals(578, scoped.scopes().get());
}
@Test
public void invalidatedIndexReceivesRuntimeRetirementAndDetachesOnUninstall() throws Exception {
IrisEngine engine = mock(IrisEngine.class);
Set<IntConsumer> listeners = new CopyOnWriteArraySet<>();
doAnswer(invocation -> {
listeners.add(invocation.getArgument(0));
return null;
}).when(engine).addGenerationRuntimeRetirementListener(any());
doAnswer(invocation -> {
listeners.remove(invocation.getArgument(0));
return null;
}).when(engine).removeGenerationRuntimeRetirementListener(any());
NativeStructureVolumeIndex.Context context = new NativeStructureVolumeIndex.Context(
null, null, null, null, () -> null, () -> null, () -> null);
NativeStructureVolumeIndex.install(engine, context);
try {
assertEquals(1, listeners.size());
IntConsumer original = listeners.iterator().next();
NativeStructureVolumeIndex.invalidate(engine);
assertEquals(1, listeners.size());
assertFalse(listeners.contains(original));
NativeStructureVolumeIndex.volumes(engine, 0, 0, 15, 15);
NativeStructureVolumeIndex current = installedIndexes().get(engine);
assertEquals(289, cache(current, "originCache").size());
assertEquals(1, cache(current, "queryCache").size());
for (IntConsumer listener : listeners) {
listener.accept(0);
}
assertTrue(cache(current, "originCache").isEmpty());
assertTrue(cache(current, "queryCache").isEmpty());
} finally {
NativeStructureVolumeIndex.uninstall(engine);
}
assertTrue(listeners.isEmpty());
}
@Test
public void retiringOriginBuildCannotRepublishWhileQueryEvictionWaits() throws Exception {
BlockingResolver resolver = new BlockingResolver();
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver);
ExecutorService executor = Executors.newFixedThreadPool(2);
Map<?, ?> origins = cache(index, "originCache");
Map<?, ?> queries = cache(index, "queryCache");
try {
Future<KList<NativeStructureVolume>> builder = executor.submit(() -> index.originVolumes(null, 0, 0));
assertTrue(resolver.awaitFirstEntry());
index.originVolumes(null, 1, 0);
assertEquals(1, origins.size());
Future<?> retirement;
synchronized (queries) {
retirement = executor.submit(() -> index.evictRuntime(0));
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
boolean cleared = false;
while (System.nanoTime() < deadline) {
synchronized (origins) {
cleared = origins.isEmpty();
}
if (cleared) {
break;
}
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(10L));
}
assertTrue("Origin eviction did not complete before the query cache lock", cleared);
resolver.release();
assertTrue(builder.get(5, TimeUnit.SECONDS).isEmpty());
synchronized (origins) {
assertTrue("The retired in-flight origin was cached again", origins.isEmpty());
}
}
retirement.get(5, TimeUnit.SECONDS);
index.originVolumes(null, 0, 0);
assertEquals(3, resolver.resolutions());
} finally {
resolver.release();
executor.shutdownNow();
assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS));
}
}
@Test
public void independentIndexesResolveIdenticalVolumesFromTheSameSeed() {
NativeStructureVolumeIndex first = NativeStructureVolumeIndex.forTesting(new CountingResolver(0, 0));
NativeStructureVolumeIndex second = NativeStructureVolumeIndex.forTesting(new CountingResolver(0, 0));
assertEquals(first.resolve(null, 0, 0, 15, 15), second.resolve(null, 0, 0, 15, 15));
assertEquals(first.resolve(null, -32, -32, 47, 47), second.resolve(null, -32, -32, 47, 47));
}
@Test
public void volumesOutsideTheQueryRectAreExcluded() {
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(new CountingResolver(0, 0));
BoundingBox bounds = swampHut(0, 0, 4, 6).getPieces().getFirst().getBoundingBox();
KList<NativeStructureVolume> hit = index.resolve(null, bounds.minX(), bounds.minZ(), bounds.maxX(), bounds.maxZ());
KList<NativeStructureVolume> miss = index.resolve(null,
bounds.maxX() + 1, bounds.minZ(), bounds.maxX() + 8, bounds.maxZ());
assertEquals(1, hit.size());
assertTrue(miss.isEmpty());
}
@Test
public void startsWithinTheOriginReachContributeAndBeyondItDoNot() {
int reach = NativeStructureVolumeIndex.originReachChunks();
NativeStructureVolumeIndex reachable = NativeStructureVolumeIndex.forTesting(new CountingResolver(reach, 0));
NativeStructureVolumeIndex unreachable = NativeStructureVolumeIndex.forTesting(new CountingResolver(reach + 1, 0));
assertFalse(reachable.resolve(null, 0, 0, 15, 15).isEmpty());
assertTrue(unreachable.resolve(null, 0, 0, 15, 15).isEmpty());
}
@Test
public void overlappingColdQueriesCoordinateBeforeResolvingOrigins() throws Exception {
BlockingResolver resolver = new BlockingResolver();
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver);
ExecutorService executor = Executors.newFixedThreadPool(2);
try {
Future<KList<NativeStructureVolume>> first = executor.submit(() -> index.resolve(null, 0, 0, 15, 15));
assertTrue(resolver.awaitFirstEntry());
Future<KList<NativeStructureVolume>> adjacent = executor.submit(() -> index.resolve(null, 16, 0, 31, 15));
assertFalse(resolver.awaitSecondEntry(250));
resolver.release();
assertTrue(first.get(5, TimeUnit.SECONDS).isEmpty());
assertTrue(adjacent.get(5, TimeUnit.SECONDS).isEmpty());
assertEquals(306, resolver.resolutions());
} finally {
resolver.release();
executor.shutdownNow();
}
}
@Test
public void contendedNativeWindowsAllowQueuedTerrainWorkToRun() throws Exception {
assertQueuedTerrainCanComplete(false);
}
@Test
public void interruptedNativeWindowWaitStillAcquiresAndReleasesOnce() throws Exception {
assertQueuedTerrainCanComplete(true);
}
private static void assertQueuedTerrainCanComplete(boolean interruptWaiter) throws Exception {
CountDownLatch ownerEntered = new CountDownLatch(1);
CompletableFuture<Void> terrain = new CompletableFuture<>();
AtomicBoolean firstOrigin = new AtomicBoolean(true);
AtomicBoolean interruptedAfterQuery = new AtomicBoolean();
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting((engine, chunkX, chunkZ) -> {
if (firstOrigin.compareAndSet(true, false)) {
ownerEntered.countDown();
terrain.join();
}
return new KList<>(new NativeStructureVolume(chunkX + ":" + chunkZ,
-256, 0, -256, 256, 100, 256));
});
ExecutorService owner = Executors.newSingleThreadExecutor();
ForkJoinPool workers = new ForkJoinPool(2);
AtomicReference<Thread> firstWaiter = new AtomicReference<>();
AtomicReference<Thread> secondWaiter = new AtomicReference<>();
try {
Future<KList<NativeStructureVolume>> original = owner.submit(
() -> index.resolve(null, 0, 0, 15, 15));
assertTrue(ownerEntered.await(5, TimeUnit.SECONDS));
Future<KList<NativeStructureVolume>> adjacent = workers.submit(() -> {
firstWaiter.set(Thread.currentThread());
KList<NativeStructureVolume> result = index.resolve(null, 16, 0, 31, 15);
interruptedAfterQuery.set(Thread.currentThread().isInterrupted());
return result;
});
Future<KList<NativeStructureVolume>> next = workers.submit(() -> {
secondWaiter.set(Thread.currentThread());
return index.resolve(null, 32, 0, 47, 15);
});
awaitOriginWindowWaiter(firstWaiter);
awaitOriginWindowWaiter(secondWaiter);
if (interruptWaiter) {
firstWaiter.get().interrupt();
}
Future<?> terrainTask = workers.submit(() -> terrain.complete(null));
terrainTask.get(3, TimeUnit.SECONDS);
assertEquals(289, original.get(3, TimeUnit.SECONDS).size());
KList<NativeStructureVolume> adjacentVolumes = adjacent.get(3, TimeUnit.SECONDS);
assertEquals(289, adjacentVolumes.size());
assertEquals(adjacentVolumes, index.resolve(null, 16, 0, 31, 15));
assertEquals(289, next.get(3, TimeUnit.SECONDS).size());
assertEquals(interruptWaiter, interruptedAfterQuery.get());
} finally {
terrain.complete(null);
owner.shutdownNow();
workers.shutdownNow();
assertTrue(owner.awaitTermination(5, TimeUnit.SECONDS));
assertTrue(workers.awaitTermination(5, TimeUnit.SECONDS));
}
}
private static void awaitOriginWindowWaiter(AtomicReference<Thread> reference) {
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
while (System.nanoTime() < deadline) {
Thread thread = reference.get();
if (thread != null && thread.getState() == Thread.State.WAITING) {
for (StackTraceElement frame : thread.getStackTrace()) {
if (frame.getClassName().equals(NativeStructureVolumeIndex.class.getName())
&& frame.getMethodName().equals("lockOriginWindow")) {
return;
}
}
}
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1));
}
throw new AssertionError("Native query did not wait for the occupied origin window");
}
@Test
public void distantColdQueriesCanResolveOriginsConcurrently() throws Exception {
BlockingResolver resolver = new BlockingResolver();
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver);
int distantChunkX = findDisjointWindow(0, 0);
ExecutorService executor = Executors.newFixedThreadPool(2);
try {
Future<KList<NativeStructureVolume>> first = executor.submit(() -> index.resolve(null, 0, 0, 15, 15));
assertTrue(resolver.awaitFirstEntry());
Future<KList<NativeStructureVolume>> distant = executor.submit(
() -> index.resolve(null, distantChunkX << 4, 0, (distantChunkX << 4) + 15, 15));
assertTrue(resolver.awaitSecondEntry(5_000));
resolver.release();
assertTrue(first.get(5, TimeUnit.SECONDS).isEmpty());
assertTrue(distant.get(5, TimeUnit.SECONDS).isEmpty());
} finally {
resolver.release();
executor.shutdownNow();
}
}
@Test
public void coordinatedResolutionPreservesCanonicalOriginOrder() {
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(new OrderedResolver());
KList<NativeStructureVolume> volumes = index.resolve(null, 0, 0, 15, 15);
assertEquals(289, volumes.size());
assertEquals("-8:-8", volumes.getFirst().structure());
assertEquals("0:0", volumes.get(144).structure());
assertEquals("8:8", volumes.getLast().structure());
}
private static ScopedEngine scopedEngine(boolean scoped, int ownerRuntimeId) throws Exception {
IrisEngine engine = mock(IrisEngine.class);
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
when(engine.hasGenerationRuntimeScope()).thenReturn(scoped);
when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
when(engine.getCacheID()).thenReturn(1);
if (ownerRuntimeId != 0) {
IrisEngine.GenerationRuntimeBinding binding = mock(IrisEngine.GenerationRuntimeBinding.class);
when(binding.runtimeId()).thenReturn(ownerRuntimeId);
when(router.currentRuntimeOwnership()).thenReturn(Optional.of(
new GenerationHistoryRuntimeRouter.RuntimeOwnership(1L, binding)));
}
AtomicInteger scopes = new AtomicInteger();
when(engine.openGenerationHistoryCoordinateScope(anyInt(), anyInt())).thenAnswer(invocation -> {
scopes.incrementAndGet();
return null;
});
return new ScopedEngine(engine, scopes);
}
@SuppressWarnings("unchecked")
private static Map<Engine, NativeStructureVolumeIndex> installedIndexes() throws Exception {
Field indexes = NativeStructureVolumeIndex.class.getDeclaredField("INDEXES");
indexes.setAccessible(true);
return (Map<Engine, NativeStructureVolumeIndex>) indexes.get(null);
}
private static Map<?, ?> cache(NativeStructureVolumeIndex index, String name) throws Exception {
Field field = NativeStructureVolumeIndex.class.getDeclaredField(name);
field.setAccessible(true);
return (Map<?, ?>) field.get(index);
}
private static StructureStart swampHut(int chunkX, int chunkZ, int x, int z) {
Structure source = new SwampHutStructure(new Structure.StructureSettings(HolderSet.empty()));
SwampHutPiece piece = new SwampHutPiece(RandomSource.create(17L), x, z);
return new StructureStart(source, new ChunkPos(chunkX, chunkZ), 0, new PiecesContainer(List.of(piece)));
}
private static Engine runtimeEngine(AtomicInteger runtimeId) {
return (Engine) Proxy.newProxyInstance(
Engine.class.getClassLoader(),
new Class<?>[]{Engine.class},
(proxy, method, arguments) -> {
if (method.getName().equals("getCacheID")) {
return runtimeId.get();
}
if (method.getName().equals("hashCode")) {
return System.identityHashCode(proxy);
}
if (method.getName().equals("equals")) {
return proxy == arguments[0];
}
if (method.getName().equals("toString")) {
return "runtime-engine-" + runtimeId.get();
}
throw new UnsupportedOperationException(method.getName());
}
);
}
private static int findDisjointWindow(int chunkX, int chunkZ) {
long sourceMask = NativeStructureVolumeIndex.originWindowStripeMask(chunkX, chunkZ);
for (int candidate = 64; candidate < 16_384; candidate += 17) {
if ((sourceMask & NativeStructureVolumeIndex.originWindowStripeMask(candidate, chunkZ)) == 0L) {
return candidate;
}
}
throw new IllegalStateException("Unable to find a disjoint native structure window stripe");
}
private static final class CountingResolver implements NativeStructureVolumeIndex.OriginResolver {
private final AtomicInteger resolutions = new AtomicInteger();
private final int originChunkX;
private final int originChunkZ;
private CountingResolver(int originChunkX, int originChunkZ) {
this.originChunkX = originChunkX;
this.originChunkZ = originChunkZ;
}
private int resolutions() {
return resolutions.get();
}
@Override
public KList<NativeStructureVolume> volumesAt(Engine engine, int chunkX, int chunkZ) {
resolutions.incrementAndGet();
if (chunkX != originChunkX || chunkZ != originChunkZ) {
return NativeStructureVolume.NONE;
}
return NativeStructureVolumeIndex.appendPieces(null, STRUCTURE_KEY, swampHut(chunkX, chunkZ, 4, 6));
}
}
private static final class BlockingResolver implements NativeStructureVolumeIndex.OriginResolver {
private final AtomicInteger resolutions = new AtomicInteger();
private final CountDownLatch firstEntry = new CountDownLatch(1);
private final CountDownLatch secondEntry = new CountDownLatch(1);
private final CountDownLatch release = new CountDownLatch(1);
private int resolutions() {
return resolutions.get();
}
private boolean awaitFirstEntry() throws InterruptedException {
return firstEntry.await(5, TimeUnit.SECONDS);
}
private boolean awaitSecondEntry(long timeoutMillis) throws InterruptedException {
return secondEntry.await(timeoutMillis, TimeUnit.MILLISECONDS);
}
private void release() {
release.countDown();
}
@Override
public KList<NativeStructureVolume> volumesAt(Engine engine, int chunkX, int chunkZ) {
int resolution = resolutions.incrementAndGet();
if (resolution == 1) {
firstEntry.countDown();
try {
if (!release.await(5, TimeUnit.SECONDS)) {
throw new IllegalStateException("Timed out waiting to release native structure resolution");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Interrupted while coordinating native structure resolution", e);
}
} else if (resolution == 2) {
secondEntry.countDown();
}
return NativeStructureVolume.NONE;
}
}
private static final class OrderedResolver implements NativeStructureVolumeIndex.OriginResolver {
@Override
public KList<NativeStructureVolume> volumesAt(Engine engine, int chunkX, int chunkZ) {
return new KList<>(new NativeStructureVolume(
chunkX + ":" + chunkZ,
0, 0, 0,
15, 255, 15));
}
}
private record ScopedEngine(IrisEngine engine, AtomicInteger scopes) {
}
}
@@ -0,0 +1,129 @@
package art.arcane.iris.nativegen.v26_3_R1;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.levelgen.Heightmap;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.OptionalInt;
import java.util.function.IntSupplier;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public final class NativeTerrainHeightCacheTest {
@BeforeClass
public static void bootstrap() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void coordinatesRuntimePredicatesAndBoundsHaveSeparateEntries() {
NativeTerrainHeightCache cache = new NativeTerrainHeightCache();
AtomicInteger calls = new AtomicInteger();
NativeTerrainHeightCache.Query[] queries = {
query(1, -1, 1),
query(1, 1, -1),
query(2, -1, 1),
new NativeTerrainHeightCache.Query(1, -1, 1, Heightmap.Types.OCEAN_FLOOR, -256, 768),
new NativeTerrainHeightCache.Query(1, -1, 1, Heightmap.Types.WORLD_SURFACE, -64, 768),
new NativeTerrainHeightCache.Query(1, -1, 1, Heightmap.Types.WORLD_SURFACE, -256, 256)
};
for (int index = 0; index < queries.length; index++) {
assertEquals(index + 1, cached(cache, queries[index], calls::incrementAndGet));
}
for (int index = 0; index < queries.length; index++) {
assertEquals(index + 1, cached(cache, queries[index], calls::incrementAndGet));
}
assertEquals(queries.length, calls.get());
cache.evictRuntime(1);
assertEquals(3, cached(cache, queries[2], calls::incrementAndGet));
assertEquals(7, cached(cache, queries[0], calls::incrementAndGet));
}
@Test
public void allIntegerResultsAreCacheable() {
NativeTerrainHeightCache cache = new NativeTerrainHeightCache();
NativeTerrainHeightCache.Query query = query(1, 0, 0);
assertEquals(Integer.MIN_VALUE, cached(cache, query, () -> Integer.MIN_VALUE));
assertEquals(Integer.MIN_VALUE, cached(cache, query, () -> 0));
}
@Test
public void failedResolutionCanBeRetried() {
NativeTerrainHeightCache cache = new NativeTerrainHeightCache();
NativeTerrainHeightCache.Query query = query(1, 0, 0);
assertThrows(IllegalStateException.class, () -> cached(cache, query, () -> {
throw new IllegalStateException("terrain unavailable");
}));
assertEquals(42, cached(cache, query, () -> 42));
}
@Test
public void coldResolutionDoesNotHoldCacheMonitor() throws Exception {
NativeTerrainHeightCache cache = new NativeTerrainHeightCache();
CountDownLatch entered = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
ExecutorService executor = Executors.newFixedThreadPool(2);
try {
Future<Integer> slow = executor.submit(() -> cached(cache, query(1, 0, 0), () -> {
entered.countDown();
try {
assertTrue(release.await(5, TimeUnit.SECONDS));
} catch (InterruptedException interruption) {
Thread.currentThread().interrupt();
throw new IllegalStateException(interruption);
}
return 9;
}));
assertTrue(entered.await(5, TimeUnit.SECONDS));
Future<Integer> independent = executor.submit(() -> cached(cache, query(1, 1, 0), () -> 17));
assertEquals(Integer.valueOf(17), independent.get(1, TimeUnit.SECONDS));
release.countDown();
assertEquals(Integer.valueOf(9), slow.get(5, TimeUnit.SECONDS));
} finally {
release.countDown();
executor.shutdownNow();
assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS));
}
}
@Test
public void boundedCacheKeepsRecentlyReadCoordinates() {
NativeTerrainHeightCache cache = new NativeTerrainHeightCache();
for (int x = 0; x < 65_536; x++) {
cached(cache, query(1, x, 0), () -> 8);
}
assertEquals(8, cached(cache, query(1, 0, 0), () -> 99));
cached(cache, query(1, 65_536, 0), () -> 9);
assertEquals(8, cached(cache, query(1, 0, 0), () -> 99));
assertEquals(99, cached(cache, query(1, 1, 0), () -> 99));
}
@Test
public void unavailableResolvedHeightIsNotCached() {
NativeTerrainHeightCache cache = new NativeTerrainHeightCache();
NativeTerrainHeightCache.Query query = query(1, 0, 0);
assertTrue(cache.resolvedHeight(query, OptionalInt::empty).isEmpty());
assertEquals(42, cache.resolvedHeight(query, () -> OptionalInt.of(42)).orElseThrow());
assertEquals(42, cache.resolvedHeight(query, OptionalInt::empty).orElseThrow());
}
private static int cached(NativeTerrainHeightCache cache, NativeTerrainHeightCache.Query query,
IntSupplier resolver) {
return cache.resolvedHeight(query, () -> OptionalInt.of(resolver.getAsInt())).orElseThrow();
}
private static NativeTerrainHeightCache.Query query(int runtimeId, int x, int z) {
return new NativeTerrainHeightCache.Query(runtimeId, x, z, Heightmap.Types.WORLD_SURFACE, -256, 768);
}
}
@@ -0,0 +1,106 @@
package art.arcane.iris.nativegen.v26_3_R1;
import art.arcane.iris.world.history.BoundaryColumnGeometry;
import art.arcane.iris.world.history.TerrainBoundarySignature;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import net.minecraft.SharedConstants;
import net.minecraft.core.Direction;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.NoiseColumn;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.RotatedPillarBlock;
import net.minecraft.world.level.block.LiquidBlock;
import net.minecraft.world.level.levelgen.Heightmap;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.util.List;
import java.util.OptionalInt;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
public final class NativeTransitionColumnTest {
@Rule
public final PlatformLeakGuard leakGuard = PlatformLeakGuard.clean();
@BeforeClass
public static void bootstrap() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
NativeBlockTags.bindHeightmapFixtures();
}
@Test
public void customKeysUseCarrierForNativeColumnsAndHeightmaps() {
String key = "itemsadder:rocks/ruby_ore";
PlatformBlockState custom = mock(PlatformBlockState.class);
PlatformBlockState carrier = mock(PlatformBlockState.class);
when(custom.placementBaseState()).thenReturn(carrier);
when(carrier.key()).thenReturn("minecraft:oak_log[axis=z]");
IrisPlatform platform = mock(IrisPlatform.class);
PlatformRegistries registries = mock(PlatformRegistries.class);
when(platform.registries()).thenReturn(registries);
when(registries.blockOrNull(key)).thenReturn(custom);
LevelHeightAccessor height = LevelHeightAccessor.create(-16, 32);
TerrainBoundarySignature signature = signature(key);
try (MockedStatic<IrisPlatforms> platforms = mockStatic(IrisPlatforms.class)) {
platforms.when(IrisPlatforms::isBound).thenReturn(true);
platforms.when(IrisPlatforms::get).thenReturn(platform);
NoiseColumn column = NativeTransitionColumn.column(signature, height);
assertEquals(Direction.Axis.Z, column.getBlock(-15).getValue(RotatedPillarBlock.AXIS));
assertEquals(-14, NativeTransitionColumn.height(signature, Heightmap.Types.OCEAN_FLOOR_WG, height));
}
}
@Test
public void preservesNativePropertiesCavesAndFluids() {
NoiseColumn column = NativeTransitionColumn.column(signature("minecraft:oak_log[axis=x]"),
LevelHeightAccessor.create(-16, 32));
assertEquals(Direction.Axis.X, column.getBlock(-15).getValue(RotatedPillarBlock.AXIS));
assertTrue(column.getBlock(-14).is(Blocks.CAVE_AIR));
assertEquals(Integer.valueOf(3), column.getBlock(-13).getValue(LiquidBlock.LEVEL));
assertTrue(column.getBlock(0).isAir());
}
@Test
public void usesNativeHeightmapPredicatesForWaterAndOpenSpace() {
TerrainBoundarySignature signature = signature("minecraft:oak_log[axis=x]");
LevelHeightAccessor height = LevelHeightAccessor.create(-16, 32);
assertEquals(-12, NativeTransitionColumn.height(signature, Heightmap.Types.WORLD_SURFACE_WG, height));
assertEquals(-14, NativeTransitionColumn.height(signature, Heightmap.Types.OCEAN_FLOOR_WG, height));
}
@Test
public void rejectsUnknownSavedStatesInsteadOfReplacingTerrain() {
assertThrows(IllegalArgumentException.class, () -> NativeTransitionColumn.column(
signature("missing:removed_block"), LevelHeightAccessor.create(-16, 32)));
}
private static TerrainBoundarySignature signature(String solidState) {
BoundaryColumnGeometry geometry = BoundaryColumnGeometry.fromVoxels(-16, List.of(
new BoundaryColumnGeometry.Voxel("minecraft:stone", BoundaryColumnGeometry.Phase.SOLID, "", false),
new BoundaryColumnGeometry.Voxel(solidState, BoundaryColumnGeometry.Phase.SOLID, "", false),
new BoundaryColumnGeometry.Voxel("minecraft:cave_air", BoundaryColumnGeometry.Phase.AIR, "", false),
new BoundaryColumnGeometry.Voxel("minecraft:water[level=3]", BoundaryColumnGeometry.Phase.FLUID,
"minecraft:water[level=3]", false)));
return new TerrainBoundarySignature(
new TerrainBoundarySignature.Column(0, 0, 3, 1, OptionalInt.of(3), OptionalInt.empty()),
new TerrainBoundarySignature.Samples(new TerrainBoundarySignature.VerticalLayout(-16, 4, 1),
new TerrainBoundarySignature.BiomeEncoding(List.of("minecraft:plains"), new short[]{0})),
geometry);
}
}
@@ -0,0 +1,28 @@
package art.arcane.iris.nativegen.v26_3_R1;
import net.minecraft.core.Holder;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.tags.BlockTags;
import net.minecraft.tags.TagLoader;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import java.util.List;
import java.util.Map;
public final class NativeBlockTags {
private NativeBlockTags() {
}
public static void bindHeightmapFixtures() {
List<Holder<Block>> solid = List.of(Blocks.STONE.builtInRegistryHolder(),
Blocks.OAK_LOG.builtInRegistryHolder(), Blocks.SPRUCE_LOG.builtInRegistryHolder());
List<Holder<Block>> canopy = List.of(Blocks.STONE.builtInRegistryHolder(),
Blocks.OAK_LOG.builtInRegistryHolder(), Blocks.SPRUCE_LOG.builtInRegistryHolder(),
Blocks.OAK_LEAVES.builtInRegistryHolder(), Blocks.SPRUCE_LEAVES.builtInRegistryHolder());
BuiltInRegistries.BLOCK.prepareTagReload(new TagLoader.LoadResult<>(Registries.BLOCK, Map.of(
BlockTags.BLOCKS_MOTION_IN_HEIGHTMAP, canopy,
BlockTags.BLOCKS_MOTION_IN_HEIGHTMAP_NO_LEAVES, solid))).apply();
}
}
@@ -0,0 +1,57 @@
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
import art.arcane.iris.generation.biome.IrisBiomeCustom;
import art.arcane.iris.generation.biome.IrisBiomeCustomSpawn;
import art.arcane.iris.generation.biome.IrisBiomeCustomSpawnType;
import art.arcane.iris.platform.bukkit.nms.datapack.v263.DataFixerV263;
import art.arcane.volmlib.util.collection.KList;
import com.google.gson.JsonParser;
import com.mojang.serialization.JsonOps;
import net.minecraft.SharedConstants;
import net.minecraft.core.HolderLookup;
import net.minecraft.data.registries.VanillaRegistries;
import net.minecraft.resources.RegistryOps;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.attribute.EnvironmentAttributes;
import net.minecraft.world.entity.MobCategory;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.MobSpawnSettings;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class CustomBiomeCodecTest {
private static HolderLookup.Provider registries;
@BeforeClass
public static void bootstrap() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
registries = VanillaRegistries.createWorldLookup();
}
@Test
public void nativeCodecAcceptsCustomColorsAndSpawnCounts() {
IrisBiomeCustom custom = new IrisBiomeCustom();
custom.setId("codec_test");
custom.setWaterColor("#102030");
custom.setFogColor("#405060");
custom.setSpawnRarity(2);
IrisBiomeCustomSpawn spawn = new IrisBiomeCustomSpawn();
spawn.setType("minecraft:zombie");
spawn.setGroup(IrisBiomeCustomSpawnType.MONSTER);
spawn.setMinCount(2);
spawn.setMaxCount(4);
spawn.setWeight(7);
custom.setSpawns(new KList<>(spawn));
Biome biome = Biome.DIRECT_CODEC.parse(RegistryOps.create(JsonOps.INSTANCE, registries),
JsonParser.parseString(custom.generateJson(new DataFixerV263()))).getOrThrow();
MobSpawnSettings settings = biome.getAttributes().applyModifier(EnvironmentAttributes.NATURAL_MOB_SPAWNS,
EnvironmentAttributes.NATURAL_MOB_SPAWNS.defaultValue());
MobSpawnSettings.SpawnerData decoded = settings.getMobsToSpawn(MobCategory.MONSTER).unwrap().getFirst().value();
assertEquals(2, decoded.count().minInclusive());
assertEquals(4, decoded.count().maxInclusive());
assertEquals(0x102030, biome.getWaterColor() & 0xFFFFFF);
}
}
@@ -0,0 +1,64 @@
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.terrain.IrisDimension;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import org.junit.BeforeClass;
import org.junit.Test;
import java.lang.reflect.Proxy;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
public class CustomBiomeSourceStructureContractTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void stackedCustomBiomesUseTheirOwningDimensionNamespace() {
IrisDimension host = new IrisDimension();
host.setLoadKey("Host");
IrisDimension upper = new IrisDimension();
upper.setLoadKey("Layers/Upper");
assertEquals("layers:upper/aurora", CustomBiomeSource.customBiomeKey(upper, "Aurora"));
assertNotEquals(
CustomBiomeSource.customBiomeKey(host, "Aurora"),
CustomBiomeSource.customBiomeKey(upper, "Aurora")
);
}
@Test
public void temporaryNaturalBiomeAnswersAreNotMemoized() {
Engine temporary = engineAnsweringNaturalFallback(true, 12, -8);
Engine stable = engineAnsweringNaturalFallback(false, 12, -8);
assertFalse(CustomBiomeSource.isBiomeCacheable(temporary, 12, -8));
assertTrue(CustomBiomeSource.isBiomeCacheable(stable, 12, -8));
}
private static Engine engineAnsweringNaturalFallback(
boolean naturalFallback,
int expectedX,
int expectedZ
) {
return (Engine) Proxy.newProxyInstance(
Engine.class.getClassLoader(),
new Class<?>[]{Engine.class},
(proxy, method, arguments) -> {
if (!method.getName().equals("answersFromNaturalTerrain")) {
throw new UnsupportedOperationException(method.getName());
}
assertEquals(Integer.valueOf(expectedX), arguments[0]);
assertEquals(Integer.valueOf(expectedZ), arguments[1]);
return naturalFallback;
}
);
}
}
@@ -0,0 +1,97 @@
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
import org.junit.Test;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.LockSupport;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class IrisChunkGeneratorFailureContractTest {
@Test
public void releasedNoiseAdmissionLetsSynchronousDependentWaitForQueuedExclusive() throws Exception {
Semaphore admission = new Semaphore(1, true);
admission.acquire();
CountDownLatch exclusiveEntered = new CountDownLatch(1);
CountDownLatch releaseExclusive = new CountDownLatch(1);
AtomicBoolean dependentFinished = new AtomicBoolean(false);
ExecutorService executor = Executors.newSingleThreadExecutor();
boolean stageReleased = false;
try {
Future<?> exclusive = executor.submit(() -> {
boolean acquired = false;
try {
admission.acquire();
acquired = true;
exclusiveEntered.countDown();
assertTrue(releaseExclusive.await(2, TimeUnit.SECONDS));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException(e);
} finally {
if (acquired) {
admission.release();
}
}
});
awaitQueueLength(admission, 1);
CompletableFuture<Void> outward = new CompletableFuture<>();
outward.thenRun(() -> {
try {
assertTrue(exclusiveEntered.await(2, TimeUnit.SECONDS));
dependentFinished.set(true);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException(e);
}
});
admission.release();
stageReleased = true;
assertTrue(outward.complete(null));
assertTrue(dependentFinished.get());
releaseExclusive.countDown();
exclusive.get(2, TimeUnit.SECONDS);
assertEquals(1, admission.availablePermits());
} finally {
if (!stageReleased) {
admission.release();
}
releaseExclusive.countDown();
executor.shutdownNow();
}
}
@Test
public void transformedDelegateCancellationIsDetectedThroughItsCompletionFailure() {
CompletableFuture<Void> delegate = new CompletableFuture<>();
CompletableFuture<Void> transformed = delegate.thenApply(value -> value);
assertTrue(delegate.cancel(false));
assertFalse(transformed.isCancelled());
CompletionException failure = assertThrows(CompletionException.class, transformed::join);
assertTrue(failure.getCause() instanceof CancellationException);
}
private static void awaitQueueLength(Semaphore semaphore, int expected) {
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2);
while (semaphore.getQueueLength() < expected && System.nanoTime() < deadline) {
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1));
}
assertTrue("Expected at least " + expected + " queued semaphore threads",
semaphore.getQueueLength() >= expected);
}
}
@@ -0,0 +1,58 @@
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
import art.arcane.iris.nativegen.v26_3_R1.NativeStructureLocateResults;
import com.mojang.datafixers.util.Pair;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.world.level.levelgen.structure.Structure;
import org.junit.Test;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
public class IrisChunkGeneratorMonumentLocateContractTest {
@Test
public void mixedLocateSelectsNearestProviderAndPrefersNativeOnTie() {
BlockPos origin = BlockPos.ZERO;
Pair<BlockPos, Holder<Structure>> irisNear = Pair.of(new BlockPos(4, 70, 0), null);
Pair<BlockPos, Holder<Structure>> nativeFar = Pair.of(new BlockPos(8, 70, 0), null);
Pair<BlockPos, Holder<Structure>> nativeNear = Pair.of(new BlockPos(2, 70, 0), null);
Pair<BlockPos, Holder<Structure>> nativeTie = Pair.of(new BlockPos(0, 70, 4), null);
assertSame(irisNear, NativeStructureLocateResults.nearest(origin, irisNear, nativeFar));
assertSame(nativeNear, NativeStructureLocateResults.nearest(origin, irisNear, nativeNear));
assertSame(nativeTie, NativeStructureLocateResults.nearest(origin, irisNear, nativeTie));
}
@Test
public void mixedUnexploredLocateReferencesOnlyTheSelectedProvider() {
BlockPos origin = BlockPos.ZERO;
Pair<BlockPos, Holder<Structure>> irisNear = Pair.of(new BlockPos(4, 70, 0), null);
Pair<BlockPos, Holder<Structure>> nativeFar = Pair.of(new BlockPos(8, 70, 0), null);
AtomicInteger irisReferences = new AtomicInteger();
AtomicInteger nativeReferences = new AtomicInteger();
Pair<BlockPos, Holder<Structure>> irisSelected =
NativeStructureLocateResults.selectAndReference(
origin,
irisNear, () -> irisReferences.incrementAndGet(),
nativeFar, () -> nativeReferences.incrementAndGet());
assertSame(irisNear, irisSelected);
assertEquals(1, irisReferences.get());
assertEquals(0, nativeReferences.get());
Pair<BlockPos, Holder<Structure>> nativeNear = Pair.of(new BlockPos(2, 70, 0), null);
Pair<BlockPos, Holder<Structure>> nativeSelected =
NativeStructureLocateResults.selectAndReference(
origin,
irisNear, () -> irisReferences.incrementAndGet(),
nativeNear, () -> nativeReferences.incrementAndGet());
assertSame(nativeNear, nativeSelected);
assertEquals(1, irisReferences.get());
assertEquals(1, nativeReferences.get());
}
}
@@ -0,0 +1,148 @@
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
import net.minecraft.server.level.ServerLevel;
import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.generation.runtime.GenerationSessionManager;
import art.arcane.iris.generation.runtime.GenerationTransitionGate;
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.world.history.NativeBiomeSpawnSelection;
import art.arcane.iris.world.history.SavedBiomeRuntime;
import art.arcane.iris.generation.context.IrisContext;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.server.Bootstrap;
import net.minecraft.util.random.WeightedList;
import net.minecraft.world.entity.MobCategory;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.MobSpawnSettings;
import net.minecraft.world.level.chunk.ChunkGenerator;
import org.junit.BeforeClass;
import org.junit.Test;
import java.lang.reflect.Field;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisChunkGeneratorSpawnAdmissionTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void tickSpawnQueryLeavesOwnerThreadAvailableForBoundaryCheckpoint() throws Exception {
Fixture fixture = new Fixture();
ExecutorService owner = Executors.newSingleThreadExecutor();
ExecutorService reloader = Executors.newSingleThreadExecutor();
AtomicInteger snapshots = new AtomicInteger();
long previousSession = fixture.sessions.currentSessionId();
try {
assertSame(fixture.expected, owner.submit(fixture::query).get(2L, TimeUnit.SECONDS));
assertEquals(previousSession, fixture.readSession.get());
CompletableFuture<Void> cutover = CompletableFuture.runAsync(() -> {
try (GenerationTransitionGate.Transition ignored = fixture.sessions.transitionGate().beginTransition(2_000L)) {
fixture.sessions.sealAndAwait("Studio generation cutover", 2_000L);
Future<?> query = owner.submit(() -> {
assertTrue(fixture.query().isEmpty());
assertNull(IrisContext.get());
});
owner.submit(snapshots::incrementAndGet).get(2L, TimeUnit.SECONDS);
query.get(2L, TimeUnit.SECONDS);
assertEquals(1, fixture.savedReads.get());
assertEquals(1, fixture.routes.get());
fixture.sessions.activateNextSession();
} catch (Exception failure) {
throw new IllegalStateException(failure);
}
}, reloader);
cutover.get(3L, TimeUnit.SECONDS);
assertEquals(1, snapshots.get());
assertEquals(0, fixture.sessions.activeLeases());
assertSame(fixture.expected, owner.submit(fixture::query).get(2L, TimeUnit.SECONDS));
assertEquals(fixture.sessions.currentSessionId(), fixture.readSession.get());
assertTrue(fixture.readSession.get() > previousSession);
assertEquals(2, fixture.savedReads.get());
assertEquals(2, fixture.routes.get());
assertEquals(0, fixture.sessions.activeLeases());
} finally {
owner.shutdownNow();
reloader.shutdownNow();
assertTrue(owner.awaitTermination(2L, TimeUnit.SECONDS));
assertTrue(reloader.awaitTermination(2L, TimeUnit.SECONDS));
}
}
private static void setField(IrisChunkGenerator generator, String name, Object value) throws Exception {
Field field = IrisChunkGenerator.class.getDeclaredField(name);
field.setAccessible(true);
field.set(generator, value);
}
private static final class Fixture {
private final GenerationSessionManager sessions = new GenerationSessionManager(true);
private final IrisEngine engine = mock(IrisEngine.class);
private final IrisChunkGenerator generator = mock(IrisChunkGenerator.class, CALLS_REAL_METHODS);
private final Holder<Biome> biome = Holder.direct(mock(Biome.class));
private final StructureManager structures = mock(StructureManager.class);
private final WeightedList<MobSpawnSettings.SpawnerData> expected = mock(WeightedList.class);
private final ServerLevel level = mock(ServerLevel.class);
private final AtomicInteger savedReads = new AtomicInteger();
private final AtomicInteger routes = new AtomicInteger();
private final AtomicLong readSession = new AtomicLong();
private Fixture() throws Exception {
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
SavedBiomeRuntime saved = mock(SavedBiomeRuntime.class);
ChunkGenerator delegate = mock(ChunkGenerator.class);
CustomBiomeSource source = mock(CustomBiomeSource.class);
when(level.getBiome(new BlockPos(0, 64, 0))).thenReturn(biome);
setField(generator, "runtimeLevel", level);
when(engine.getGenerationSessions()).thenReturn(sessions);
when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
when(router.biomes()).thenReturn(saved);
when(saved.nativeSpawnSelection(0, 64, 0, "")).thenAnswer(invocation -> {
assertAdmitted();
savedReads.incrementAndGet();
return new NativeBiomeSpawnSelection(NativeBiomeSpawnSelection.Mode.RETAINED, "minecraft:plains");
});
when(engine.openGenerationHistoryCoordinateScope(0, 0)).thenAnswer(invocation -> {
assertAdmitted();
routes.incrementAndGet();
return null;
});
when(delegate.getMobsAt(level, structures, MobCategory.MONSTER, new BlockPos(0, 64, 0)))
.thenReturn(expected);
setField(generator, "engine", engine);
setField(generator, "delegate", delegate);
setField(generator, "customBiomeSource", source);
}
private WeightedList<MobSpawnSettings.SpawnerData> query() {
return generator.getMobsAt(level, structures, MobCategory.MONSTER, new BlockPos(0, 64, 0));
}
private void assertAdmitted() {
assertEquals(1, sessions.activeLeases());
assertSame(engine, IrisContext.get().getEngine());
readSession.set(IrisContext.get().getGenerationSessionId());
}
}
}
@@ -0,0 +1,437 @@
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
import net.minecraft.world.level.levelgen.structure.placement.AbstractSpreadingStructurePlacement;
import art.arcane.iris.pack.datapack.DatapackIngestService;
import art.arcane.iris.pack.datapack.DatapackStructureScopeIndex;
import art.arcane.iris.structure.nativegen.IrisImportedStructureControl;
import art.arcane.iris.structure.placement.IrisStructureSetFrequencyOverride;
import art.arcane.volmlib.util.collection.KList;
import com.mojang.datafixers.util.Either;
import net.minecraft.SharedConstants;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderOwner;
import net.minecraft.core.Vec3i;
import net.minecraft.core.component.DataComponentMap;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.Bootstrap;
import net.minecraft.tags.TagKey;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureSet;
import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement;
import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadType;
import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement;
import com.mojang.serialization.MapCodec;
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
import org.junit.Test;
import org.junit.BeforeClass;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Stream;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
public class NMSBindingDatapackStructureScopeTest {
private static final String SOURCE = "https://example.test/managed.zip";
@BeforeClass
public static void bootstrapMinecraft() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void managedSetContainingVanillaStructureIsAbsentOutsideDeclaringDimension() {
Holder<Structure> vanillaStructure = structureHolder("minecraft:pillager_outpost");
Holder<StructureSet> managedSet = structureSetHolder(
"managed:illager_barracks", vanillaStructure);
DatapackStructureScopeIndex index = index(
List.of(),
List.of("managed:illager_barracks"));
DatapackStructureStateFilter.Selection vanilla = DatapackStructureStateFilter.filter(
List.of(managedSet), index, Set.of());
DatapackStructureStateFilter.Selection declaring = DatapackStructureStateFilter.filter(
List.of(managedSet), index, index.declaredSources(List.of(SOURCE)));
assertEquals(0, vanilla.structureSets().size());
assertEquals(1, vanilla.excludedManagedSets());
assertEquals(1, declaring.structureSets().size());
assertSame(managedSet, declaring.structureSets().getFirst());
}
@Test
public void unmanagedSetRetainsOnlyDefinitionsAllowedInTheWorld() {
Holder<Structure> vanillaStructure = structureHolder("minecraft:village_plains");
Holder<Structure> managedStructure = structureHolder("managed:tavern");
Holder<StructureSet> vanillaSet = structureSetHolder(
"minecraft:villages", vanillaStructure, managedStructure);
DatapackStructureScopeIndex index = index(
List.of("managed:tavern"),
List.of());
DatapackStructureStateFilter.Selection vanilla = DatapackStructureStateFilter.filter(
List.of(vanillaSet), index, Set.of());
DatapackStructureStateFilter.Selection declaring = DatapackStructureStateFilter.filter(
List.of(vanillaSet), index, index.declaredSources(List.of(SOURCE)));
assertEquals(1, vanilla.structureSets().size());
assertEquals(1, vanilla.structureSets().getFirst().value().structures().size());
assertSame(vanillaStructure,
vanilla.structureSets().getFirst().value().structures().getFirst().structure());
assertSame(vanillaSet.value().placement(),
vanilla.structureSets().getFirst().value().placement());
assertSame(vanillaSet, declaring.structureSets().getFirst());
}
@Test
public void setWithNoAllowedDefinitionsIsRemoved() {
Holder<StructureSet> unmanagedSet = structureSetHolder(
"minecraft:custom", structureHolder("managed:only"));
DatapackStructureScopeIndex index = index(List.of("managed:only"), List.of());
DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter(
List.of(unmanagedSet), index, Set.of());
assertEquals(0, selection.structureSets().size());
}
@Test
public void spigotDirectHolderRecoversItsRegistryKeyFromSharedEntries() {
ResourceKey<StructureSet> key = ResourceKey.create(
Registries.STRUCTURE_SET,
Identifier.parse("managed:illager_barracks"));
List<StructureSet.StructureSelectionEntry> entries = List.of(
new StructureSet.StructureSelectionEntry(
structureHolder("minecraft:pillager_outpost"), 1));
Holder<StructureSet> registered = new KeyedHolder<>(key, new StructureSet(
entries,
new RandomSpreadStructurePlacement(
34, 8, RandomSpreadType.LINEAR, 10387312)));
Holder<StructureSet> spigotDirect = Holder.direct(new StructureSet(
entries,
new RandomSpreadStructurePlacement(
34, 8, RandomSpreadType.LINEAR, 14357620)));
DatapackStructureScopeIndex scopeIndex = index(
List.of(), List.of("managed:illager_barracks"));
DatapackStructureStateFilter.StructureSetKeyIndex keyIndex =
DatapackStructureStateFilter.keyIndex(List.of(registered));
DatapackStructureStateFilter.Selection excluded =
DatapackStructureStateFilter.filter(
List.of(spigotDirect),
scopeIndex,
Set.of(),
new IrisImportedStructureControl(),
keyIndex);
DatapackStructureStateFilter.Selection retained =
DatapackStructureStateFilter.filter(
List.of(spigotDirect),
scopeIndex,
scopeIndex.declaredSources(List.of(SOURCE)),
new IrisImportedStructureControl(),
keyIndex);
assertEquals(0, excluded.structureSets().size());
assertEquals(1, excluded.excludedManagedSets());
assertSame(spigotDirect, retained.structureSets().getFirst());
assertEquals(1, retained.retainedManagedSets());
}
@Test
public void structureScopeDoesNotLinkPaperOnlyPlacementClasses() throws IOException {
InputStream classResource = NMSBindingDatapackStructureScopeTest.class
.getResourceAsStream("DatapackStructureStateFilter.class");
assertNotNull(classResource);
try (InputStream input = classResource) {
String classFile = new String(input.readAllBytes(), StandardCharsets.ISO_8859_1);
assertFalse(classFile.contains("KeyedRandomSpreadStructurePlacement"));
}
}
@Test
public void exactFrequencyOverridesScaleOnlyTheNamedNativeStructureSet() {
Holder<StructureSet> complexes = structureSetHolder(
"minecraft:nether_complexes",
new RandomSpreadStructurePlacement(27, 4, RandomSpreadType.LINEAR, 30084232),
structureHolder("minecraft:fortress"));
Holder<StructureSet> fossils = structureSetHolder(
"minecraft:nether_fossils",
new RandomSpreadStructurePlacement(2, 1, RandomSpreadType.LINEAR, 14357921),
structureHolder("minecraft:nether_fossil"));
KList<IrisStructureSetFrequencyOverride> overrides = new KList<>();
overrides.add(new IrisStructureSetFrequencyOverride()
.setStructureSet("minecraft:nether_complexes")
.setMultiplier(1.1D));
IrisImportedStructureControl control = new IrisImportedStructureControl()
.setFrequencyOverrides(overrides);
DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter(
List.of(complexes, fossils), index(List.of(), List.of()), Set.of(), control);
RandomSpreadStructurePlacement scaledComplexes =
(RandomSpreadStructurePlacement) selection.structureSets().get(0).value().placement();
RandomSpreadStructurePlacement unchangedFossils =
(RandomSpreadStructurePlacement) selection.structureSets().get(1).value().placement();
assertEquals(26, scaledComplexes.spacing());
assertEquals(4, scaledComplexes.separation());
assertEquals(2, unchangedFossils.spacing());
assertSame(fossils, selection.structureSets().get(1));
assertEquals(27, ((RandomSpreadStructurePlacement) complexes.value().placement()).spacing());
}
@Test
public void frequencyOnlyPassLeavesUnrelatedCustomPlacementUntouched() {
Holder<StructureSet> exclusionTarget = structureSetHolder(
"example:target", structureHolder("example:target"));
Holder<StructureSet> custom = structureSetHolder(
"example:custom",
new UnsupportedPlacement(Optional.of(
new AbstractSpreadingStructurePlacement.ExclusionZone(exclusionTarget, 1))),
structureHolder("example:custom"));
KList<IrisStructureSetFrequencyOverride> overrides = new KList<>();
overrides.add(new IrisStructureSetFrequencyOverride()
.setStructureSet("minecraft:nether_complexes")
.setMultiplier(1.1D));
IrisImportedStructureControl control = new IrisImportedStructureControl()
.setFrequencyOverrides(overrides);
DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter(
List.of(custom), index(List.of(), List.of()), Set.of(), control);
assertSame(custom, selection.structureSets().getFirst());
}
@Test
public void frequencyOnlyPassRebindsDependentExclusionTarget() {
Holder<StructureSet> target = structureSetHolder(
"example:target",
new RandomSpreadStructurePlacement(27, 4, RandomSpreadType.LINEAR, 30084232),
structureHolder("example:target"));
RandomSpreadStructurePlacement dependentPlacement = new RandomSpreadStructurePlacement(
Vec3i.ZERO,
AbstractSpreadingStructurePlacement.FrequencyReductionMethod.DEFAULT,
1.0F,
4567,
Optional.of(new AbstractSpreadingStructurePlacement.ExclusionZone(target, 1)),
32,
8,
RandomSpreadType.LINEAR);
Holder<StructureSet> dependent = structureSetHolder(
"example:dependent",
dependentPlacement,
structureHolder("example:dependent"));
KList<IrisStructureSetFrequencyOverride> overrides = new KList<>();
overrides.add(new IrisStructureSetFrequencyOverride()
.setStructureSet("example:target")
.setMultiplier(1.1D));
IrisImportedStructureControl control = new IrisImportedStructureControl()
.setFrequencyOverrides(overrides);
DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter(
List.of(dependent, target), index(List.of(), List.of()), Set.of(), control);
Holder<StructureSet> scaledDependent = selection.structureSets().get(0);
Holder<StructureSet> scaledTarget = selection.structureSets().get(1);
assertNotSame(dependent, scaledDependent);
assertNotSame(target, scaledTarget);
assertSame(scaledTarget, DatapackStructureStateFilter.exclusionZone(
scaledDependent.value().placement()).orElseThrow().otherSet());
assertEquals(26, ((RandomSpreadStructurePlacement)
scaledTarget.value().placement()).spacing());
}
@Test
public void frequencyOnlyPassRebindsAffectedExclusionCycle() {
MutableStructureSetHolder first = new MutableStructureSetHolder("example:first");
MutableStructureSetHolder second = new MutableStructureSetHolder("example:second");
first.bind(structureSet(
new RandomSpreadStructurePlacement(
Vec3i.ZERO,
AbstractSpreadingStructurePlacement.FrequencyReductionMethod.DEFAULT,
1.0F,
101,
Optional.of(new AbstractSpreadingStructurePlacement.ExclusionZone(second, 1)),
32,
8,
RandomSpreadType.LINEAR),
"example:first"));
second.bind(structureSet(
new RandomSpreadStructurePlacement(
Vec3i.ZERO,
AbstractSpreadingStructurePlacement.FrequencyReductionMethod.DEFAULT,
1.0F,
102,
Optional.of(new AbstractSpreadingStructurePlacement.ExclusionZone(first, 1)),
27,
4,
RandomSpreadType.LINEAR),
"example:second"));
KList<IrisStructureSetFrequencyOverride> overrides = new KList<>();
overrides.add(new IrisStructureSetFrequencyOverride()
.setStructureSet("example:second")
.setMultiplier(1.1D));
IrisImportedStructureControl control = new IrisImportedStructureControl()
.setFrequencyOverrides(overrides);
DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter(
List.of(first, second), index(List.of(), List.of()), Set.of(), control);
Holder<StructureSet> scaledFirst = selection.structureSets().get(0);
Holder<StructureSet> scaledSecond = selection.structureSets().get(1);
assertSame(scaledSecond, DatapackStructureStateFilter.exclusionZone(
scaledFirst.value().placement()).orElseThrow().otherSet());
assertSame(scaledFirst, DatapackStructureStateFilter.exclusionZone(
scaledSecond.value().placement()).orElseThrow().otherSet());
assertEquals(26, ((RandomSpreadStructurePlacement)
scaledSecond.value().placement()).spacing());
}
@Test
public void randomSpreadSubtypeUsesTheCanonicalPlacementContract() {
Holder<StructureSet> custom = structureSetHolder(
"example:custom",
new CustomRandomSpreadPlacement(),
structureHolder("example:custom"));
KList<IrisStructureSetFrequencyOverride> overrides = new KList<>();
overrides.add(new IrisStructureSetFrequencyOverride()
.setStructureSet("example:custom")
.setMultiplier(1.1D));
IrisImportedStructureControl control = new IrisImportedStructureControl()
.setFrequencyOverrides(overrides);
DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter(
List.of(custom), index(List.of(), List.of()), Set.of(), control);
StructurePlacement placement = selection.structureSets().getFirst().value().placement();
assertEquals(RandomSpreadStructurePlacement.class, placement.getClass());
assertEquals(26, ((RandomSpreadStructurePlacement) placement).spacing());
}
@Test
public void excludedManagedSetCannotSuppressAnAllowedSetThroughExclusionZone() {
Holder<StructureSet> managedSet = structureSetHolder(
"managed:blocked",
structureHolder("managed:blocked"));
RandomSpreadStructurePlacement originalPlacement = new RandomSpreadStructurePlacement(
Vec3i.ZERO,
AbstractSpreadingStructurePlacement.FrequencyReductionMethod.DEFAULT,
1.0F,
4567,
Optional.of(new AbstractSpreadingStructurePlacement.ExclusionZone(managedSet, 1)),
32,
8,
RandomSpreadType.LINEAR);
Holder<StructureSet> vanillaSet = structureSetHolder(
"minecraft:allowed",
originalPlacement,
structureHolder("minecraft:village_plains"));
DatapackStructureScopeIndex index = index(
List.of("managed:blocked"),
List.of("managed:blocked"));
DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter(
List.of(vanillaSet, managedSet), index, Set.of());
assertEquals(1, selection.structureSets().size());
StructurePlacement scopedPlacement = selection.structureSets().getFirst().value().placement();
assertEquals(0, DatapackStructureStateFilter.exclusionZone(scopedPlacement).stream().count());
}
private static DatapackStructureScopeIndex index(
List<String> structureKeys,
List<String> structureSetKeys
) {
return DatapackStructureScopeIndex.create(List.of(
new DatapackIngestService.StructureScopeResources(
SOURCE,
structureKeys,
structureSetKeys)));
}
private static Holder<Structure> structureHolder(String key) {
return new KeyedHolder<>(ResourceKey.create(Registries.STRUCTURE, Identifier.parse(key)), null);
}
private static Holder<StructureSet> structureSetHolder(
String key,
Holder<Structure>... structures
) {
return structureSetHolder(
key,
new RandomSpreadStructurePlacement(32, 8, RandomSpreadType.LINEAR, 12345),
structures);
}
private static Holder<StructureSet> structureSetHolder(
String key,
StructurePlacement placement,
Holder<Structure>... structures
) {
List<StructureSet.StructureSelectionEntry> entries = Stream.of(structures)
.map(structure -> new StructureSet.StructureSelectionEntry(structure, 1))
.toList();
StructureSet value = new StructureSet(entries, placement);
return new KeyedHolder<>(
ResourceKey.create(Registries.STRUCTURE_SET, Identifier.parse(key)),
value);
}
private static StructureSet structureSet(StructurePlacement placement, String structureKey) {
return new StructureSet(
List.of(new StructureSet.StructureSelectionEntry(
structureHolder(structureKey), 1)),
placement);
}
private static final class KeyedHolder<T> extends Holder.Reference<T> {
private KeyedHolder(ResourceKey<T> key, T value) {
super(Type.STAND_ALONE, new HolderOwner<>() {}, key, null);
bindValue(value);
}
}
private static final class UnsupportedPlacement extends AbstractSpreadingStructurePlacement {
private UnsupportedPlacement(Optional<ExclusionZone> exclusionZone) {
super(Vec3i.ZERO, FrequencyReductionMethod.DEFAULT, 1F, 1, exclusionZone);
}
@Override
protected boolean isPlacementChunk(ChunkGeneratorStructureState state, int x, int z) {
return false;
}
@Override
public MapCodec<? extends AbstractSpreadingStructurePlacement> codec() {
return null;
}
}
private static final class MutableStructureSetHolder extends Holder.Reference<StructureSet> {
private MutableStructureSetHolder(String key) {
super(Type.STAND_ALONE, new HolderOwner<>() {
}, ResourceKey.create(Registries.STRUCTURE_SET, Identifier.parse(key)), null);
}
private void bind(StructureSet structureSet) {
bindValue(structureSet);
}
}
private static final class CustomRandomSpreadPlacement extends RandomSpreadStructurePlacement {
private CustomRandomSpreadPlacement() {
super(27, 4, RandomSpreadType.LINEAR, 30084232);
}
}
}
@@ -0,0 +1,250 @@
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
import ca.spottedleaf.moonrise.common.util.TickThread;
import com.mojang.serialization.Codec;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.IdMapper;
import net.minecraft.core.SectionPos;
import net.minecraft.server.Bootstrap;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.ai.village.poi.PoiManager;
import net.minecraft.world.entity.ai.village.poi.PoiRecord;
import net.minecraft.world.entity.ai.village.poi.PoiSection;
import net.minecraft.world.entity.ai.village.poi.PoiType;
import net.minecraft.world.entity.ai.village.poi.PoiTypes;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ImposterProtoChunk;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.level.chunk.PalettedContainer;
import net.minecraft.world.level.chunk.PalettedContainerFactory;
import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.chunk.Strategy;
import net.minecraft.world.level.chunk.UpgradeData;
import org.bukkit.NamespacedKey;
import org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry;
import org.bukkit.craftbukkit.persistence.DirtyCraftPersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class NativeStructurePoiUpdatesTest {
private static final NamespacedKey DIRTY_SECTIONS = new NamespacedKey("iris", "native_poi_sections");
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void generationRecordsIntentBeforeSuppressingNativePoiCallbacks() {
Fixture fixture = new Fixture();
BlockPos position = new BlockPos(-31, 5, 49);
doAnswer(call -> {
assertNotNull(fixture.proto.persistentDataContainer.get(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY));
assertEquals(Block.UPDATE_CLIENTS | 4096, (int) call.getArgument(2));
fixture.proto.setBlockState(position, call.getArgument(1), 0);
return true;
}).when(fixture.world).setBlock(any(), any(), anyInt(), anyInt());
assertTrue(fixture.updates.setBlock(fixture.world, position,
Blocks.COMPOSTER.defaultBlockState(), Block.UPDATE_CLIENTS, 17));
assertArrayEquals(new long[]{16L}, fixture.marker());
}
@Test
public void denseWritesStoreOneSectionBitAndSurvivePersistentDataRoundTrip() throws Exception {
Fixture fixture = new Fixture();
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
fixture.write(new BlockPos(-32 + x, 5, 48 + z), Blocks.COMPOSTER.defaultBlockState());
}
}
assertArrayEquals(new long[]{16L}, fixture.marker());
DirtyCraftPersistentDataContainer restored = new DirtyCraftPersistentDataContainer(new CraftPersistentDataTypeRegistry());
restored.putAll(fixture.proto.persistentDataContainer.toTagCompound());
fixture.loaded.persistentDataContainer = restored;
onOwner(() -> NativeStructurePoiUpdates.reconcile(fixture.loaded));
assertEquals(256L, fixture.records().getRecords(type -> true, PoiManager.Occupancy.ANY).count());
assertFalse(restored.has(DIRTY_SECTIONS));
}
@Test
public void validSectionsReconcileFinalStatesAndKeepMatchingClaimedTickets() throws Exception {
Fixture fixture = new Fixture();
BlockPos retained = new BlockPos(-31, 5, 49);
BlockPos replaced = new BlockPos(-30, 5, 49);
BlockPos removed = new BlockPos(-29, 5, 49);
Holder<PoiType> farmer = PoiTypes.forState(Blocks.COMPOSTER.defaultBlockState()).orElseThrow();
PoiSection existing = new PoiSection.Packed(true, List.of(
new PoiRecord.Packed(retained, farmer, 0),
new PoiRecord.Packed(replaced, farmer, 1),
new PoiRecord.Packed(removed, farmer, 1))).unpack(() -> {});
fixture.pois.put(SectionPos.of(retained).asLong(), existing);
PoiRecord retainedRecord = existing.getRecords(type -> true, PoiManager.Occupancy.ANY)
.filter(record -> record.getPos().equals(retained)).findFirst().orElseThrow();
for (BlockPos position : List.of(retained, replaced, removed)) {
fixture.proto.setBlockState(position, Blocks.COMPOSTER.defaultBlockState(), 0);
}
fixture.write(replaced, Blocks.BARREL.defaultBlockState());
fixture.write(removed, Blocks.AIR.defaultBlockState());
onOwner(() -> NativeStructurePoiUpdates.reconcile(fixture.loaded));
assertEquals(PoiTypes.forState(Blocks.BARREL.defaultBlockState()), existing.getType(replaced));
assertTrue(existing.getType(removed).isEmpty());
assertSame(retainedRecord, existing.getRecords(type -> true, PoiManager.Occupancy.ANY)
.filter(record -> record.getPos().equals(retained)).findFirst().orElseThrow());
assertEquals(0, existing.getFreeTickets(retained));
onOwner(() -> NativeStructurePoiUpdates.reconcile(fixture.loaded));
assertEquals(0, existing.getFreeTickets(retained));
}
@Test
public void failedReconciliationRetainsIntentForRetry() throws Exception {
Fixture fixture = new Fixture();
fixture.write(new BlockPos(-31, 5, 49), Blocks.COMPOSTER.defaultBlockState());
IllegalStateException failure = new IllegalStateException("POI storage failure");
doThrow(failure).when(fixture.manager).add(any(), any());
onOwner(() -> assertSame(failure, assertThrows(IllegalStateException.class,
() -> NativeStructurePoiUpdates.reconcile(fixture.loaded))));
assertArrayEquals(new long[]{16L}, fixture.marker());
fixture.allowPoiAdds();
onOwner(() -> NativeStructurePoiUpdates.reconcile(fixture.loaded));
assertFalse(fixture.proto.persistentDataContainer.has(DIRTY_SECTIONS));
}
@Test
public void explicitPoiSuppressionAndReadOnlyFullChunksDoNotCreateIntent() {
Fixture fixture = new Fixture();
BlockPos position = new BlockPos(-31, 5, 49);
fixture.updates.setBlock(fixture.world, position, Blocks.COMPOSTER.defaultBlockState(),
4096, 17);
assertFalse(fixture.proto.persistentDataContainer.has(DIRTY_SECTIONS));
ImposterProtoChunk full = mock(ImposterProtoChunk.class);
when(fixture.world.getChunk(any(BlockPos.class))).thenReturn(full);
fixture.updates.setBlock(fixture.world, position, Blocks.BARREL.defaultBlockState(), 2, 17);
assertFalse(fixture.proto.persistentDataContainer.has(DIRTY_SECTIONS));
}
@Test
public void invalidSectionBitsRetainTheStoredRecord() throws Exception {
Fixture fixture = new Fixture();
fixture.proto.persistentDataContainer.set(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY, new long[]{1L << 24});
onOwner(() -> assertThrows(IllegalArgumentException.class,
() -> NativeStructurePoiUpdates.reconcile(fixture.loaded)));
assertArrayEquals(new long[]{1L << 24}, fixture.marker());
}
private static void onOwner(Runnable action) throws Exception {
FutureTask<Void> task = new FutureTask<>(action, null);
new TickThread(task, "Native POI test").start();
task.get(5L, TimeUnit.SECONDS);
}
private static PalettedContainerFactory containerFactory() {
Strategy<BlockState> blocks = Strategy.createForBlockStates(Block.BLOCK_STATE_REGISTRY);
BlockState air = Blocks.AIR.defaultBlockState();
IdMapper<Holder<Biome>> biomeIds = new IdMapper<>();
Holder<Biome> biome = Holder.direct((Biome) null);
biomeIds.add(biome);
Strategy<Holder<Biome>> biomes = Strategy.createForBiomes(biomeIds);
Codec<Holder<Biome>> biomeCodec = Codec.STRING.xmap(name -> biome, holder -> "iris-test-biome");
return new PalettedContainerFactory(blocks, air, PalettedContainer.codecRW(BlockState.CODEC, blocks, air),
biomes, biome, PalettedContainer.codecRO(biomeCodec, biomes, biome),
PalettedContainer.codecRW(biomeCodec, biomes, biome));
}
private static final class Fixture {
private final NativeStructurePoiUpdates updates = new NativeStructurePoiUpdates(4096);
private final ProtoChunk proto = new ProtoChunk(new ChunkPos(-2, 3), UpgradeData.EMPTY,
LevelHeightAccessor.create(-64, 384), containerFactory(), null);
private final LevelChunk loaded = mock(LevelChunk.class);
private final WorldGenLevel world = mock(WorldGenLevel.class);
private final PoiManager manager = mock(PoiManager.class);
private final Map<Long, PoiSection> pois = new HashMap<>();
private Fixture() {
ServerLevel level = mock(ServerLevel.class);
when(level.getPoiManager()).thenReturn(manager);
loaded.persistentDataContainer = proto.persistentDataContainer;
when(loaded.getLevel()).thenReturn(level);
when(loaded.getPos()).thenReturn(proto.getPos());
when(loaded.getSectionsCount()).thenReturn(proto.getSectionsCount());
when(loaded.getSectionYFromSectionIndex(anyInt())).thenAnswer(call -> proto.getSectionYFromSectionIndex(call.getArgument(0)));
when(loaded.getSection(anyInt())).thenAnswer(call -> proto.getSection(call.getArgument(0)));
when(loaded.getBlockState(any())).thenAnswer(call -> proto.getBlockState(call.getArgument(0)));
when(world.getChunk(any(BlockPos.class))).thenReturn(proto);
when(world.setBlock(any(), any(), anyInt(), anyInt())).thenAnswer(call -> {
proto.setBlockState(call.getArgument(0), call.getArgument(1), call.getArgument(2));
return true;
});
when(manager.getOrLoad(anyLong())).thenAnswer(call -> Optional.ofNullable(pois.get((long) call.getArgument(0))));
when(manager.getType(any())).thenAnswer(call -> {
BlockPos position = call.getArgument(0);
PoiSection section = pois.get(SectionPos.of(position).asLong());
return section == null ? Optional.empty() : section.getType(position);
});
doAnswer(call -> {
BlockPos position = call.getArgument(0);
pois.get(SectionPos.of(position).asLong()).remove(position);
return null;
}).when(manager).remove(any());
allowPoiAdds();
}
private void allowPoiAdds() {
doAnswer(call -> {
BlockPos position = call.getArgument(0);
return pois.computeIfAbsent(SectionPos.of(position).asLong(), ignored -> new PoiSection(() -> {}))
.add(position, call.getArgument(1));
}).when(manager).add(any(), any());
}
private void write(BlockPos position, BlockState state) {
updates.setBlock(world, position, state, Block.UPDATE_CLIENTS, Block.UPDATE_LIMIT);
}
private long[] marker() {
return proto.persistentDataContainer.get(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY);
}
private PoiSection records() {
return pois.get(SectionPos.of(new BlockPos(-31, 5, 49)).asLong());
}
}
}
@@ -0,0 +1,464 @@
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
import art.arcane.iris.structure.placement.IrisStructureTerrain;
import art.arcane.iris.structure.placement.IrisStructureTerrainMode;
import art.arcane.iris.nativegen.v26_3_R1.NativeStructurePostProcessor;
import art.arcane.iris.nativegen.v26_3_R1.NativeStructureTerrainIntegrator;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderSet;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeManager;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.entity.EntityTypeTest;
import net.minecraft.world.level.levelgen.GenerationStep;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.TerrainAdjustment;
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidStructure;
import net.minecraft.world.level.levelgen.structure.templatesystem.GravityProcessor;
import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
import net.minecraft.world.level.material.Fluid;
import net.minecraft.world.level.material.FluidState;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
import net.minecraft.world.ticks.LevelTickAccess;
import net.minecraft.world.ticks.ScheduledTick;
import org.junit.BeforeClass;
import org.junit.Test;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class NativeStructureWorldgenAccessTest {
private static final int GENERATION_CENTER_X = 60;
private static final int GENERATION_CENTER_Z = 15;
private static final int SURFACE_FIRST_FREE_Y = 80;
private static final int FLOOR_FIRST_FREE_Y = 70;
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void ordinaryWorldFlattenPreparationHandlesReferencedVillagePiecesBeyondEightChunks() {
int farX = (GENERATION_CENTER_X - 9) << 4;
int farZ = (GENERATION_CENTER_Z + 4) << 4;
BoundingBox bounds = new BoundingBox(farX, 64, farZ, farX + 4, 90, farZ + 4);
PoolElementStructurePiece piece = new PoolElementStructurePiece(
null, StructurePoolElement.single("minecraft:empty").apply(StructureTemplatePool.Projection.RIGID),
new BlockPos(farX, 64, farZ), 1, Rotation.NONE, bounds, LiquidSettings.APPLY_WATERLOGGING);
Structure structure = new DesertPyramidStructure(new Structure.StructureSettings(
HolderSet.empty(), Map.of(), GenerationStep.Decoration.SURFACE_STRUCTURES, TerrainAdjustment.BEARD_BOX));
StructureStart start = new StructureStart(structure, generationCenter(), 0, new PiecesContainer(List.of(piece)));
List<NativeStructureTerrainIntegrator.TerrainTarget> targets = List.of(
new NativeStructureTerrainIntegrator.TerrainTarget("minecraft:village_plains", start,
new IrisStructureTerrain().setMode(IrisStructureTerrainMode.FLATTEN)));
BoundingBox area = new BoundingBox(generationCenter().getMinBlockX(), -64, generationCenter().getMinBlockZ(),
generationCenter().getMaxBlockX(), 319, generationCenter().getMaxBlockZ());
RecordingDelegate unbounded = new RecordingDelegate();
unbounded.rejectDistantTerrainReads = true;
assertThrows(IllegalStateException.class, () -> NativeStructurePostProcessor.prepareTerrain(
unbounded.world(), area, targets, (palette, rng, x, y, z) -> Blocks.STONE.defaultBlockState()));
RecordingDelegate recording = new RecordingDelegate();
recording.rejectDistantTerrainReads = true;
NativeStructureWorldgenAccess access = access(recording);
NativeStructurePostProcessor.prepareTerrain(
access, area, targets, (palette, rng, x, y, z) -> Blocks.STONE.defaultBlockState());
assertEquals(0, recording.heightReads);
assertEquals(0, recording.chunkReads);
assertEquals(0, recording.terrainReads);
assertEquals(0, recording.mutations);
assertEquals(91, access.getHeight(Heightmap.Types.WORLD_SURFACE_WG,
generationCenter().getMinBlockX(), generationCenter().getMinBlockZ()));
assertEquals(1, recording.heightReads);
}
@Test
public void projectedTemplatePrefiltersUseRemoteTerrainAndRetainLocalHeightmaps() {
RecordingDelegate recording = new RecordingDelegate();
recording.rejectDistantTerrainReads = true;
NativeStructureWorldgenAccess access = access(recording);
GravityProcessor gravity = new GravityProcessor(Heightmap.Types.WORLD_SURFACE_WG, -1);
BlockPos remote = new BlockPos((GENERATION_CENTER_X - 9) << 4, 64, (GENERATION_CENTER_Z + 4) << 4);
StructureTemplate.StructureBlockInfo raw = new StructureTemplate.StructureBlockInfo(
new BlockPos(0, 4, 0), Blocks.DIRT_PATH.defaultBlockState(), null);
StructurePlaceSettings settings = new StructurePlaceSettings().addProcessor(gravity);
StructureTemplate.StructureBlockInfo projected = StructureTemplate.processBlockInfos(
access, remote, BlockPos.ZERO, settings, List.of(raw)).getFirst();
assertEquals(remote.atY(SURFACE_FIRST_FREE_Y + 3), projected.pos());
assertEquals(0, recording.heightReads);
BlockPos local = generationCenter().getMiddleBlockPosition(64);
StructureTemplate.StructureBlockInfo localBlock = new StructureTemplate.StructureBlockInfo(
BlockPos.ZERO, Blocks.DIRT_PATH.defaultBlockState(), null);
StructureTemplate.StructureBlockInfo localProjection = StructureTemplate.processBlockInfos(
access, local, BlockPos.ZERO, settings, List.of(localBlock)).getFirst();
assertEquals(local.atY(90), localProjection.pos());
assertEquals(1, recording.heightReads);
assertEquals(0, recording.chunkReads);
}
@Test
public void distanceTwoTerrainReadsNeverReachTheDelegate() {
RecordingDelegate recording = new RecordingDelegate();
NativeStructureWorldgenAccess access = access(recording);
ChunkPos generationCenter = generationCenter();
int x = generationCenter.getMiddleBlockX();
int z = (generationCenter.z() + 2) << 4;
assertSame(Blocks.STONE, access.getBlockState(new BlockPos(x, FLOOR_FIRST_FREE_Y - 1, z)).getBlock());
assertSame(Blocks.WATER, access.getBlockState(new BlockPos(x, FLOOR_FIRST_FREE_Y, z)).getBlock());
assertSame(Blocks.AIR, access.getBlockState(new BlockPos(x, SURFACE_FIRST_FREE_Y, z)).getBlock());
assertSame(Blocks.WATER.defaultBlockState().getFluidState(),
access.getFluidState(new BlockPos(x, FLOOR_FIRST_FREE_Y, z)));
assertEquals(SURFACE_FIRST_FREE_Y, access.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z));
assertEquals(FLOOR_FIRST_FREE_Y, access.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, x, z));
assertNull(access.getBlockStateIfLoaded(new BlockPos(x, FLOOR_FIRST_FREE_Y, z)));
assertNull(access.getChunk(generationCenter.x(), generationCenter.z() + 2, ChunkStatus.FEATURES, false));
assertNull(access.getChunkIfLoadedImmediately(
generationCenter.x(), generationCenter.z() + 2));
List<BlockState> streamed = access.getBlockStates(new AABB(
x, FLOOR_FIRST_FREE_Y, z, x, FLOOR_FIRST_FREE_Y, z)).toList();
assertEquals(1, streamed.size());
assertSame(Blocks.WATER, streamed.getFirst().getBlock());
assertEquals(0, recording.terrainReads);
assertEquals(0, recording.heightReads);
assertEquals(0, recording.chunkReads);
}
@Test
public void distanceTwoMutationsAndSideEffectsNeverReachTheDelegate() {
RecordingDelegate recording = new RecordingDelegate();
NativeStructureWorldgenAccess access = access(recording);
ChunkPos generationCenter = generationCenter();
BlockPos position = new BlockPos(
generationCenter.getMiddleBlockX(),
FLOOR_FIRST_FREE_Y,
(generationCenter.z() + 2) << 4);
assertFalse(access.ensureCanWrite(position));
assertFalse(access.setBlock(position, Blocks.DIRT.defaultBlockState(), 2));
assertFalse(access.removeBlock(position, false));
assertFalse(access.destroyBlock(position, false));
access.updateNeighborsAt(position, Blocks.DIRT);
access.neighborShapeChanged(
Direction.NORTH, position, position.north(),
Blocks.DIRT.defaultBlockState(), 2, 512);
access.levelEvent(null, 2001, position, 0);
access.gameEvent(null, Vec3.atCenterOf(position), null);
access.addParticle(null, position.getX(), position.getY(), position.getZ(), 0, 0, 0);
access.getBlockTicks().schedule(new ScheduledTick<>(
Blocks.DIRT, position, 1L, 0L));
assertEquals(0, recording.mutations);
assertEquals(0, recording.events);
assertEquals(0, recording.blockTicks.count());
}
@Test
public void radiusOneReadsWritesAndTicksRemainUnchanged() {
RecordingDelegate recording = new RecordingDelegate();
NativeStructureWorldgenAccess access = access(recording);
ChunkPos generationCenter = generationCenter();
BlockPos position = new BlockPos(
(generationCenter.x() + 1) << 4,
FLOOR_FIRST_FREE_Y,
(generationCenter.z() - 1) << 4);
assertTrue(access.isInsideGenerationRegion(
generationCenter.x() + 1, generationCenter.z() - 1));
assertSame(Blocks.DIRT, access.getBlockState(position).getBlock());
assertEquals(91, access.getHeight(
Heightmap.Types.WORLD_SURFACE_WG, position.getX(), position.getZ()));
assertTrue(access.ensureCanWrite(position));
assertTrue(access.setBlock(position, Blocks.STONE.defaultBlockState(), 2));
assertTrue(access.removeBlock(position, false));
assertTrue(access.destroyBlock(position, false));
access.getBlockTicks().schedule(new ScheduledTick<>(
Blocks.DIRT, position, 1L, 0L));
assertEquals(1, recording.terrainReads);
assertEquals(1, recording.heightReads);
assertEquals(4, recording.mutations);
assertEquals(1, recording.blockTicks.count());
}
@Test
public void virtualizedHeightsHideInRegionTerrainFromTheDelegate() {
RecordingDelegate recording = new RecordingDelegate();
ChunkPos generationCenter = generationCenter();
NativeStructureWorldgenAccess access = NativeStructureWorldgenAccess.create(
recording.world(), generationCenter,
(x, z) -> SURFACE_FIRST_FREE_Y,
(x, z) -> FLOOR_FIRST_FREE_Y,
true,
position -> false);
int x = generationCenter.getMiddleBlockX();
int z = generationCenter.getMiddleBlockZ();
assertEquals(SURFACE_FIRST_FREE_Y, access.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z));
assertEquals(FLOOR_FIRST_FREE_Y, access.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, x, z));
assertEquals(0, recording.heightReads);
}
@Test
public void protectedCellsRemainReadableButRejectWritesTilesAndTicks() {
RecordingDelegate recording = new RecordingDelegate();
BlockPos position = generationCenter().getMiddleBlockPosition(70);
NativeStructureWorldgenAccess access = NativeStructureWorldgenAccess.create(
recording.world(), generationCenter(),
(x, z) -> SURFACE_FIRST_FREE_Y,
(x, z) -> FLOOR_FIRST_FREE_Y,
false,
position::equals);
assertSame(Blocks.DIRT, access.getBlockState(position).getBlock());
assertFalse(access.ensureCanWrite(position));
assertFalse(access.setBlock(position, Blocks.STONE.defaultBlockState(), 2));
assertFalse(access.removeBlock(position, false));
assertFalse(access.destroyBlock(position, false));
assertNull(access.getBlockEntity(position));
assertTrue(access.getBlockEntity(position, null).isEmpty());
access.getBlockTicks().schedule(new ScheduledTick<>(Blocks.DIRT, position, 1L, 0L));
access.updateNeighborsAt(position.east(), Blocks.DIRT);
access.neighborShapeChanged(
Direction.EAST, position, position.east(), Blocks.DIRT.defaultBlockState(), 2, 512);
assertEquals(1, recording.terrainReads);
assertEquals(0, recording.mutations);
assertEquals(0, recording.blockTicks.count());
assertTrue(access.setBlock(position.above(), Blocks.STONE.defaultBlockState(), 2));
assertEquals(1, recording.mutations);
}
@Test
public void statuslessChunkReadsPreserveWorldgenDelegateSemantics() {
RecordingDelegate recording = new RecordingDelegate();
NativeStructureWorldgenAccess access = access(recording);
ChunkPos generationCenter = generationCenter();
assertNull(access.getChunk(generationCenter.getWorldPosition()));
assertEquals(1, recording.statuslessChunkReads);
assertEquals(0, recording.statusChunkReads);
}
@Test
public void entityQueriesRejectDisjointAreasAndClampOverlaps() {
RecordingDelegate recording = new RecordingDelegate();
NativeStructureWorldgenAccess access = access(recording);
ChunkPos generationCenter = generationCenter();
int safeMaxX = generationCenter.getMaxBlockX() + 17;
int safeMinZ = generationCenter.getMinBlockZ() - 16;
int safeMaxZ = generationCenter.getMaxBlockZ() + 17;
AABB disjoint = new AABB(
safeMaxX, -64, safeMinZ,
safeMaxX + 16, 320, safeMaxZ);
AABB overlap = new AABB(
safeMaxX - 8, -128, safeMinZ - 8,
safeMaxX + 16, 400, safeMaxZ + 8);
EntityTypeTest<Entity, Entity> type = EntityTypeTest.forClass(Entity.class);
assertTrue(access.getEntities((Entity) null, disjoint, entity -> true).isEmpty());
assertTrue(access.getEntities(type, disjoint, entity -> true).isEmpty());
access.getEntities((Entity) null, overlap, entity -> true);
access.getEntities(type, overlap, entity -> true);
assertEquals(2, recording.entityAreas.size());
for (AABB delegatedArea : recording.entityAreas) {
assertEquals(safeMaxX - 8, delegatedArea.minX, 0D);
assertEquals(-64, delegatedArea.minY, 0D);
assertEquals(safeMinZ, delegatedArea.minZ, 0D);
assertEquals(safeMaxX, delegatedArea.maxX, 0D);
assertEquals(320, delegatedArea.maxY, 0D);
assertEquals(safeMaxZ, delegatedArea.maxZ, 0D);
}
}
private static NativeStructureWorldgenAccess access(RecordingDelegate recording) {
return NativeStructureWorldgenAccess.create(
recording.world(), generationCenter(),
(x, z) -> SURFACE_FIRST_FREE_Y,
(x, z) -> FLOOR_FIRST_FREE_Y,
false,
position -> false);
}
private static ChunkPos generationCenter() {
return new ChunkPos(GENERATION_CENTER_X, GENERATION_CENTER_Z);
}
private static final class RecordingDelegate implements InvocationHandler {
private final Holder<Biome> biome;
private final BiomeManager biomeManager;
private final RecordingTicks<Block> blockTicks;
private final RecordingTicks<Fluid> fluidTicks;
private final List<AABB> entityAreas;
private int terrainReads;
private int heightReads;
private int chunkReads;
private int statuslessChunkReads;
private int statusChunkReads;
private int mutations;
private int events;
private boolean rejectDistantTerrainReads;
private RecordingDelegate() {
this.biome = Holder.direct((Biome) null);
this.biomeManager = new BiomeManager((x, y, z) -> biome, 13L);
this.blockTicks = new RecordingTicks<>();
this.fluidTicks = new RecordingTicks<>();
this.entityAreas = new ArrayList<>();
}
private WorldGenLevel world() {
return (WorldGenLevel) Proxy.newProxyInstance(
WorldGenLevel.class.getClassLoader(),
new Class<?>[]{WorldGenLevel.class}, this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] arguments) {
String name = method.getName();
if (name.equals("getSeaLevel")) {
return 63;
}
if (name.equals("getBiome")) {
return biome;
}
if (name.equals("getBiomeManager")) {
return biomeManager;
}
if (name.equals("getBlockTicks")) {
return blockTicks;
}
if (name.equals("getFluidTicks")) {
return fluidTicks;
}
if (name.equals("getMinY")) {
return -64;
}
if (name.equals("getHeight") && arguments == null) {
return 384;
}
if (name.equals("getHeight")) {
if (rejectDistantTerrainReads
&& (Math.abs(((int) arguments[1] >> 4) - GENERATION_CENTER_X) > 1
|| Math.abs(((int) arguments[2] >> 4) - GENERATION_CENTER_Z) > 1)) {
throw new IllegalStateException("Requested chunk unavailable during world generation");
}
heightReads++;
return 91;
}
if (name.equals("getBlockState")) {
terrainReads++;
return Blocks.DIRT.defaultBlockState();
}
if (name.equals("getFluidState")) {
terrainReads++;
return Blocks.WATER.defaultBlockState().getFluidState();
}
if (name.equals("getChunk")) {
chunkReads++;
if (arguments.length == 2) {
statuslessChunkReads++;
} else {
statusChunkReads++;
}
return null;
}
if (name.equals("getChunkIfLoadedImmediately")) {
chunkReads++;
return null;
}
if (name.equals("getEntities")) {
entityAreas.add((AABB) arguments[1]);
return List.of();
}
if (name.equals("ensureCanWrite") || name.equals("setBlock")
|| name.equals("removeBlock") || name.equals("destroyBlock")) {
mutations++;
return true;
}
if (name.equals("updateNeighborsAt") || name.equals("neighborShapeChanged")) {
mutations++;
return null;
}
if (name.equals("levelEvent") || name.equals("gameEvent") || name.equals("addParticle")) {
events++;
return null;
}
if (name.equals("hashCode")) {
return System.identityHashCode(proxy);
}
if (name.equals("equals")) {
return proxy == arguments[0];
}
if (name.equals("toString")) {
return "native-structure-worldgen-test-delegate";
}
throw new UnsupportedOperationException(method.toString());
}
}
private static final class RecordingTicks<T> implements LevelTickAccess<T> {
private int count;
@Override
public void schedule(ScheduledTick<T> tick) {
count++;
}
@Override
public boolean hasScheduledTick(BlockPos position, T type) {
return false;
}
@Override
public int count() {
return count;
}
@Override
public boolean willTickThisTick(BlockPos position, T type) {
return false;
}
}
}
@@ -0,0 +1,100 @@
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import net.minecraft.server.level.ServerLevel;
import org.bukkit.craftbukkit.CraftWorld;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class NmsChunkIOFlushTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void nativeDrainCanWaitOffOwnerUntilChunkPoiAndEntityIoFinish() throws Exception {
Fixture fixture = new Fixture();
AtomicBoolean chunkPending = new AtomicBoolean(true);
AtomicBoolean poiPending = new AtomicBoolean(true);
AtomicBoolean entityPending = new AtomicBoolean(true);
CountDownLatch chunkDrain = new CountDownLatch(1);
CountDownLatch poiDrain = new CountDownLatch(1);
CountDownLatch entityDrain = new CountDownLatch(1);
when(fixture.chunks.hasTasks()).thenAnswer(invocation -> {
chunkDrain.countDown();
return chunkPending.get();
});
when(fixture.pois.hasTasks()).thenAnswer(invocation -> {
poiDrain.countDown();
return poiPending.get();
});
when(fixture.entities.hasTasks()).thenAnswer(invocation -> {
entityDrain.countDown();
return entityPending.get();
});
ExecutorService io = Executors.newSingleThreadExecutor();
try {
Future<?> drain = io.submit(() -> fixture.binding.flushChunkIO(fixture.world));
assertTrue(chunkDrain.await(5L, TimeUnit.SECONDS));
assertFalse(drain.isDone());
chunkPending.set(false);
assertTrue(poiDrain.await(5L, TimeUnit.SECONDS));
assertFalse(drain.isDone());
poiPending.set(false);
assertTrue(entityDrain.await(5L, TimeUnit.SECONDS));
assertFalse(drain.isDone());
entityPending.set(false);
drain.get(5L, TimeUnit.SECONDS);
} finally {
chunkPending.set(false);
poiPending.set(false);
entityPending.set(false);
io.shutdownNow();
assertTrue(io.awaitTermination(5L, TimeUnit.SECONDS));
}
}
@Test
public void nativeDrainFailurePropagatesToItsCompletionReceipt() {
Fixture fixture = new Fixture();
IllegalStateException failure = new IllegalStateException("native IO failed");
when(fixture.chunks.hasTasks()).thenThrow(failure);
assertSame(failure, assertThrows(IllegalStateException.class,
() -> fixture.binding.flushChunkIO(fixture.world)));
}
private static final class Fixture {
private final NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS);
private final CraftWorld world = mock(CraftWorld.class);
private final ServerLevel level = mock(ServerLevel.class);
private final RegionDataController chunks = mock(RegionDataController.class);
private final RegionDataController pois = mock(RegionDataController.class);
private final RegionDataController entities = mock(RegionDataController.class);
private Fixture() {
when(world.getHandle()).thenReturn(level);
when(level.moonrise$getChunkDataController()).thenReturn(chunks);
when(level.moonrise$getPoiChunkDataController()).thenReturn(pois);
when(level.moonrise$getEntityChunkDataController()).thenReturn(entities);
}
}
}
@@ -0,0 +1,95 @@
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
import art.arcane.volmlib.util.scheduling.FoliaScheduler;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerChunkCache;
import net.minecraft.server.level.ServerLevel;
import org.bukkit.Bukkit;
import org.bukkit.Server;
import org.bukkit.craftbukkit.CraftWorld;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.util.concurrent.CompletableFuture;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
public class NmsChunkTaskPollingTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void serverOwnerPumpsOnlyTheTargetWorldQueue() {
Server bukkitServer = mock(Server.class);
CraftWorld world = mock(CraftWorld.class);
ServerLevel level = mock(ServerLevel.class);
MinecraftServer server = mock(MinecraftServer.class);
ServerChunkCache chunks = mock(ServerChunkCache.class);
NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS);
CompletableFuture<Void> completed = new CompletableFuture<>();
when(world.getHandle()).thenReturn(level);
when(level.getServer()).thenReturn(server);
when(server.getRunningThread()).thenReturn(Thread.currentThread());
when(level.getChunkSource()).thenReturn(chunks);
when(chunks.pollTask()).thenAnswer(invocation -> completed.complete(null)).thenReturn(false);
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<FoliaScheduler> folia = mockStatic(FoliaScheduler.class)) {
bukkit.when(Bukkit::getServer).thenReturn(bukkitServer);
folia.when(() -> FoliaScheduler.isRegionizedRuntime(bukkitServer)).thenReturn(false);
assertTrue(binding.pollChunkTask(world));
assertTrue(completed.isDone());
assertFalse(binding.pollChunkTask(world));
}
verify(chunks, times(2)).pollTask();
verify(server, never()).executeAllRecentInternalTasks();
}
@Test
public void nonOwnerIsRejectedBeforeAccessingTheChunkQueue() {
Server bukkitServer = mock(Server.class);
CraftWorld world = mock(CraftWorld.class);
ServerLevel level = mock(ServerLevel.class);
MinecraftServer server = mock(MinecraftServer.class);
NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS);
when(world.getHandle()).thenReturn(level);
when(level.getServer()).thenReturn(server);
when(server.getRunningThread()).thenReturn(new Thread("Other server owner"));
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<FoliaScheduler> folia = mockStatic(FoliaScheduler.class)) {
bukkit.when(Bukkit::getServer).thenReturn(bukkitServer);
folia.when(() -> FoliaScheduler.isRegionizedRuntime(bukkitServer)).thenReturn(false);
assertThrows(IllegalStateException.class, () -> binding.pollChunkTask(world));
}
verify(level, never()).getChunkSource();
}
@Test
public void foliaNeverUsesTheWorldWideTaskPump() {
Server bukkitServer = mock(Server.class);
CraftWorld world = mock(CraftWorld.class);
NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS);
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<FoliaScheduler> folia = mockStatic(FoliaScheduler.class)) {
bukkit.when(Bukkit::getServer).thenReturn(bukkitServer);
folia.when(() -> FoliaScheduler.isRegionizedRuntime(bukkitServer)).thenReturn(true);
assertFalse(binding.pollChunkTask(world));
}
verifyNoInteractions(world);
}
}
@@ -0,0 +1,103 @@
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
import net.minecraft.SharedConstants;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.Identifier;
import net.minecraft.server.Bootstrap;
import org.bukkit.Difficulty;
import org.bukkit.Location;
import org.bukkit.craftbukkit.CraftWorld;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.event.entity.CreatureSpawnEvent;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class NmsEntitySpawnDifficultyTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void peacefulRejectsEveryNativeForbiddenTypeBeforeBukkitCreatesIt() {
CraftWorld world = mock(CraftWorld.class);
when(world.getDifficulty()).thenReturn(Difficulty.PEACEFUL);
NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS);
Location location = new Location(world, 0.5, 80, 0.5);
int rejected = 0;
for (EntityType type : EntityType.values()) {
if (type.getEntityClass() == null) {
continue;
}
if (BuiltInRegistries.ENTITY_TYPE.getValue(Identifier.parse(type.getKey().toString())).isAllowedInPeaceful()) {
continue;
}
assertNull(type.name(), binding.spawnEntity(location, type, CreatureSpawnEvent.SpawnReason.NATURAL));
rejected++;
}
assertTrue(rejected > 20);
verify(world, never()).spawn(any(Location.class), any(Class.class), isNull(), any(CreatureSpawnEvent.SpawnReason.class));
}
@Test
public void peacefulPreservesPassiveAndNativeMonsterCategoryExceptions() {
CraftWorld world = mock(CraftWorld.class);
when(world.getDifficulty()).thenReturn(Difficulty.PEACEFUL);
NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS);
Location location = new Location(world, 0.5, 80, 0.5);
Entity created = mock(Entity.class);
doReturn(created).when(world).spawn(eq(location), any(Class.class), isNull(), eq(CreatureSpawnEvent.SpawnReason.NATURAL));
for (EntityType type : List.of(EntityType.COW, EntityType.VILLAGER, EntityType.WOLF, EntityType.PIGLIN, EntityType.SHULKER)) {
assertSame(type.name(), created, binding.spawnEntity(location, type, CreatureSpawnEvent.SpawnReason.NATURAL));
}
}
@Test
public void nonPeacefulDifficultiesPreserveHostileSpawns() {
CraftWorld world = mock(CraftWorld.class);
NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS);
Location location = new Location(world, 0.5, 80, 0.5);
Entity created = mock(Entity.class);
doReturn(created).when(world).spawn(eq(location), any(Class.class), isNull(), eq(CreatureSpawnEvent.SpawnReason.NATURAL));
for (Difficulty difficulty : List.of(Difficulty.EASY, Difficulty.NORMAL, Difficulty.HARD)) {
when(world.getDifficulty()).thenReturn(difficulty);
assertSame(created, binding.spawnEntity(location, EntityType.ZOMBIE, CreatureSpawnEvent.SpawnReason.NATURAL));
}
}
@Test
public void unrelatedBukkitSpawnFailuresStillPropagate() {
CraftWorld world = mock(CraftWorld.class);
when(world.getDifficulty()).thenReturn(Difficulty.PEACEFUL);
NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS);
Location location = new Location(world, 0.5, 80, 0.5);
IllegalStateException failure = new IllegalStateException("Spawn rejected by platform");
doThrow(failure).when(world).spawn(eq(location), any(Class.class), isNull(), eq(CreatureSpawnEvent.SpawnReason.NATURAL));
assertSame(failure, assertThrows(IllegalStateException.class,
() -> binding.spawnEntity(location, EntityType.COW, CreatureSpawnEvent.SpawnReason.NATURAL)));
}
}
@@ -0,0 +1,114 @@
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
import art.arcane.iris.world.history.SavedTerrainChunk;
import ca.spottedleaf.concurrentutil.util.Priority;
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.ChunkPos;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.io.IOException;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
public class NmsSavedTerrainCheckpointTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void checkpointWaitsForDirtyAndUnloadedChunkWritesBeforeFlushAndVerification() throws Exception {
ServerLevel level = mock(ServerLevel.class);
Path root = Path.of("checkpoint-world");
byte[] receipt = new byte[]{1, 2, 3};
NmsSavedTerrainCapture.Checkpoint saved = new NmsSavedTerrainCapture.Checkpoint(new ChunkPos(4, -5),
new NmsSavedTerrainCapture.Verification("minecraft:full", receipt, 7L));
NmsSavedTerrainCapture.Checkpoint unloaded = NmsSavedTerrainCapture.checkpoint(-8, 9, null);
Set<ChunkPos> completed = new HashSet<>();
AtomicBoolean flushed = new AtomicBoolean();
try (MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class);
MockedStatic<SavedTerrainChunk> terrain = mockStatic(SavedTerrainChunk.class)) {
io.when(() -> MoonriseRegionFileIO.getPriority(level, 4, -5, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))
.thenAnswer(invocation -> {
completed.add(saved.chunk());
return Priority.COMPLETING;
});
io.when(() -> MoonriseRegionFileIO.getPriority(level, -8, 9, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))
.thenAnswer(invocation -> {
completed.add(unloaded.chunk());
return Priority.COMPLETING;
});
io.when(() -> MoonriseRegionFileIO.flushRegionStorages(level, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))
.thenAnswer(invocation -> {
assertEquals(Set.of(saved.chunk(), unloaded.chunk()), completed);
flushed.set(true);
return null;
});
terrain.when(() -> SavedTerrainChunk.verifyCheckpoint(root, 4, -5, "minecraft:full", receipt, 7L))
.thenAnswer(invocation -> {
assertTrue(flushed.get());
return null;
});
NmsSavedTerrainCapture.finishCheckpoint(level, root, List.of(saved, unloaded));
terrain.verify(() -> SavedTerrainChunk.verifyCheckpoint(root, 4, -5, "minecraft:full", receipt, 7L));
io.verify(() -> MoonriseRegionFileIO.flush(level, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA), never());
io.verify(() -> MoonriseRegionFileIO.getPriority(level, 100, 100, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA), never());
terrain.verifyNoMoreInteractions();
}
}
@Test
public void storageFlushFailureCannotCompleteTheCheckpoint() throws Exception {
ServerLevel level = mock(ServerLevel.class);
IOException failure = new IOException("storage flush failed");
try (MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class);
MockedStatic<SavedTerrainChunk> terrain = mockStatic(SavedTerrainChunk.class)) {
io.when(() -> MoonriseRegionFileIO.flushRegionStorages(level, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))
.thenThrow(failure);
assertSame(failure, assertThrows(IOException.class,
() -> NmsSavedTerrainCapture.finishCheckpoint(level, Path.of("checkpoint-world"), List.of())));
terrain.verifyNoInteractions();
}
}
@Test
public void persistedReceiptMismatchCannotCompleteTheCheckpoint() throws Exception {
ServerLevel level = mock(ServerLevel.class);
Path root = Path.of("checkpoint-world");
byte[] receipt = new byte[]{4, 5};
IOException failure = new IOException("saved terrain receipt mismatch");
NmsSavedTerrainCapture.Checkpoint saved = new NmsSavedTerrainCapture.Checkpoint(new ChunkPos(1, 2),
new NmsSavedTerrainCapture.Verification("minecraft:noise", receipt, 3L));
try (MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class);
MockedStatic<SavedTerrainChunk> terrain = mockStatic(SavedTerrainChunk.class)) {
io.when(() -> MoonriseRegionFileIO.getPriority(level, 1, 2, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))
.thenReturn(Priority.COMPLETING);
terrain.when(() -> SavedTerrainChunk.verifyCheckpoint(root, 1, 2, "minecraft:noise", receipt, 3L))
.thenThrow(failure);
assertSame(failure, assertThrows(IOException.class,
() -> NmsSavedTerrainCapture.finishCheckpoint(level, root, List.of(saved))));
io.verify(() -> MoonriseRegionFileIO.flushRegionStorages(level, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA));
}
}
}
@@ -0,0 +1,223 @@
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
import art.arcane.iris.world.task.J;
import ca.spottedleaf.concurrentutil.completable.Completable;
import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor;
import ca.spottedleaf.concurrentutil.executor.thread.BalancedPrioritisedThreadPool;
import ca.spottedleaf.concurrentutil.util.Priority;
import ca.spottedleaf.moonrise.common.PlatformHooks;
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager;
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder;
import net.minecraft.SharedConstants;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.server.Bootstrap;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.storage.SerializableChunkData;
import org.bukkit.World;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class NmsSavedTerrainSnapshotTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void ownerReservesSaveOrderBeforeOffOwnerSerializationAndLaterAutosave() throws Exception {
try (SaveFixture fixture = new SaveFixture();
ExecutorService checkpoint = Executors.newSingleThreadExecutor();
MockedStatic<SerializableChunkData> snapshots = mockStatic(SerializableChunkData.class);
MockedStatic<PlatformHooks> hooks = mockStatic(PlatformHooks.class);
MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class)) {
Thread owner = Thread.currentThread();
ChunkAccess chunk = mock(ChunkAccess.class);
SerializableChunkData snapshot = mock(SerializableChunkData.class);
PlatformHooks platform = mock(PlatformHooks.class);
CompoundTag capturedData = new CompoundTag();
CompoundTag autosave = new CompoundTag();
List<String> submissions = new ArrayList<>();
when(chunk.getPos()).thenReturn(new ChunkPos(4, -9));
snapshots.when(() -> SerializableChunkData.copyOf(fixture.level, chunk)).thenAnswer(invocation -> {
assertSame(owner, Thread.currentThread());
return snapshot;
});
hooks.when(PlatformHooks::get).thenReturn(platform);
doAnswer(invocation -> {
assertSame(owner, Thread.currentThread());
return null;
}).when(platform).chunkSyncSave(fixture.level, chunk, snapshot);
when(snapshot.write()).thenAnswer(invocation -> {
assertNotSame(owner, Thread.currentThread());
assertEquals(List.of("checkpoint", "autosave"), submissions);
return capturedData;
});
io.when(() -> MoonriseRegionFileIO.scheduleSave(eq(fixture.level), eq(4), eq(-9),
any(Completable.class), any(PrioritisedExecutor.PrioritisedTask.class),
eq(MoonriseRegionFileIO.RegionFileType.CHUNK_DATA), eq(Priority.NORMAL)))
.thenAnswer(invocation -> {
assertSame(owner, Thread.currentThread());
submissions.add("checkpoint");
return null;
});
io.when(() -> MoonriseRegionFileIO.scheduleSave(fixture.level, 4, -9, autosave,
MoonriseRegionFileIO.RegionFileType.CHUNK_DATA)).thenAnswer(invocation -> {
submissions.add("autosave");
return null;
});
NmsSavedTerrainCapture.CapturedChunk captured = NmsSavedTerrainCapture.scheduleSnapshot(fixture.level, chunk);
assertEquals(List.of("checkpoint"), submissions);
assertFalse(captured.data().isDone());
verify(snapshot, never()).write();
verify(platform).chunkSyncSave(fixture.level, chunk, snapshot);
MoonriseRegionFileIO.scheduleSave(fixture.level, 4, -9, autosave,
MoonriseRegionFileIO.RegionFileType.CHUNK_DATA);
Future<CompoundTag> result = checkpoint.submit(() -> NmsSavedTerrainCapture.awaitSnapshot(
fixture.level, captured, System.nanoTime() + TimeUnit.SECONDS.toNanos(3)));
assertSame(capturedData, result.get(5, TimeUnit.SECONDS));
verify(snapshot).write();
}
}
@Test
public void failedOffOwnerSerializationCannotSucceedAsACheckpoint() throws Exception {
try (SaveFixture fixture = new SaveFixture();
ExecutorService checkpoint = Executors.newSingleThreadExecutor();
MockedStatic<SerializableChunkData> snapshots = mockStatic(SerializableChunkData.class);
MockedStatic<PlatformHooks> hooks = mockStatic(PlatformHooks.class);
MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class)) {
ChunkAccess chunk = mock(ChunkAccess.class);
SerializableChunkData snapshot = mock(SerializableChunkData.class);
IllegalStateException failure = new IllegalStateException("serialization failed");
when(chunk.getPos()).thenReturn(new ChunkPos(4, -9));
snapshots.when(() -> SerializableChunkData.copyOf(fixture.level, chunk)).thenReturn(snapshot);
hooks.when(PlatformHooks::get).thenReturn(mock(PlatformHooks.class));
when(snapshot.write()).thenThrow(failure);
NmsSavedTerrainCapture.CapturedChunk captured = NmsSavedTerrainCapture.scheduleSnapshot(fixture.level, chunk);
Future<IOException> result = checkpoint.submit(() -> assertThrows(IOException.class,
() -> NmsSavedTerrainCapture.awaitSnapshot(fixture.level, captured,
System.nanoTime() + TimeUnit.SECONDS.toNanos(3))));
assertSame(failure, result.get(5, TimeUnit.SECONDS).getCause());
assertTrue(captured.data().isCompletedExceptionally());
}
}
@Test
public void checkpointBoundsOwnerRequestsUntilPriorNativeWritesFinish() throws Exception {
try (SaveFixture fixture = new SaveFixture();
ExecutorService owner = Executors.newSingleThreadExecutor();
ExecutorService checkpoint = Executors.newSingleThreadExecutor()) {
World world = mock(World.class);
when(world.getWorldFolder()).thenReturn(new File("checkpoint-world"));
List<NewChunkHolder> holders = new ArrayList<>(129);
for (int index = 0; index < 129; index++) {
NewChunkHolder holder = mock(NewChunkHolder.class);
setField(holder, "chunkX", index);
holders.add(holder);
}
AtomicInteger requested = new AtomicInteger();
AtomicBoolean writesBlocked = new AtomicBoolean(true);
CountDownLatch firstBatchWaiting = new CountDownLatch(1);
Future<?> result = checkpoint.submit(() -> {
try (MockedStatic<J> scheduling = mockStatic(J.class);
MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class)) {
scheduling.when(() -> J.runRegionFuture(eq(world), anyInt(), anyInt(), any(Runnable.class)))
.thenAnswer(invocation -> {
requested.incrementAndGet();
Runnable action = invocation.getArgument(3);
return CompletableFuture.runAsync(action, owner);
});
io.when(() -> MoonriseRegionFileIO.getPriority(eq(fixture.level), anyInt(), anyInt(),
eq(MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))).thenAnswer(invocation -> {
firstBatchWaiting.countDown();
return writesBlocked.get() ? Priority.NORMAL : Priority.COMPLETING;
});
NmsSavedTerrainCapture.flushSnapshots(world, fixture.level, holders);
io.verify(() -> MoonriseRegionFileIO.getPriority(fixture.level, 128, 0,
MoonriseRegionFileIO.RegionFileType.CHUNK_DATA), times(2));
return null;
}
});
try {
assertTrue(firstBatchWaiting.await(5, TimeUnit.SECONDS));
assertEquals(64, requested.get());
assertTrue(owner.submit(() -> true).get(5, TimeUnit.SECONDS));
assertFalse(result.isDone());
writesBlocked.set(false);
result.get(5, TimeUnit.SECONDS);
assertEquals(129, requested.get());
} finally {
writesBlocked.set(false);
}
}
}
private static void setField(Object instance, String name, Object value) throws Exception {
Field field = instance.getClass().getField(name);
field.setAccessible(true);
field.set(instance, value);
}
private static final class SaveFixture implements AutoCloseable {
private final BalancedPrioritisedThreadPool pool = new BalancedPrioritisedThreadPool(
TimeUnit.MILLISECONDS.toNanos(1), Executors.defaultThreadFactory());
private final ServerLevel level = mock(ServerLevel.class);
private SaveFixture() throws Exception {
BalancedPrioritisedThreadPool.OrderedStreamGroup group = pool.createOrderedStreamGroup();
ChunkTaskScheduler scheduler = mock(ChunkTaskScheduler.class);
setField(scheduler, "saveExecutor", group.createExecutor());
setField(scheduler, "compressionExecutor", group.createExecutor());
setField(scheduler, "chunkHolderManager", mock(ChunkHolderManager.class));
when(level.moonrise$getChunkTaskScheduler()).thenReturn(scheduler);
}
@Override
public void close() throws Exception {
pool.halt(true);
assertTrue(pool.joinInterruptable(5_000));
}
}
}
@@ -0,0 +1,135 @@
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
import ca.spottedleaf.concurrentutil.executor.thread.BalancedPrioritisedThreadPool;
import ca.spottedleaf.concurrentutil.util.Priority;
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.ChunkPos;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
public class NmsSavedTerrainWriteBarrierTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void checkpointHelpsSaveAndCompressionWhileNativeGenerationOccupiesTheWorker() throws Exception {
BalancedPrioritisedThreadPool pool = new BalancedPrioritisedThreadPool(
TimeUnit.MILLISECONDS.toNanos(1), Executors.defaultThreadFactory());
BalancedPrioritisedThreadPool.OrderedStreamGroup group = pool.createOrderedStreamGroup();
BalancedPrioritisedThreadPool.OrderedStreamGroup.Queue generation = group.createExecutor();
BalancedPrioritisedThreadPool.OrderedStreamGroup.Queue saves = group.createExecutor();
BalancedPrioritisedThreadPool.OrderedStreamGroup.Queue compression = group.createExecutor();
ServerLevel level = level(saves, compression);
ChunkPos chunk = new ChunkPos(4, -9);
CountDownLatch generationEntered = new CountDownLatch(1);
CountDownLatch releaseGeneration = new CountDownLatch(1);
AtomicBoolean pendingWrite = new AtomicBoolean(true);
AtomicInteger serialized = new AtomicInteger();
AtomicInteger compressed = new AtomicInteger();
AtomicReference<Thread> saveThread = new AtomicReference<>();
AtomicReference<Thread> compressionThread = new AtomicReference<>();
try (MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class)) {
io.when(() -> MoonriseRegionFileIO.getPriority(level, chunk.x(), chunk.z(),
MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))
.thenAnswer(invocation -> pendingWrite.get() ? Priority.NORMAL : Priority.COMPLETING);
generation.queueTask(() -> {
generationEntered.countDown();
try {
assertTrue(releaseGeneration.await(5, TimeUnit.SECONDS));
} catch (InterruptedException interruption) {
Thread.currentThread().interrupt();
throw new AssertionError(interruption);
}
});
pool.adjustThreadCount(1);
assertTrue(generationEntered.await(5, TimeUnit.SECONDS));
saves.queueTask(() -> {
saveThread.set(Thread.currentThread());
serialized.incrementAndGet();
compression.queueTask(() -> {
compressionThread.set(Thread.currentThread());
compressed.incrementAndGet();
pendingWrite.set(false);
});
});
NmsSavedTerrainCapture.awaitWrite(level, chunk, System.nanoTime() + TimeUnit.SECONDS.toNanos(3));
assertEquals(1L, releaseGeneration.getCount());
assertEquals(1, serialized.get());
assertEquals(1, compressed.get());
assertFalse(pendingWrite.get());
assertSame(Thread.currentThread(), saveThread.get());
assertSame(Thread.currentThread(), compressionThread.get());
} finally {
releaseGeneration.countDown();
pool.halt(true);
assertTrue(pool.joinInterruptable(5_000));
}
}
@Test
public void waitingForNativeWriteHonorsTheSharedDeadline() throws Exception {
ServerLevel level = mock(ServerLevel.class);
ChunkPos chunk = new ChunkPos(2, 3);
IOException failure = assertThrows(IOException.class,
() -> NmsSavedTerrainCapture.awaitWrite(level, chunk, System.nanoTime() - 1L));
assertTrue(failure.getMessage().contains("Timed out"));
}
@Test
public void interruptedNativeWriteWaitPreservesInterruption() throws Exception {
ServerLevel level = mock(ServerLevel.class);
ChunkPos chunk = new ChunkPos(2, 3);
Thread.currentThread().interrupt();
try {
IOException failure = assertThrows(IOException.class,
() -> NmsSavedTerrainCapture.awaitWrite(level, chunk,
System.nanoTime() + TimeUnit.SECONDS.toNanos(3)));
assertTrue(failure.getMessage().contains("Interrupted"));
assertTrue(Thread.currentThread().isInterrupted());
} finally {
Thread.interrupted();
}
}
private static ServerLevel level(BalancedPrioritisedThreadPool.OrderedStreamGroup.Queue saves,
BalancedPrioritisedThreadPool.OrderedStreamGroup.Queue compression) throws Exception {
ServerLevel level = mock(ServerLevel.class);
ChunkTaskScheduler scheduler = mock(ChunkTaskScheduler.class);
Field savesField = ChunkTaskScheduler.class.getField("saveExecutor");
Field compressionField = ChunkTaskScheduler.class.getField("compressionExecutor");
savesField.setAccessible(true);
compressionField.setAccessible(true);
savesField.set(scheduler, saves);
compressionField.set(scheduler, compression);
when(level.moonrise$getChunkTaskScheduler()).thenReturn(scheduler);
return level;
}
}
@@ -0,0 +1,345 @@
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
import art.arcane.iris.nativegen.v26_3_R1.NativeBlockTags;
import art.arcane.iris.nativegen.v26_3_R1.WorldgenTerrainHeightmaps;
import com.mojang.serialization.Codec;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderSet;
import net.minecraft.core.IdMapper;
import net.minecraft.server.Bootstrap;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.LevelChunkSection;
import net.minecraft.world.level.chunk.PalettedContainer;
import net.minecraft.world.level.chunk.PalettedContainerFactory;
import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.chunk.Strategy;
import net.minecraft.world.level.chunk.UpgradeData;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
import net.minecraft.world.level.levelgen.structure.structures.IglooPieces;
import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece;
import net.minecraft.world.level.levelgen.structure.structures.SwampHutStructure;
import org.junit.BeforeClass;
import org.junit.Test;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.IntBinaryOperator;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class WorldgenTerrainHeightmapsTest {
private static final int MIN_Y = -64;
private static final int HEIGHT = 384;
private static final int LAND_TERRAIN_TOP = 70;
private static final int LAND_CANOPY_TOP = 80;
private static final int OCEAN_FLOOR_TOP = 60;
private static final int OCEAN_FLUID_TOP = 64;
private static final int LAND_X = 3;
private static final int LAND_Z = 5;
private static final int OCEAN_X = 8;
private static final int OCEAN_Z = 8;
private static final int TERRAIN_SLAB_DEPTH = 4;
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
NativeBlockTags.bindHeightmapFixtures();
}
@Test
public void authoringHeightmapsReadPlacedObjectsAndFluidsWithoutTerrainResolvers() {
ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0));
IrisChunkGenerator.primeAuthoringHeightmaps(chunk);
assertEquals(LAND_CANOPY_TOP, chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
assertEquals(LAND_CANOPY_TOP, chunk.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, LAND_X, LAND_Z));
assertEquals(LAND_CANOPY_TOP, chunk.getHeight(Heightmap.Types.MOTION_BLOCKING, LAND_X, LAND_Z));
assertEquals(LAND_TERRAIN_TOP + 6,
chunk.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, LAND_X, LAND_Z));
assertEquals(OCEAN_FLUID_TOP, chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, OCEAN_X, OCEAN_Z));
assertEquals(OCEAN_FLOOR_TOP, chunk.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, OCEAN_X, OCEAN_Z));
}
@Test
public void primeTerrainWritesEngineSurfaceAndOceanFloorHeights() {
ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0));
assertFalse(chunk.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG));
assertFalse(chunk.hasPrimedHeightmap(Heightmap.Types.OCEAN_FLOOR_WG));
WorldgenTerrainHeightmaps.primeTerrain(chunk, surfaceFirstFreeY(), floorFirstFreeY());
assertTrue(chunk.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG));
assertTrue(chunk.hasPrimedHeightmap(Heightmap.Types.OCEAN_FLOOR_WG));
assertEquals(LAND_TERRAIN_TOP, chunk.getHeight(
Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
assertEquals(LAND_TERRAIN_TOP, chunk.getHeight(
Heightmap.Types.OCEAN_FLOOR_WG, LAND_X, LAND_Z));
assertEquals(OCEAN_FLUID_TOP, chunk.getHeight(
Heightmap.Types.WORLD_SURFACE_WG, OCEAN_X, OCEAN_Z));
assertEquals(OCEAN_FLOOR_TOP, chunk.getHeight(
Heightmap.Types.OCEAN_FLOOR_WG, OCEAN_X, OCEAN_Z));
}
@Test
public void finalHeightmapPrimingDoesNotClobberTheWorldgenHeightmaps() {
ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0));
WorldgenTerrainHeightmaps.primeTerrain(chunk, surfaceFirstFreeY(), floorFirstFreeY());
Heightmap.primeHeightmaps(chunk, ChunkStatus.FINAL_HEIGHTMAPS);
assertEquals(LAND_TERRAIN_TOP, chunk.getHeight(
Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
assertEquals(LAND_TERRAIN_TOP, chunk.getHeight(
Heightmap.Types.OCEAN_FLOOR_WG, LAND_X, LAND_Z));
assertEquals(LAND_CANOPY_TOP, chunk.getHeight(
Heightmap.Types.WORLD_SURFACE, LAND_X, LAND_Z));
}
@Test
public void unprimedWorldgenHeightmapResolvesTheIrisCanopyInstead() {
ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0));
assertEquals(LAND_CANOPY_TOP, chunk.getHeight(
Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
}
@Test
public void primeTerrainOverwritesAnAlreadyLazyPrimedCanopyHeight() {
ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0));
assertEquals(LAND_CANOPY_TOP, chunk.getHeight(
Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
WorldgenTerrainHeightmaps.primeTerrain(chunk, surfaceFirstFreeY(), floorFirstFreeY());
assertEquals(LAND_TERRAIN_TOP, chunk.getHeight(
Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
}
@Test
public void structurePlacementPrimesTheWestNeighbourAnIglooEntranceReads() {
Map<Long, ChunkAccess> chunks = new HashMap<>();
chunks.put(ChunkPos.pack(0, 0), terrainChunk(new ChunkPos(0, 0)));
chunks.put(ChunkPos.pack(-1, 0), terrainChunk(new ChunkPos(-1, 0)));
WorldGenLevel world = world(chunks);
StructureStart start = startInOrigin();
int canopySnap = IglooPieces.GENERATION_HEIGHT + iglooSnapOffset(world);
WorldgenTerrainHeightmaps.primeStructurePlacement(
world, new ChunkPos(0, 0), List.of(start), surfaceFirstFreeY(), floorFirstFreeY());
int terrainSnap = IglooPieces.GENERATION_HEIGHT + iglooSnapOffset(world);
assertEquals(LAND_TERRAIN_TOP + 1, world.getHeight(
Heightmap.Types.WORLD_SURFACE_WG, -2, 5));
assertEquals(LAND_TERRAIN_TOP, terrainSnap);
assertEquals(LAND_CANOPY_TOP, canopySnap);
assertEquals(LAND_CANOPY_TOP - LAND_TERRAIN_TOP, canopySnap - terrainSnap);
}
@Test
public void structurePlacementPrimesEveryChunkTheStartFootprintTouches() {
Map<Long, ChunkAccess> chunks = new HashMap<>();
for (int chunkX = -1; chunkX <= 1; chunkX++) {
for (int chunkZ = -1; chunkZ <= 1; chunkZ++) {
chunks.put(ChunkPos.pack(chunkX, chunkZ), terrainChunk(new ChunkPos(chunkX, chunkZ)));
}
}
WorldGenLevel world = world(chunks);
WorldgenTerrainHeightmaps.primeStructurePlacement(
world, new ChunkPos(0, 0), List.of(startInOrigin()),
surfaceFirstFreeY(), floorFirstFreeY());
for (ChunkAccess chunk : chunks.values()) {
assertTrue(chunk.getPos().toString(),
chunk.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG));
assertTrue(chunk.getPos().toString(),
chunk.hasPrimedHeightmap(Heightmap.Types.OCEAN_FLOOR_WG));
assertEquals(LAND_TERRAIN_TOP, chunk.getHeight(
Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
}
}
@Test
public void structurePlacementDoesNotPrimeALoadedDistanceTwoChunk() {
Map<Long, ChunkAccess> chunks = new HashMap<>();
ProtoChunk origin = terrainChunk(new ChunkPos(0, 0));
ProtoChunk neighbour = terrainChunk(new ChunkPos(1, 0));
ProtoChunk distanceTwo = terrainChunk(new ChunkPos(2, 0));
chunks.put(ChunkPos.pack(0, 0), origin);
chunks.put(ChunkPos.pack(1, 0), neighbour);
chunks.put(ChunkPos.pack(2, 0), distanceTwo);
WorldGenLevel world = world(chunks);
StructureStart shifted = startInOrigin();
shifted.getPieces().forEach(piece -> piece.move(16, 0, 0));
WorldgenTerrainHeightmaps.primeStructurePlacement(
world, new ChunkPos(0, 0), List.of(shifted),
surfaceFirstFreeY(), floorFirstFreeY());
assertTrue(origin.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG));
assertTrue(neighbour.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG));
assertFalse(distanceTwo.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG));
}
@Test
public void structurePlacementSkipsChunksOutsideTheGenerationRegion() {
Map<Long, ChunkAccess> chunks = new HashMap<>();
ProtoChunk origin = terrainChunk(new ChunkPos(0, 0));
chunks.put(ChunkPos.pack(0, 0), origin);
WorldGenLevel world = world(chunks);
WorldgenTerrainHeightmaps.primeStructurePlacement(
world, new ChunkPos(0, 0), List.of(startInOrigin()),
surfaceFirstFreeY(), floorFirstFreeY());
assertTrue(origin.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG));
assertEquals(LAND_TERRAIN_TOP, origin.getHeight(
Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
}
private static int iglooSnapOffset(WorldGenLevel world) {
return world.getHeight(Heightmap.Types.WORLD_SURFACE_WG, -2, 5)
- IglooPieces.GENERATION_HEIGHT - 1;
}
private static StructureStart startInOrigin() {
Structure structure = new SwampHutStructure(new Structure.StructureSettings(HolderSet.empty()));
SwampHutPiece piece = new SwampHutPiece(RandomSource.create(13L), 0, 0);
return new StructureStart(
structure, new ChunkPos(0, 0), 0, new PiecesContainer(List.of(piece)));
}
private static IntBinaryOperator surfaceFirstFreeY() {
return (x, z) -> isOcean(x, z) ? OCEAN_FLUID_TOP + 1 : LAND_TERRAIN_TOP + 1;
}
private static IntBinaryOperator floorFirstFreeY() {
return (x, z) -> isOcean(x, z) ? OCEAN_FLOOR_TOP + 1 : LAND_TERRAIN_TOP + 1;
}
private static boolean isOcean(int x, int z) {
return Math.floorMod(x, 16) == OCEAN_X && Math.floorMod(z, 16) == OCEAN_Z;
}
private static ProtoChunk terrainChunk(ChunkPos pos) {
ProtoChunk chunk = new ProtoChunk(
pos, UpgradeData.EMPTY, LevelHeightAccessor.create(MIN_Y, HEIGHT),
containerFactory(), null);
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
boolean ocean = isOcean(pos.getMinBlockX() + x, pos.getMinBlockZ() + z);
int stoneTop = ocean ? OCEAN_FLOOR_TOP : LAND_TERRAIN_TOP;
for (int y = stoneTop - TERRAIN_SLAB_DEPTH; y <= stoneTop; y++) {
write(chunk, x, y, z, Blocks.STONE.defaultBlockState());
}
if (ocean) {
for (int y = OCEAN_FLOOR_TOP + 1; y <= OCEAN_FLUID_TOP; y++) {
write(chunk, x, y, z, Blocks.WATER.defaultBlockState());
}
continue;
}
for (int y = LAND_TERRAIN_TOP + 1; y <= LAND_TERRAIN_TOP + 6; y++) {
write(chunk, x, y, z, Blocks.SPRUCE_LOG.defaultBlockState());
}
for (int y = LAND_TERRAIN_TOP + 7; y <= LAND_CANOPY_TOP; y++) {
write(chunk, x, y, z, Blocks.SPRUCE_LEAVES.defaultBlockState());
}
}
}
return chunk;
}
private static void write(ProtoChunk chunk, int x, int y, int z, BlockState state) {
LevelChunkSection section = chunk.getSection(chunk.getSectionIndex(y));
section.setBlockState(x, y & 15, z, state, false);
}
private static PalettedContainerFactory containerFactory() {
Strategy<BlockState> blockStates = Strategy.createForBlockStates(Block.BLOCK_STATE_REGISTRY);
BlockState air = Blocks.AIR.defaultBlockState();
IdMapper<Holder<Biome>> biomeIds = new IdMapper<>();
Holder<Biome> defaultBiome = Holder.direct((Biome) null);
biomeIds.add(defaultBiome);
Strategy<Holder<Biome>> biomes = Strategy.createForBiomes(biomeIds);
Codec<Holder<Biome>> biomeCodec = Codec.STRING.xmap(
name -> defaultBiome, holder -> "iris-test-biome");
return new PalettedContainerFactory(
blockStates,
air,
PalettedContainer.codecRW(BlockState.CODEC, blockStates, air),
biomes,
defaultBiome,
PalettedContainer.codecRO(biomeCodec, biomes, defaultBiome),
PalettedContainer.codecRW(biomeCodec, biomes, defaultBiome));
}
private static WorldGenLevel world(Map<Long, ChunkAccess> chunks) {
InvocationHandler handler = (proxy, method, arguments) -> {
String methodName = method.getName();
if (methodName.equals("hasChunk")) {
return chunks.containsKey(ChunkPos.pack((int) arguments[0], (int) arguments[1]));
}
if (methodName.equals("getChunk") && arguments.length == 2) {
ChunkAccess chunk = chunks.get(ChunkPos.pack((int) arguments[0], (int) arguments[1]));
if (chunk == null) {
throw new AssertionError("Heightmap priming requested an unavailable chunk "
+ arguments[0] + "," + arguments[1]);
}
return chunk;
}
if (methodName.equals("getHeight") && arguments.length == 3) {
Heightmap.Types type = (Heightmap.Types) arguments[0];
int x = (int) arguments[1];
int z = (int) arguments[2];
ChunkAccess chunk = chunks.get(ChunkPos.pack(x >> 4, z >> 4));
if (chunk == null) {
throw new AssertionError("Heightmap read requested an unavailable chunk at "
+ x + "," + z);
}
return chunk.getHeight(type, x & 15, z & 15) + 1;
}
if (methodName.equals("getBlockState")) {
BlockPos position = (BlockPos) arguments[0];
ChunkAccess chunk = chunks.get(ChunkPos.pack(position.getX() >> 4, position.getZ() >> 4));
if (chunk == null) {
throw new AssertionError("Block read requested an unavailable chunk at " + position);
}
return chunk.getBlockState(position);
}
if (methodName.equals("hashCode")) {
return System.identityHashCode(proxy);
}
if (methodName.equals("equals")) {
return proxy == arguments[0];
}
if (methodName.equals("toString")) {
return "worldgen-heightmap-test-world";
}
throw new UnsupportedOperationException(method.toString());
};
return (WorldGenLevel) Proxy.newProxyInstance(
WorldGenLevel.class.getClassLoader(), new Class<?>[]{WorldGenLevel.class}, handler);
}
}
@@ -0,0 +1,294 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.structure;
import art.arcane.iris.structure.authoring.StructureCapability;
import art.arcane.iris.structure.authoring.StructureLoss;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.levelgen.structure.pools.ListPoolElement;
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public class VillageImporterListPoolElementRuntimeContractTest {
@BeforeClass
public static void bootstrapMinecraft() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void pillagerOutpostTowerCompositeProducesOnePhysicalTowerChoice() throws Exception {
StructurePoolElement tower = StructurePoolElement.legacy("minecraft:pillager_outpost/watchtower")
.apply(StructureTemplatePool.Projection.RIGID);
StructurePoolElement overgrown = StructurePoolElement.legacy("minecraft:pillager_outpost/watchtower_overgrown")
.apply(StructureTemplatePool.Projection.RIGID);
ListPoolElement towers = new ListPoolElement(
List.of(tower, overgrown),
StructureTemplatePool.Projection.RIGID
);
VillageImporter.PoolElementResolution resolution = VillageImporter.resolvePoolElement(towers);
Map<String, Object> entry = VillageImporter.piecePoolEntry(
"qa/pillager_outpost/piece/minecraft/pillager_outpost/watchtower",
1
);
StructureLoss loss = VillageImporter.listElementFallbackLoss(
resolution,
"minecraft:pillager_outpost/towers"
);
Map<String, Object> basePlatePiece = VillageImporter.pieceJson(
"qa/pillager_outpost/piece/minecraft/pillager_outpost/base_plate",
List.of(Map.of("name", "minecraft:bottom")),
0
);
Map<String, Object> watchtowerPiece = VillageImporter.pieceJson(
"qa/pillager_outpost/piece/minecraft/pillager_outpost/watchtower",
List.of(),
1155
);
VillageImporter.PoolMemberNormalization connectorScaffold = VillageImporter.normalizePoolMember(
"minecraft:pillager_outpost/features",
"qa/pillager_outpost/pool/minecraft/pillager_outpost/features",
false,
2,
"minecraft:empty",
"minecraft:pillager_outpost/feature_plate",
"qa/pillager_outpost/piece/minecraft/pillager_outpost/feature_plate",
1,
0,
List.of(Map.of("name", "minecraft:bottom")));
assertSame(tower, resolution.physicalElement());
assertEquals("minecraft:pillager_outpost/watchtower", resolution.templateLocation());
assertEquals(1, resolution.omittedElements());
assertEquals("qa/pillager_outpost/piece/minecraft/pillager_outpost/watchtower", entry.get("piece"));
assertEquals(1, entry.get("weight"));
assertEquals(StructureCapability.LIST_ELEMENTS, loss.capability());
assertEquals("list_pool_overlays_not_imported", loss.code());
assertEquals(false, basePlatePiece.get("collidable"));
assertFalse(watchtowerPiece.containsKey("collidable"));
assertEquals(VillageImporter.PoolMemberDisposition.PHYSICAL, connectorScaffold.disposition());
assertTrue(connectorScaffold.losses().isEmpty());
}
@Test
public void actualWaystoneLikeAllAirMemberNormalizesToEmpty() throws Exception {
StructurePoolElement waystone = StructurePoolElement
.legacy("dungeons_and_taverns:waystones/waystone_scaffold")
.apply(StructureTemplatePool.Projection.RIGID);
VillageImporter.PoolElementResolution resolution = VillageImporter.resolvePoolElement(waystone);
VillageImporter.PoolMemberNormalization normalization = VillageImporter.normalizePoolMember(
"dungeons_and_taverns:waystones",
"qa/pool/dungeons_and_taverns/waystones",
false,
1,
"dungeons_and_taverns:waystones",
resolution.templateLocation(),
"qa/piece/dungeons_and_taverns/waystones/waystone_scaffold",
2,
0,
List.of());
assertSame(waystone, resolution.physicalElement());
assertEquals("dungeons_and_taverns:waystones/waystone_scaffold", resolution.templateLocation());
assertEquals(VillageImporter.PoolMemberDisposition.EMPTY, normalization.disposition());
assertEquals(true, normalization.poolEntry().get("empty"));
assertEquals(2, normalization.poolEntry().get("weight"));
assertEquals("connectorless_all_air_member_normalized_empty",
normalization.losses().getFirst().code());
}
@Test
public void actualNonAirOrphanMemberIsOmitted() throws Exception {
StructurePoolElement orphan = StructurePoolElement
.legacy("test:orphan_house")
.apply(StructureTemplatePool.Projection.RIGID);
VillageImporter.PoolElementResolution resolution = VillageImporter.resolvePoolElement(orphan);
VillageImporter.PoolMemberNormalization normalization = VillageImporter.normalizePoolMember(
"test:orphan_pool",
"qa/pool/test/orphan_pool",
false,
1,
null,
resolution.templateLocation(),
"qa/piece/test/orphan_house",
4,
37,
List.of());
assertSame(orphan, resolution.physicalElement());
assertEquals("test:orphan_house", resolution.templateLocation());
assertEquals(VillageImporter.PoolMemberDisposition.OMITTED, normalization.disposition());
assertTrue(normalization.poolEntry().isEmpty());
assertEquals("connectorless_non_air_member_omitted",
normalization.losses().getFirst().code());
assertTrue(normalization.losses().getFirst().detail().contains("37 non-air block(s)"));
assertTrue(normalization.losses().getFirst().detail().contains("no source fallback"));
assertTrue(normalization.losses().getFirst().detail().contains("selection weights"));
assertTrue(normalization.losses().getFirst().detail().contains("RNG consumption"));
}
@Test
public void actualConnectorlessStartMemberRemainsPhysical() throws Exception {
StructurePoolElement start = StructurePoolElement
.legacy("test:air_start")
.apply(StructureTemplatePool.Projection.RIGID);
VillageImporter.PoolElementResolution resolution = VillageImporter.resolvePoolElement(start);
VillageImporter.PoolMemberNormalization normalization = VillageImporter.normalizePoolMember(
"test:start",
"qa/pool/test/start",
true,
2,
"test:fallback",
resolution.templateLocation(),
"qa/piece/test/air_start",
6,
0,
List.of());
assertSame(start, resolution.physicalElement());
assertEquals(VillageImporter.PoolMemberDisposition.PHYSICAL, normalization.disposition());
assertEquals("qa/piece/test/air_start", normalization.poolEntry().get("piece"));
assertTrue(normalization.losses().isEmpty());
}
@Test
public void actualMixedPoolAllAirMemberIsOmittedWithoutBecomingEmpty() throws Exception {
StructurePoolElement inert = StructurePoolElement
.legacy("test:mixed_air_inert")
.apply(StructureTemplatePool.Projection.RIGID);
VillageImporter.PoolElementResolution resolution = VillageImporter.resolvePoolElement(inert);
VillageImporter.PoolMemberNormalization normalization = VillageImporter.normalizePoolMember(
"test:mixed",
"qa/pool/test/mixed",
false,
4,
"test:mixed",
resolution.templateLocation(),
"qa/piece/test/mixed_air_inert",
9,
0,
List.of());
assertSame(inert, resolution.physicalElement());
assertEquals(VillageImporter.PoolMemberDisposition.OMITTED, normalization.disposition());
assertTrue(normalization.poolEntry().isEmpty());
assertEquals("connectorless_all_air_mixed_member_omitted",
normalization.losses().getFirst().code());
assertTrue(normalization.losses().getFirst().detail().contains("selection weights"));
assertTrue(normalization.losses().getFirst().detail().contains("RNG consumption"));
}
@Test
public void actualMixedDistinctFallbackRetainsAllConnectorlessMembers() throws Exception {
StructurePoolElement allAirElement = StructurePoolElement
.legacy("test:mixed_all_air_primary")
.apply(StructureTemplatePool.Projection.RIGID);
StructurePoolElement nonAirElement = StructurePoolElement
.legacy("test:mixed_non_air_primary")
.apply(StructureTemplatePool.Projection.RIGID);
VillageImporter.PoolElementResolution allAirResolution = VillageImporter.resolvePoolElement(allAirElement);
VillageImporter.PoolElementResolution nonAirResolution = VillageImporter.resolvePoolElement(nonAirElement);
VillageImporter.PoolMemberNormalization allAir = VillageImporter.normalizePoolMember(
"test:mixed",
"qa/pool/test/mixed",
false,
4,
"test:fallback",
allAirResolution.templateLocation(),
"qa/piece/test/mixed_all_air_primary",
9,
0,
List.of());
VillageImporter.PoolMemberNormalization nonAir = VillageImporter.normalizePoolMember(
"test:mixed",
"qa/pool/test/mixed",
false,
4,
"test:fallback",
nonAirResolution.templateLocation(),
"qa/piece/test/mixed_non_air_primary",
12,
41,
List.of());
assertSame(allAirElement, allAirResolution.physicalElement());
assertSame(nonAirElement, nonAirResolution.physicalElement());
assertEquals(VillageImporter.PoolMemberDisposition.PHYSICAL, allAir.disposition());
assertEquals(VillageImporter.PoolMemberDisposition.PHYSICAL, nonAir.disposition());
assertEquals("qa/piece/test/mixed_all_air_primary", allAir.poolEntry().get("piece"));
assertEquals("qa/piece/test/mixed_non_air_primary", nonAir.poolEntry().get("piece"));
assertTrue(allAir.losses().isEmpty());
assertTrue(nonAir.losses().isEmpty());
}
@Test
public void actualSingletonDistinctFallbackRetainsAllAirAndNonAirMembers() throws Exception {
StructurePoolElement allAirElement = StructurePoolElement
.legacy("test:all_air_primary")
.apply(StructureTemplatePool.Projection.RIGID);
StructurePoolElement nonAirElement = StructurePoolElement
.legacy("test:non_air_primary")
.apply(StructureTemplatePool.Projection.RIGID);
VillageImporter.PoolElementResolution allAirResolution = VillageImporter.resolvePoolElement(allAirElement);
VillageImporter.PoolElementResolution nonAirResolution = VillageImporter.resolvePoolElement(nonAirElement);
VillageImporter.PoolMemberNormalization allAir = VillageImporter.normalizePoolMember(
"test:primary",
"qa/pool/test/primary",
false,
1,
"test:fallback",
allAirResolution.templateLocation(),
"qa/piece/test/all_air_primary",
5,
0,
List.of());
VillageImporter.PoolMemberNormalization nonAir = VillageImporter.normalizePoolMember(
"test:primary",
"qa/pool/test/primary",
false,
1,
"test:fallback",
nonAirResolution.templateLocation(),
"qa/piece/test/non_air_primary",
7,
29,
List.of());
assertSame(allAirElement, allAirResolution.physicalElement());
assertSame(nonAirElement, nonAirResolution.physicalElement());
assertEquals(VillageImporter.PoolMemberDisposition.PHYSICAL, allAir.disposition());
assertEquals(VillageImporter.PoolMemberDisposition.PHYSICAL, nonAir.disposition());
assertEquals("qa/piece/test/all_air_primary", allAir.poolEntry().get("piece"));
assertEquals("qa/piece/test/non_air_primary", nonAir.poolEntry().get("piece"));
assertTrue(allAir.losses().isEmpty());
assertTrue(nonAir.losses().isEmpty());
}
}
@@ -8,6 +8,9 @@ import art.arcane.iris.world.lifecycle.MissingWorldStorageLog;
import art.arcane.iris.world.lifecycle.WorldReplacementBootstrap;
import art.arcane.iris.world.lifecycle.WorldReplacementBootstrapMarker;
import art.arcane.iris.pack.DefaultPackBootstrapProvisioner;
import art.arcane.iris.pack.DefaultPackBootstrapProvisioner.BootstrapRequest;
import art.arcane.iris.platform.bukkit.nms.datapack.DataVersion;
import io.papermc.paper.ServerBuildInfo;
import art.arcane.iris.pack.DefaultPackBootstrapProvisioner.ProvisionResult;
import art.arcane.iris.platform.bootstrap.SlimJar;
import io.papermc.paper.plugin.bootstrap.BootstrapContext;
@@ -184,11 +187,12 @@ public final class IrisBootstrap implements PluginBootstrap {
private static ProvisionResult provision(BootstrapContext context, BukkitStartupPaths startupPaths) {
try {
return DefaultPackBootstrapProvisioner.provision(
return DefaultPackBootstrapProvisioner.provision(new BootstrapRequest(
context.getDataDirectory(),
message -> context.getLogger().info(message),
startupPaths
);
startupPaths,
DataVersion.forMinecraftVersion(ServerBuildInfo.buildInfo().minecraftVersionId())
));
} catch (IOException e) {
throw new IllegalStateException("Unable to provision the Iris startup datapack", e);
}
@@ -343,11 +343,11 @@ public final class ModdedForcedDatapack {
throw new IllegalStateException("SHA-256 is unavailable", missing);
}
digest.update((HASH_SALT + '|' + ModdedEngineBootstrap.loader().platformName()
+ '|' + DataVersion.getLatest().getPackFormat() + '\n').getBytes(StandardCharsets.UTF_8));
+ '|' + DataVersion.V26_2.getPackFormat() + '\n').getBytes(StandardCharsets.UTF_8));
String inputFingerprint = IrisDatapackCompiler.computeInputFingerprint(
compilationPackRoots(),
List.of(),
DataVersion.getLatest().get(),
DataVersion.V26_2.get(),
false
);
digest.update(inputFingerprint.getBytes(StandardCharsets.UTF_8));
@@ -356,7 +356,7 @@ public final class ModdedForcedDatapack {
private static void writeStagedPack(Path stagingDirectory) throws IOException {
Map<String, KSet<String>> seenBiomes = new LinkedHashMap<>();
IDataFixer fixer = DataVersion.getLatest().get();
IDataFixer fixer = DataVersion.V26_2.get();
int packCount = 0;
KList<String> presetIds = new KList<>();
@@ -706,8 +706,8 @@ public final class ModdedForcedDatapack {
+ "}\n", StandardCharsets.UTF_8);
}
private static void writePackMeta(Path packDirectory) throws IOException {
int packFormat = DataVersion.getLatest().getPackFormat();
static void writePackMeta(Path packDirectory) throws IOException {
int packFormat = DataVersion.V26_2.getPackFormat();
String json = "{\n"
+ " \"pack\": {\n"
+ " \"description\": \"Iris world generation biomes and dimension types for installed packs.\",\n"
@@ -55,7 +55,7 @@ final class WorldCheckDimensionContract {
}
static DimensionContract expectedDimensionContract(IrisDimension dimension) {
JSONObject json = new JSONObject(dimension.getDimensionType().toJson(DataVersion.getLatest().get()));
JSONObject json = new JSONObject(dimension.getDimensionType().toJson(DataVersion.V26_2.get()));
return new DimensionContract(
json.getInt("min_y"),
json.getInt("height"),
@@ -166,7 +166,7 @@ public final class ModdedDatapackCommands {
IrisDimension dimension = engine.getDimension();
String json;
try {
json = dimension.getDimensionType().toJson(DataVersion.getLatest().get());
json = dimension.getDimensionType().toJson(DataVersion.V26_2.get());
} catch (Throwable e) {
ModdedIrisLog.error("Iris dimension type generation failed for {}", dimension.getLoadKey(), e);
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_DATAPACK_COMMANDS_DIMENSION_TYPE_GENERATION_FAILED, MessageArgument.untrusted("value", level.dimension().identifier()), MessageArgument.untrusted("value2", String.valueOf(e.getMessage()))));
@@ -188,7 +188,7 @@ public final class ModdedDatapackCommands {
}
File mcmeta = new File(worldDatapacksFolder(server), WORLD_PACK_NAME + "/pack.mcmeta");
int packFormat = DataVersion.getLatest().getPackFormat();
int packFormat = DataVersion.V26_2.getPackFormat();
String meta = "{\n"
+ " \"pack\": {\n"
+ " \"description\": \"Iris dimension types derived from the installed Iris packs.\",\n"
@@ -77,7 +77,7 @@ public class ModdedDimensionTypeParityTest {
IrisDimension end = dimension("the_end", IrisEnvironment.THE_END, 0, 256, 256, new IrisDimensionTypeOptions());
IrisDimension custom = dimension("custom_contract", IrisEnvironment.CUSTOM, -128, 384, 384, customOptions);
List<IrisDimension> dimensions = List.of(overworld, nether, end, custom);
IDataFixer fixer = DataVersion.getLatest().get();
IDataFixer fixer = DataVersion.V26_2.get();
Path packDirectory = Files.createTempDirectory("iris-dimension-contracts");
KList<File> roots = new KList<>();
roots.add(packDirectory.toFile());
@@ -19,6 +19,7 @@
package art.arcane.iris.modded;
import art.arcane.volmlib.util.collection.KSet;
import art.arcane.volmlib.util.json.JSONObject;
import art.arcane.iris.platform.bukkit.nms.datapack.v1217.DataFixerV1217;
import art.arcane.iris.world.history.GenerationEpochContractFactory;
import art.arcane.iris.world.history.GenerationRegistryContract;
@@ -49,6 +50,21 @@ import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class ModdedForcedDatapackTest {
@Test
public void metadataRetains262FormatWhenBukkitAlsoSupports263() throws IOException {
Path packDirectory = Files.createTempDirectory("iris-modded-pack-format");
try {
ModdedForcedDatapack.writePackMeta(packDirectory);
JSONObject pack = new JSONObject(Files.readString(packDirectory.resolve("pack.mcmeta")))
.getJSONObject("pack");
assertEquals(107, pack.getInt("pack_format"));
assertEquals(107, pack.getInt("min_format"));
assertEquals(107, pack.getInt("max_format"));
} finally {
deleteTree(packDirectory);
}
}
@Test
public void retainedDimensionSourceRendersBeforeNativeRegistryExists() throws Exception {
DataFixerV1217 fixer = new DataFixerV1217();
+32 -3
View File
@@ -74,8 +74,8 @@ project(':core') {
version = rootProject.version
}
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse('26.2')
// The Bukkit jar runs Paper 26.1.2 through 26.2 (compile-low pins); the loader jars stay 26.2-only.
String bukkitMinecraftRange = providers.gradleProperty('bukkitMinecraftRange').getOrElse('26.1.2-26.2')
// The Bukkit jar retains 26.1.2 and 26.2 alongside 26.3; loader jars stay 26.2-only.
String bukkitMinecraftRange = providers.gradleProperty('bukkitMinecraftRange').getOrElse('26.1.2-26.3')
String fabricLoaderVersion = providers.gradleProperty('fabricLoaderVersion').getOrElse('0.19.3')
String forgeVersion = providers.gradleProperty('forgeVersion').getOrElse('26.2-65.1.1')
String neoForgeVersion = providers.gradleProperty('neoForgeVersion').getOrElse('26.2.0.59')
@@ -141,6 +141,7 @@ String neoForgeConsumerPath = "${consumerLocation}/neoforge-mod-consumers/dropin
def nmsBindings = [
v26_2_R1: '26.1.2.build.74-stable',
v26_3_R1: '26.3.build.25-alpha',
]
Class nmsTypeClass = Class.forName('NMSBinding$Type')
nmsBindings.each { key, value ->
@@ -154,7 +155,9 @@ nmsBindings.each { key, value ->
extensions.extraProperties.set('nms', nmsConfig)
plugins.apply(NMSBinding)
if (key == 'v26_2_R1') {
sourceSets.main.java.srcDir(rootProject.file('adapters/minecraft-common/src/main/java'))
}
dependencies {
compileOnly(project(':core'))
@@ -619,6 +622,7 @@ List<String> requiredBukkitArtifactEntries = [
'art/arcane/iris/IrisBootstrap.class',
'art/arcane/iris/platform/bukkit/nms/INMS.class',
'art/arcane/iris/platform/bukkit/nms/v26_2_R1/NMSBinding.class',
'art/arcane/iris/platform/bukkit/nms/v26_3_R1/NMSBinding.class',
'art/arcane/iris/platform/bukkit/nms/v1X/NMSBinding1X.class',
'art/arcane/iris/platform/generation/PlatformChunkGenerator.class',
'art/arcane/iris/world/lifecycle/WorldLifecycleStaging.class',
@@ -856,9 +860,34 @@ pluginPackaging {
prunePrefixes = ['art/arcane/volmlib/']
keepPrefixes = ['art/arcane/volmlib/util/matter/slices/']
shrinkRules = [file('packaging/shrink.pro')]
shrinkDontwarn = [
'java.lang.invoke.MethodHandle',
'java.lang.invoke.VarHandle',
'net.minecraft.server.MinecraftServer',
'net.minecraft.server.level.ServerLevel',
'net.minecraft.world.attribute.EnvironmentAttributes',
'net.minecraft.world.level.NaturalSpawner',
'net.minecraft.world.level.StructureManager',
'net.minecraft.world.level.biome.BiomeManager',
'net.minecraft.world.level.biome.BiomeResolver',
'net.minecraft.world.level.biome.BiomeSource',
'net.minecraft.world.level.biome.MobSpawnSettings',
'net.minecraft.world.level.biome.MobSpawnSettings$SpawnerData',
'net.minecraft.world.level.chunk.ChunkAccess',
'net.minecraft.world.level.chunk.ChunkGenerator',
'net.minecraft.world.level.chunk.status.ChunkStatus',
'net.minecraft.world.level.levelgen.RandomState',
'net.minecraft.world.level.levelgen.feature.Feature',
'net.minecraft.world.level.levelgen.structure.Structure',
'net.minecraft.world.level.levelgen.structure.placement.ConcentricRingsStructurePlacement',
'net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement',
'org.bukkit.craftbukkit.inventory.CraftItemStack',
]
shrinkLibraries.from(provider { project(':core').configurations.compileClasspath })
shrinkLibraries.from(provider { project(':adapters:bukkit:plugin').configurations.compileClasspath })
shrinkLibraries.from(provider { project(':adapters:bukkit:nms:v26_2_R1').configurations.compileClasspath })
nmsBindings.keySet().each { binding ->
shrinkLibraries.from(provider { project(":adapters:bukkit:nms:${binding}").configurations.compileClasspath })
}
shrinkRelocations = runtimeDownloadRelocations
}
}
@@ -12,6 +12,7 @@ public final class GenerationRevisionScope {
"spi/src/main/java",
"adapters/bukkit/plugin/src/main/java",
"adapters/bukkit/nms/v26_2_R1/src/main/java",
"adapters/bukkit/nms/v26_3_R1/src/main/java",
"adapters/minecraft-common/src/main/java",
"adapters/modded-common/src/main/java",
"adapters/client-common/src/main/java",
@@ -31,6 +32,7 @@ public final class GenerationRevisionScope {
"core/src/main/java/art/arcane/iris/world/safeguard/ValueWithDiagnostics.java",
"core/src/main/java/art/arcane/iris/diagnostics/splash/IrisSplashRenderer.java",
"adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_2_R1/NmsWorldLifecycle.java",
"adapters/bukkit/nms/v26_3_R1/src/main/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NmsWorldLifecycle.java",
"adapters/bukkit/plugin/src/main/java/art/arcane/iris/command/CommandDeveloper.java",
"adapters/bukkit/plugin/src/main/java/art/arcane/iris/command/CommandPregen.java",
"adapters/bukkit/plugin/src/main/java/art/arcane/iris/command/CommandIris.java",
@@ -62,15 +62,10 @@ public final class DefaultPackBootstrapProvisioner {
return DEFAULT_PACKS;
}
public static ProvisionResult provision(Path dataDirectory, Consumer<String> feedback) throws IOException {
return provision(dataDirectory, feedback, BukkitStartupPaths.resolveCurrent());
}
public static ProvisionResult provision(
Path dataDirectory,
Consumer<String> feedback,
BukkitStartupPaths startupPaths
) throws IOException {
public static ProvisionResult provision(BootstrapRequest request) throws IOException {
Path dataDirectory = request.dataDirectory();
Consumer<String> feedback = request.feedback();
BukkitStartupPaths startupPaths = request.startupPaths();
Objects.requireNonNull(dataDirectory, "dataDirectory");
Objects.requireNonNull(feedback, "feedback");
BukkitStartupPaths requiredStartupPaths = Objects.requireNonNull(startupPaths, "startupPaths");
@@ -94,7 +89,8 @@ public final class DefaultPackBootstrapProvisioner {
3,
Duration.ofMillis(250),
MAX_ARCHIVE_BYTES,
levelRoot
levelRoot,
request.dataVersion()
);
return provision(dataDirectory, feedback, options);
}
@@ -135,7 +131,7 @@ public final class DefaultPackBootstrapProvisioner {
if (!Integer.toString(MARKER_SCHEMA).equals(marker.getProperty("schema"))) {
return false;
}
IDataFixer fixer = DataVersion.getLatest().get();
IDataFixer fixer = DataVersion.getRuntime().get();
if (fixer == null
|| !IrisDatapackCompiler.compilerIdentity(fixer)
.equals(marker.getProperty("compilerIdentity"))) {
@@ -248,9 +244,9 @@ public final class DefaultPackBootstrapProvisioner {
}
List<File> packRoots = IrisDatapackCompiler.collectPackRoots(normalizedData, options.levelRoot());
IDataFixer fixer = DataVersion.getLatest().get();
IDataFixer fixer = options.dataVersion().get();
if (fixer == null) {
throw new IOException("Latest Iris datapack fixer is unavailable during bootstrap");
throw new IOException("Selected Iris datapack fixer is unavailable during bootstrap");
}
String compilerIdentity = IrisDatapackCompiler.compilerIdentity(fixer);
String aggregateFingerprint = packRootsFingerprint(packRoots);
@@ -915,6 +911,16 @@ public final class DefaultPackBootstrapProvisioner {
}
}
public record BootstrapRequest(Path dataDirectory, Consumer<String> feedback,
BukkitStartupPaths startupPaths, DataVersion dataVersion) {
public BootstrapRequest {
Objects.requireNonNull(dataDirectory);
Objects.requireNonNull(feedback);
Objects.requireNonNull(startupPaths);
Objects.requireNonNull(dataVersion);
}
}
record ProvisionOptions(
List<PackSpec> packs,
List<IrisGeneratorBinding> bindings,
@@ -925,7 +931,8 @@ public final class DefaultPackBootstrapProvisioner {
int attempts,
Duration retryDelay,
long maxArchiveBytes,
Path levelRoot
Path levelRoot,
DataVersion dataVersion
) {
ProvisionOptions {
packs = List.copyOf(Objects.requireNonNull(packs, "packs"));
@@ -936,6 +943,10 @@ public final class DefaultPackBootstrapProvisioner {
Objects.requireNonNull(requestTimeout, "requestTimeout");
Objects.requireNonNull(retryDelay, "retryDelay");
Objects.requireNonNull(levelRoot, "levelRoot");
Objects.requireNonNull(dataVersion, "dataVersion");
if (dataVersion == DataVersion.UNSUPPORTED) {
throw new IllegalArgumentException("Unsupported bootstrap datapack version");
}
if (attempts < 1 || maxArchiveBytes < 1L) {
throw new IllegalArgumentException("Invalid bootstrap provisioning options");
}
@@ -321,7 +321,7 @@ public interface INMSBinding {
if (!supportsIrisWorldGeneration()) {
throw new IllegalStateException("Iris world '" + c.name() + "' cannot be created with limited NMS binding "
+ getClass().getSimpleName()
+ "; set general.disableNMS=false and use a supported Minecraft server runtime (26.1.2 or 26.2)");
+ "; set general.disableNMS=false and use a supported Minecraft server runtime (26.1.2, 26.2 or 26.3)");
}
if (missingDimensionTypes(generator.getTarget().getDimension().getDimensionTypeKey())) {
throw new IllegalStateException("Missing dimension types to create world");
@@ -1,7 +1,7 @@
package art.arcane.iris.platform.bukkit.nms;
final class NmsBindingSelector {
private static final String SUPPORTED_VERSIONS = "26.1.2 or 26.2";
private static final String SUPPORTED_VERSIONS = "26.1.2, 26.2 or 26.3";
private static final String SUPPORTED_TAG = "v26_2_R1";
private NmsBindingSelector() {
@@ -11,6 +11,9 @@ final class NmsBindingSelector {
if (version == null) {
throw new IllegalStateException("Iris requires an exact Minecraft version before selecting NMS");
}
if (version.isSameRelease(26, 3, 0)) {
return "v26_3_R1";
}
if (!version.isSameRelease(26, 2, 0) && !version.isSameRelease(26, 1, 2)) {
throw new IllegalStateException("Iris requires Minecraft " + SUPPORTED_VERSIONS
+ ". Detected server version: " + version.value());
@@ -1,6 +1,7 @@
package art.arcane.iris.platform.bukkit.nms.datapack;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.platform.bukkit.nms.INMS;
import art.arcane.iris.platform.bukkit.nms.datapack.v1217.DataFixerV1217;
import art.arcane.volmlib.util.collection.KMap;
@@ -8,13 +9,15 @@ import lombok.AccessLevel;
import lombok.Getter;
import java.util.function.Supplier;
import art.arcane.iris.platform.bukkit.nms.datapack.v263.DataFixerV263;
//https://minecraft.wiki/w/Pack_format
@Getter
public enum DataVersion {
UNSUPPORTED("0.0.0", 0, () -> null),
V26_1_2("26.1.2", 101, DataFixerV1217::new),
V26_2("26.2", 107, DataFixerV1217::new);
V26_2("26.2", 107, DataFixerV1217::new),
V26_3("26.3", 121, DataFixerV263::new);
private static final KMap<DataVersion, IDataFixer> cache = new KMap<>();
@Getter(AccessLevel.NONE)
private final Supplier<IDataFixer> constructor;
@@ -49,6 +52,29 @@ public enum DataVersion {
return fixer;
}
public static DataVersion forMinecraftVersion(String version) {
if (version == null) {
return UNSUPPORTED;
}
return switch (version) {
case "26.1.2" -> V26_1_2;
case "26.2", "26.2.0" -> V26_2;
case "26.3", "26.3.0" -> V26_3;
default -> UNSUPPORTED;
};
}
public static DataVersion getRuntime() {
if (!IrisPlatforms.isBound() || IrisPlatforms.get().minecraftVersion() == null) {
return getLatest();
}
DataVersion version = forMinecraftVersion(IrisPlatforms.get().minecraftVersion());
if (version == UNSUPPORTED) {
throw new IllegalStateException("Unsupported Minecraft datapack version: " + IrisPlatforms.get().minecraftVersion());
}
return version;
}
public static DataVersion getLatest() {
return values()[values().length - 1];
}
@@ -0,0 +1,54 @@
package art.arcane.iris.platform.bukkit.nms.datapack.v263;
import art.arcane.iris.generation.biome.IrisBiomeCustom;
import art.arcane.iris.platform.bukkit.nms.datapack.v1217.DataFixerV1217;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import java.util.Locale;
public final class DataFixerV263 extends DataFixerV1217 {
@Override
public JSONObject fixCustomBiome(IrisBiomeCustom biome, JSONObject json) {
JSONObject fixed = super.fixCustomBiome(biome, json);
JSONObject attributes = fixed.getJSONObject("attributes");
JSONObject effects = fixed.getJSONObject("effects");
for (String key : new String[]{"water_color", "foliage_color", "dry_foliage_color", "grass_color"}) {
color(effects, key);
}
for (String key : new String[]{"minecraft:visual/sky_color", "minecraft:visual/fog_color", "minecraft:visual/water_fog_color"}) {
color(attributes, key);
}
JSONObject spawners = fixed.getJSONObject("spawners");
for (String category : spawners.keySet()) {
JSONArray entries = spawners.getJSONArray(category);
for (int index = 0; index < entries.length(); index++) {
JSONObject spawn = entries.getJSONObject(index);
int minimum = spawn.getInt("minCount");
int maximum = spawn.getInt("maxCount");
spawn.remove("minCount");
spawn.remove("maxCount");
spawn.put("count", minimum == maximum ? minimum : new JSONObject()
.put("type", "minecraft:uniform")
.put("min_inclusive", minimum)
.put("max_inclusive", maximum));
}
}
attributes.put("minecraft:gameplay/natural_mob_spawns", new JSONObject()
.put("spawns_by_category", spawners)
.put("spawn_costs", fixed.getJSONObject("spawn_costs")));
Object probability = fixed.remove("creature_spawn_probability");
if (probability != null) {
attributes.put("minecraft:gameplay/creature_world_gen_spawn_probability", probability);
}
fixed.remove("spawners");
fixed.remove("spawn_costs");
return fixed;
}
private static void color(JSONObject object, String key) {
if (object.opt(key) instanceof Number value) {
object.put(key, String.format(Locale.ROOT, "#%06x", value.intValue() & 0xFFFFFF));
}
}
}
@@ -317,6 +317,6 @@ public class NMSBinding1X implements INMSBinding {
private IllegalStateException unsupportedStructureHook(String operation) {
return new IllegalStateException("Iris cannot " + operation + " with limited NMS binding "
+ getClass().getSimpleName()
+ "; set general.disableNMS=false and use a supported Minecraft server runtime (26.1.2 or 26.2)");
+ "; set general.disableNMS=false and use a supported Minecraft server runtime (26.1.2, 26.2 or 26.3)");
}
}
@@ -1,6 +1,7 @@
package art.arcane.iris.structure.export;
import art.arcane.volmlib.util.nbt.tag.CompoundTag;
import art.arcane.volmlib.util.nbt.mca.MCABlockStateCodecSupport;
import java.util.Collections;
import java.util.LinkedHashMap;
@@ -73,15 +74,15 @@ record VanillaBlockState(String name, Map<String, String> properties) {
return builder.append(']').toString();
}
CompoundTag toNbt() {
CompoundTag toNbt(MCABlockStateCodecSupport.Format format) {
CompoundTag state = new CompoundTag();
state.putString("Name", name);
state.putString(format.nameKey(), name);
if (!properties.isEmpty()) {
CompoundTag propertyTag = new CompoundTag();
for (Map.Entry<String, String> property : properties.entrySet()) {
propertyTag.putString(property.getKey(), property.getValue());
}
state.put("Properties", propertyTag);
state.put(format.propertiesKey(), propertyTag);
}
return state;
}
@@ -211,7 +211,7 @@ final class VanillaJigsawExportCompiler {
addError(diagnostics,
VanillaJigsawExportDiagnostic.Code.INVALID_MAX_DEPTH,
structureKey(structure),
"Minecraft 26.2 jigsaw size must be within 0..20; Iris export requires 1..20.");
"Minecraft jigsaw size must be within 0..20; Iris export requires 1..20.");
}
long horizontalDistance = (long) structure.getMaxSizeChunks() * 16L;
if (horizontalDistance < 1L || horizontalDistance > MAX_HORIZONTAL_DISTANCE) {
@@ -219,7 +219,7 @@ final class VanillaJigsawExportCompiler {
VanillaJigsawExportDiagnostic.Code.INVALID_MAX_DISTANCE,
structureKey(structure),
"Iris maxSizeChunks maps to " + horizontalDistance
+ " blocks, outside Minecraft 26.2's 1..128 horizontal limit.");
+ " blocks, outside Minecraft's 1..128 horizontal limit.");
}
int terrainPadding = settings.terrainAdaptation() == VanillaJigsawExportSettings.TerrainAdaptation.NONE
? 0 : 12;
@@ -315,7 +315,7 @@ final class VanillaJigsawExportCompiler {
addError(diagnostics,
VanillaJigsawExportDiagnostic.Code.INVALID_POOL_WEIGHT,
poolKey + "/pieces[" + index + "]",
"Minecraft 26.2 template-pool weights must be within 1..150.");
"Minecraft template-pool weights must be within 1..150.");
}
if (entry.getChance() != 1D) {
addError(diagnostics,
@@ -577,10 +577,12 @@ final class VanillaJigsawExportCompiler {
JsonObject pack = new JsonObject();
pack.addProperty("description", request.description());
JsonArray minimum = new JsonArray();
minimum.add(107);
minimum.add(1);
boolean current = VanillaStructureTemplateEncoder.targets263();
int packFormat = current ? 121 : 107;
minimum.add(packFormat);
minimum.add(current ? 0 : 1);
pack.add("min_format", minimum);
pack.addProperty("max_format", 107);
pack.addProperty("max_format", packFormat);
JsonObject root = new JsonObject();
root.add("pack", pack);
return root;
@@ -65,7 +65,7 @@ public final class VanillaJigsawExportRequest {
private final Path output;
private String namespace = "iris";
private String resourcePath;
private String description = "Iris vanilla jigsaw export for Minecraft 26.2";
private String description = "Iris vanilla jigsaw export";
private VanillaJigsawExportFormat format = VanillaJigsawExportFormat.DIRECTORY;
private VanillaJigsawExportSettings settings = VanillaJigsawExportSettings.defaults();
private boolean replaceExisting;
@@ -1,6 +1,8 @@
package art.arcane.iris.structure.export;
import art.arcane.iris.pack.value.IrisDirection;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.volmlib.util.nbt.mca.MCABlockStateCodecSupport;
import art.arcane.iris.structure.jigsaw.IrisJigsawConnector;
import art.arcane.iris.structure.jigsaw.IrisJigsawPiece;
import art.arcane.iris.structure.object.IrisObject;
@@ -51,18 +53,26 @@ final class VanillaStructureTemplateEncoder {
}
}
boolean lowercase = targets263();
MCABlockStateCodecSupport.Format format = lowercase
? MCABlockStateCodecSupport.Format.LOWERCASE : MCABlockStateCodecSupport.Format.CAPITALIZED;
CompoundTag root = new CompoundTag();
root.put("size", intList(object.getW(), object.getH(), object.getD()));
root.put("palette", palette(paletteStates));
root.put("palette", palette(paletteStates, format));
root.put("blocks", blocks(orderedBlocks, paletteIndexes));
root.put("entities", new ListTag<>(CompoundTag.class));
root.putInt("DataVersion", DATA_VERSION_26_2);
root.putInt("DataVersion", lowercase ? 5023 : DATA_VERSION_26_2);
ByteArrayOutputStream output = new ByteArrayOutputStream();
NBTUtil.write(new NamedTag("", root), output, true);
return output.toByteArray();
}
static boolean targets263() {
String minecraftVersion = IrisPlatforms.get().minecraftVersion();
return "26.3".equals(minecraftVersion) || "26.3.0".equals(minecraftVersion);
}
private Map<BlockPosition, BlockEntry> objectBlocks(IrisObject object) {
Map<BlockPosition, BlockEntry> blocks = new LinkedHashMap<>();
Vector3i center = object.getCenter();
@@ -135,10 +145,10 @@ final class VanillaStructureTemplateEncoder {
};
}
private ListTag<CompoundTag> palette(List<VanillaBlockState> states) {
private ListTag<CompoundTag> palette(List<VanillaBlockState> states, MCABlockStateCodecSupport.Format format) {
ListTag<CompoundTag> palette = new ListTag<>(CompoundTag.class);
for (VanillaBlockState state : states) {
palette.add(state.toNbt());
palette.add(state.toNbt(format));
}
return palette;
}
@@ -110,7 +110,7 @@ public final class GenerationRegistryContractFactory {
data,
dimension,
packFingerprint,
DataVersion.getLatest().get(),
DataVersion.getRuntime().get(),
registries,
registries.generationRegistry(),
aliasPolicy
@@ -224,7 +224,7 @@ public final class GenerationRegistryContractFactory {
data,
dimension,
customBiome,
DataVersion.getLatest().get(),
DataVersion.getRuntime().get(),
registry
);
}
@@ -508,7 +508,7 @@ public final class GenerationRegistryContractFactory {
) throws IOException {
return captureRequiredDefinitions(
requiredContracts,
DataVersion.getLatest().get(),
DataVersion.getRuntime().get(),
IrisPlatforms.get().registries().generationRegistry()
);
}
@@ -519,7 +519,7 @@ public final class GenerationRegistryContractFactory {
) throws IOException {
return captureRequiredDefinitions(
requiredContracts,
DataVersion.getLatest().get(),
DataVersion.getRuntime().get(),
generationRegistry
);
}
@@ -603,7 +603,7 @@ public final class GenerationRegistryContractFactory {
return requireGeneratedSource(
contract,
key,
DataVersion.getLatest().get(),
DataVersion.getRuntime().get(),
IrisPlatforms.get().registries().generationRegistry()
);
}
@@ -652,7 +652,7 @@ public final class GenerationRegistryContractFactory {
return requireGeneratedSource(
contract,
key,
DataVersion.getLatest().get(),
DataVersion.getRuntime().get(),
generationRegistry
);
}
@@ -171,7 +171,7 @@ public final class SavedTerrainChunk {
case "biomes" -> 3;
case "noise" -> 4;
case "surface" -> 5;
case "carvers" -> 6;
case "carvers", "terrain" -> 6;
case "features" -> 7;
case "initialize_light" -> 8;
case "light" -> 9;
@@ -1,6 +1,9 @@
package art.arcane.iris.world.history;
import art.arcane.volmlib.util.nbt.io.NBTUtil;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.volmlib.util.nbt.mca.MCABlockStateCodecSupport;
import art.arcane.volmlib.util.nbt.tag.CompoundTag;
import art.arcane.volmlib.util.nbt.tag.ByteArrayTag;
import art.arcane.volmlib.util.nbt.tag.ListTag;
@@ -81,7 +84,8 @@ final class SavedTerrainChunkReader {
if (sectionY * 16 < minimumY || sectionY * 16 >= (long) minimumY + height) {
continue;
}
if (sections.put(sectionY, decodeSection(section)) != null) {
if (sections.put(sectionY, decodeSection(section, root.getInt("DataVersion") >= 5023
? MCABlockStateCodecSupport.Format.LOWERCASE : MCABlockStateCodecSupport.Format.CAPITALIZED)) != null) {
throw new IOException("Saved chunk contains duplicate section " + sectionY);
}
}
@@ -158,16 +162,29 @@ final class SavedTerrainChunkReader {
return section;
}
private static Section decodeSection(CompoundTag section) throws IOException {
private static Section decodeSection(CompoundTag section, MCABlockStateCodecSupport.Format format) throws IOException {
CompoundTag blocks = compound(section, "block_states");
CompoundTag biomes = compound(section, "biomes");
ListTag<?> blockPalette = list(blocks, "palette");
List<BoundaryColumnGeometry.Voxel> decodedBlocks = new ArrayList<>(blockPalette.size());
for (Tag<?> entry : blockPalette) {
if (!(entry instanceof CompoundTag state)) {
throw new IOException("Saved block palette entry is not a compound");
Tag<?> state = entry;
if (format == MCABlockStateCodecSupport.Format.LOWERCASE
&& state instanceof CompoundTag wrapped && wrapped.size() == 1 && wrapped.containsKey("")) {
state = wrapped.get("");
}
if (format == MCABlockStateCodecSupport.Format.LOWERCASE && state instanceof StringTag name) {
PlatformBlockState resolved = IrisPlatforms.get().registries().blockOrNull(name.getValue());
if (resolved == null) {
throw new IOException("Saved block state cannot be resolved: " + name.getValue());
}
decodedBlocks.add(decodeBlock(MCABlockStateCodecSupport.encodeBlockState(
resolved.key(), name.getValue(), format), format));
} else if (state instanceof CompoundTag compound) {
decodedBlocks.add(decodeBlock(compound, format));
} else {
throw new IOException("Saved block palette entry is not a block state");
}
decodedBlocks.add(decodeBlock(state));
}
ListTag<?> biomePalette = list(biomes, "palette");
List<String> decodedBiomes = new ArrayList<>(biomePalette.size());
@@ -181,10 +198,10 @@ final class SavedTerrainChunkReader {
new Palette<>(decodedBiomes, packedData(biomes), 1, 64));
}
private static BoundaryColumnGeometry.Voxel decodeBlock(CompoundTag state) throws IOException {
String name = string(state, "Name");
private static BoundaryColumnGeometry.Voxel decodeBlock(CompoundTag state, MCABlockStateCodecSupport.Format format) throws IOException {
String name = string(state, format.nameKey());
TreeMap<String, String> properties = new TreeMap<>();
if (state.get("Properties") instanceof CompoundTag values) {
if (state.get(format.propertiesKey()) instanceof CompoundTag values) {
for (Map.Entry<String, Tag<?>> property : values) {
if (!(property.getValue() instanceof StringTag value)) {
throw new IOException("Saved block property is not a string");
@@ -192,6 +209,13 @@ final class SavedTerrainChunkReader {
properties.put(property.getKey(), value.getValue());
}
}
return decodeBlock(name, properties);
}
private static BoundaryColumnGeometry.Voxel decodeBlock(String name, Map<String, String> properties) throws IOException {
if (name.isBlank()) {
throw new IOException("Saved block state has no resource key");
}
String stateKey = stateKey(name, properties);
if (name.equals("minecraft:air") || name.equals("minecraft:cave_air") || name.equals("minecraft:void_air")) {
return name.equals("minecraft:air") ? AIR
@@ -19,6 +19,8 @@
package art.arcane.iris.world.storage.region;
import art.arcane.iris.platform.bukkit.BukkitBlockResolution;
import art.arcane.volmlib.util.nbt.mca.MCABlockStateCodecSupport;
import art.arcane.iris.platform.bukkit.nms.datapack.DataVersion;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.platform.bukkit.nms.INMS;
@@ -30,6 +32,7 @@ import art.arcane.volmlib.util.nbt.mca.NBTWorldSupport;
import art.arcane.iris.platform.reflect.KeyedType;
import art.arcane.volmlib.util.math.M;
import art.arcane.volmlib.util.nbt.tag.CompoundTag;
import art.arcane.volmlib.util.nbt.tag.Tag;
import art.arcane.iris.generation.concurrent.HyperLock;
import org.bukkit.NamespacedKey;
import org.bukkit.Registry;
@@ -43,7 +46,7 @@ import java.util.Locale;
public class NBTWorld {
private static final class Holder {
private static final BlockData AIR = BukkitBlockResolution.get("AIR");
private static final NBTWorldSupport.BlockStateCodec<BlockData> BLOCK_STATE_CODEC = NBTWorldSupport.blockStateCodec(
private static final NBTWorldSupport.BlockStateCodec<BlockData> BLOCK_STATE_CODEC = NBTWorldSupport.blockStateCodec(new NBTWorldSupport.BlockStateCodecOptions<>(
blockStateString -> BukkitBlockResolution.getOrNull(blockStateString, true),
BukkitBlockResolution::getAir,
blockData -> blockData.getAsString(true),
@@ -53,8 +56,11 @@ public class NBTWorld {
return "minecraft:" + blockData.getMaterial().name().toLowerCase(Locale.ROOT);
}
return key.getNamespace() + ":" + key.getKey();
}
);
},
INMS.get().getDataVersion() == DataVersion.V26_3
? MCABlockStateCodecSupport.Format.LOWERCASE
: MCABlockStateCodecSupport.Format.CAPITALIZED
));
private static final Map<Biome, Integer> BIOME_IDS = computeBiomeIDs();
}
@@ -127,7 +133,7 @@ public class NBTWorld {
);
}
public static BlockData getBlockData(CompoundTag tag) {
public static BlockData getBlockData(Tag<?> tag) {
return Holder.BLOCK_STATE_CODEC.decode(tag);
}
@@ -1,5 +1,7 @@
package art.arcane.iris.pack;
import art.arcane.iris.platform.bukkit.nms.datapack.DataVersion;
import art.arcane.iris.world.lifecycle.BukkitStartupPaths;
import art.arcane.iris.world.history.GenerationRegistryContractFactory;
import art.arcane.iris.generation.biome.IrisBiomeCustom;
@@ -49,6 +51,28 @@ public class DefaultPackBootstrapProvisionerTest {
assertTrue(DefaultPackBootstrapProvisioner.defaultPacks().isEmpty());
}
@Test
public void bootstrapUsesTheSelectedRuntimeBiomeSchema() throws Exception {
Path root = Files.createTempDirectory("iris-bootstrap-runtime");
try {
Path dataDirectory = root.resolve("plugins/Iris");
writePack(dataDirectory.resolve("packs/overworld"), "overworld", "runtime");
BukkitStartupPaths startupPaths = BukkitStartupPaths.resolve(root, new String[0]);
for (DataVersion version : new DataVersion[]{DataVersion.V26_1_2, DataVersion.V26_2, DataVersion.V26_3}) {
DefaultPackBootstrapProvisioner.ProvisionResult result = DefaultPackBootstrapProvisioner.provision(
new DefaultPackBootstrapProvisioner.BootstrapRequest(dataDirectory, ignored -> {}, startupPaths, version));
Path biomeRoot = result.datapackRoot().resolve("data/iris/worldgen/biome/biomes");
try (Stream<Path> paths = Files.list(biomeRoot)) {
String json = Files.readString(paths.findFirst().orElseThrow());
assertEquals(version != DataVersion.V26_3, json.contains("\"spawners\""));
assertEquals(version == DataVersion.V26_3, json.contains("minecraft:gameplay/natural_mob_spawns"));
}
}
} finally {
delete(root);
}
}
@Test
public void emptyInstallPublishesValidDatapackWithoutNetworkRequests() throws Exception {
AtomicInteger requests = new AtomicInteger();
@@ -66,7 +90,8 @@ public class DefaultPackBootstrapProvisionerTest {
1,
Duration.ZERO,
8L * 1024L * 1024L,
root
root,
DataVersion.getLatest()
);
DefaultPackBootstrapProvisioner.ProvisionResult result = DefaultPackBootstrapProvisioner.provision(
@@ -648,8 +673,8 @@ public class DefaultPackBootstrapProvisionerTest {
BukkitStartupPaths startupPaths = BukkitStartupPaths.resolve(serverRoot, new String[0]);
DefaultPackBootstrapProvisioner.ProvisionResult installed =
DefaultPackBootstrapProvisioner.provision(dataDirectory, ignored -> {
}, startupPaths);
DefaultPackBootstrapProvisioner.provision(new DefaultPackBootstrapProvisioner.BootstrapRequest(dataDirectory, ignored -> {
}, startupPaths, DataVersion.getLatest()));
Path levelStem = installed.datapackRoot().resolve("data/iris/dimension/moon.json");
assertEquals(DefaultPackBootstrapProvisioner.ProvisionStatus.INSTALLED, installed.status());
@@ -661,8 +686,8 @@ public class DefaultPackBootstrapProvisionerTest {
StandardCharsets.UTF_8
);
DefaultPackBootstrapProvisioner.ProvisionResult updated =
DefaultPackBootstrapProvisioner.provision(dataDirectory, ignored -> {
}, startupPaths);
DefaultPackBootstrapProvisioner.provision(new DefaultPackBootstrapProvisioner.BootstrapRequest(dataDirectory, ignored -> {
}, startupPaths, DataVersion.getLatest()));
assertEquals(DefaultPackBootstrapProvisioner.ProvisionStatus.UPDATED, updated.status());
assertTrue(Files.readString(levelStem).contains("\"type\": \"iris:underworld\""));
@@ -709,7 +734,8 @@ public class DefaultPackBootstrapProvisionerTest {
1,
Duration.ZERO,
8L * 1024L * 1024L,
serverRoot
serverRoot,
DataVersion.getLatest()
);
}
@@ -170,14 +170,16 @@ public class IrisDatapackCompilerTest {
int maxFormat = pack.getInt("max_format");
assertEquals(101, minFormat);
assertEquals(107, maxFormat);
assertEquals(121, maxFormat);
assertEquals(maxFormat, pack.getInt("pack_format"));
// One artifact serves both runtimes: each supported DataVersion's format must be in range.
// One artifact serves supported runtimes: each supported DataVersion's format must be in range.
assertTrue(minFormat <= DataVersion.V26_1_2.getPackFormat()
&& DataVersion.V26_1_2.getPackFormat() <= maxFormat);
assertTrue(minFormat <= DataVersion.V26_2.getPackFormat()
&& DataVersion.V26_2.getPackFormat() <= maxFormat);
assertTrue(minFormat <= DataVersion.V26_3.getPackFormat()
&& DataVersion.V26_3.getPackFormat() <= maxFormat);
}
@Test
@@ -496,14 +498,14 @@ public class IrisDatapackCompilerTest {
Path dataDirectory = server.resolve("plugins/Iris");
BukkitStartupPaths startupPaths = BukkitStartupPaths.resolve(server, new String[0]);
DefaultPackBootstrapProvisioner.ProvisionResult installed =
DefaultPackBootstrapProvisioner.provision(dataDirectory, ignored -> {
}, startupPaths);
DefaultPackBootstrapProvisioner.provision(new DefaultPackBootstrapProvisioner.BootstrapRequest(dataDirectory, ignored -> {
}, startupPaths, DataVersion.getLatest()));
assertEquals(DefaultPackBootstrapProvisioner.ProvisionStatus.INSTALLED, installed.status());
assertTrue(Files.isRegularFile(installed.datapackRoot().resolve("data/iris/dimension_type/upper.json")));
assertEquals(2, regularFileCount(installed.datapackRoot().resolve("data/iris/worldgen/biome/biomes")));
DefaultPackBootstrapProvisioner.ProvisionResult unchanged =
DefaultPackBootstrapProvisioner.provision(dataDirectory, ignored -> {
}, startupPaths);
DefaultPackBootstrapProvisioner.provision(new DefaultPackBootstrapProvisioner.BootstrapRequest(dataDirectory, ignored -> {
}, startupPaths, DataVersion.getLatest()));
assertEquals(DefaultPackBootstrapProvisioner.ProvisionStatus.UNCHANGED, unchanged.status());
assertFalse(IrisPlatforms.isBound());
}
@@ -7,6 +7,12 @@ import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class NmsBindingSelectorTest {
@Test
public void selects263Revision() {
assertEquals("v26_3_R1", NmsBindingSelector.select(MinecraftVersion.fromBukkitVersion("26.3-R0.1-SNAPSHOT")));
assertEquals("v26_3_R1", NmsBindingSelector.select(MinecraftVersion.fromBukkitVersion("26.3.0-R0.1-SNAPSHOT")));
}
@Test
public void selectsOnlyTheExactSupportedRevision() {
MinecraftVersion version = MinecraftVersion.fromBukkitVersion("26.2-R0.1-SNAPSHOT");
@@ -23,12 +29,12 @@ public class NmsBindingSelectorTest {
@Test
public void rejectsUnsupportedVersionsWithoutProbingAnotherRevision() {
MinecraftVersion version = MinecraftVersion.fromBukkitVersion("26.3-R0.1-SNAPSHOT");
MinecraftVersion version = MinecraftVersion.fromBukkitVersion("26.4-R0.1-SNAPSHOT");
IllegalStateException failure = assertThrows(IllegalStateException.class,
() -> NmsBindingSelector.select(version));
assertTrue(failure.getMessage().contains("26.3"));
assertTrue(failure.getMessage().contains("26.4"));
}
@Test

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