This commit is contained in:
Brian Neumann-Fopiano
2026-08-06 07:41:58 -06:00
parent 967c082372
commit dda5ea397e
32 changed files with 2254 additions and 40 deletions
@@ -23,6 +23,7 @@ import art.arcane.iris.nativegen.NativeStructureTerrainIntegrator;
import art.arcane.iris.nativegen.NativeStructureVegetationClearer;
import art.arcane.iris.nativegen.NativeStructureVerticalPlacer;
import art.arcane.iris.nativegen.NativeStructureVanillaLocator;
import art.arcane.iris.nativegen.NativeStructureVolumeIndex;
import art.arcane.iris.nativegen.WorldgenTerrainHeightmaps;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.IrisCustomData;
@@ -87,6 +88,7 @@ import org.bukkit.block.data.BlockData;
import org.spigotmc.SpigotWorldConfig;
import javax.annotation.Nullable;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
@@ -130,6 +132,24 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
this.runtimeMinY = level.getMinY();
this.runtimeHeight = level.getHeight();
this.runtimeSeaLevel = runtimeMinY + engine.getDimension().getFluidHeight();
installNativeStructureVolumeIndex(level);
}
private void installNativeStructureVolumeIndex(ServerLevel level) {
WeakReference<ServerLevel> levelReference = new WeakReference<>(level);
WeakReference<ChunkGenerator> generatorReference = new WeakReference<>(this);
WeakReference<BiomeSource> biomeSourceReference = new WeakReference<>(customBiomeSource);
NativeStructureVolumeIndex.install(engine, new NativeStructureVolumeIndex.Context(
level.registryAccess(),
level.getServer().getStructureManager(),
level.dimension(),
LevelHeightAccessor.create(runtimeMinY, runtimeHeight),
generatorReference::get,
biomeSourceReference::get,
() -> {
ServerLevel active = levelReference.get();
return active == null ? null : active.getChunkSource().getGeneratorState();
}));
}
@Override
@@ -13,6 +13,8 @@ import art.arcane.iris.core.nms.datapack.DataVersion;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.data.chunk.TerrainChunk;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.NativeStructureVolume;
import art.arcane.iris.nativegen.NativeStructureVolumeIndex;
import art.arcane.iris.engine.object.IrisDimensionRuntimeContract;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.nativegen.NativeStructureFactory;
@@ -794,6 +796,11 @@ public class NMSBinding implements INMSBinding {
return true;
}
@Override
public KList<NativeStructureVolume> nativeStructureVolumes(Engine engine, int minX, int minZ, int maxX, int maxZ) {
return NativeStructureVolumeIndex.volumes(engine, minX, minZ, maxX, maxZ);
}
@Override
public int getBiomeId(Biome biome) {
for (World i : Bukkit.getWorlds()) {
@@ -0,0 +1,233 @@
package art.arcane.iris.nativegen;
import com.mojang.datafixers.util.Either;
import com.mojang.serialization.MapCodec;
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.server.Bootstrap;
import net.minecraft.util.valueproviders.ConstantInt;
import net.minecraft.world.level.LevelReader;
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.BeforeClass;
import org.junit.Test;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
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 straddlingGravityPieceReadsHeightsOnlyInsideTheProcessingArea() throws Exception {
ColumnRecorder recorder = new ColumnRecorder();
resolve(straddlingPiece(
List.of(new GravityProcessor(Heightmap.Types.WORLD_SURFACE_WG, 0))), recorder);
assertEquals(columns(0, 15), recorder.heightColumns);
}
@Test
public void straddlingPieceRunsProcessorsOnlyInsideTheProcessingArea() throws Exception {
CountingProcessor counter = new CountingProcessor();
resolve(straddlingPiece(List.of(counter)), new ColumnRecorder());
assertEquals(columns(0, 15), counter.columns);
assertTrue(counter.columns.size() < TEMPLATE_WIDTH);
}
@Test
public void cappedStraddlingPieceRunsProcessorsAcrossTheWholePiece() throws Exception {
CountingProcessor counter = new CountingProcessor();
resolve(straddlingPiece(List.of(counter,
new CappedProcessor(NopProcessor.INSTANCE, ConstantInt.of(0)))),
new ColumnRecorder());
assertEquals(columns(0, TEMPLATE_WIDTH - 1), counter.columns);
}
@Test
public void cappedProcessorIsTheOnlyProcessorThatDisablesTheProcessingAreaClip() {
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),
new ProtectedBlockProcessor(HolderSet.empty()))) {
assertFalse(processor.getClass().getName(),
processor.evaluatesEntirePieceState());
}
assertTrue(new CappedProcessor(NopProcessor.INSTANCE, ConstantInt.of(4))
.evaluatesEntirePieceState());
}
@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 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 CountingProcessor implements StructureProcessor {
private final Set<Integer> columns = new TreeSet<>();
@Override
public StructureTemplate.StructureBlockInfo processBlock(
LevelReader level, BlockPos targetPosition, BlockPos referencePos,
BlockPos templateRelativePos,
StructureTemplate.StructureBlockInfo processedBlockInfo,
StructurePlaceSettings settings) {
columns.add(processedBlockInfo.pos().getX());
return processedBlockInfo;
}
@Override
public MapCodec<? extends StructureProcessor> codec() {
return BlockIgnoreProcessor.STRUCTURE_BLOCK.codec();
}
}
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,160 @@
package art.arcane.iris.nativegen;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.NativeStructureVolume;
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.nio.file.Files;
import java.nio.file.Path;
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.assertNull;
import static org.junit.Assert.assertTrue;
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 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 volumeResolutionNeverReadsChunkOrOwnershipState() throws Exception {
String source = Files.readString(Path.of(System.getProperty("iris.nativeStructureVolumeIndexSource")));
assertFalse(source.contains("StructureManager"));
assertFalse(source.contains("getStartForStructure"));
assertFalse(source.contains("getAllStarts"));
assertFalse(source.contains("ChunkAccess"));
assertFalse(source.contains("NativeStructureOwnershipStore"));
assertFalse(source.contains("NativeStructureOwnershipRecovery"));
}
@Test
public void vanillaVolumesAreGatedOnPackPolicy() throws Exception {
String source = Files.readString(Path.of(System.getProperty("iris.nativeStructureVolumeIndexSource")));
assertTrue(source.contains("NativeStructureGenerationPolicy.resolve("));
assertTrue(source.contains("if (!decision.generate())"));
assertTrue(source.contains("isStructureChunk("));
}
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 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));
}
}
}
@@ -23,6 +23,7 @@ import art.arcane.iris.core.ServerConfigurator;
import art.arcane.iris.core.datapack.DatapackIngestService;
import art.arcane.iris.core.events.IrisEngineHotloadEvent;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.project.IrisProject;
import art.arcane.iris.core.project.IrisCodeWorkspace;
import art.arcane.iris.core.service.IrisApiEventSVC;
@@ -31,15 +32,22 @@ import art.arcane.iris.core.tools.WorldMaintenance;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineMode;
import art.arcane.iris.engine.framework.EnginePlatformHooks;
import art.arcane.iris.engine.framework.NativeStructureVolume;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisDimensionRuntimeContract;
import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.collection.KList;
import org.bukkit.World;
public final class BukkitEnginePlatformHooks implements EnginePlatformHooks {
@Override
public KList<NativeStructureVolume> nativeStructureVolumes(Engine engine, int minX, int minZ, int maxX, int maxZ) {
return INMS.get().nativeStructureVolumes(engine, minX, minZ, maxX, maxZ);
}
@Override
public void refreshWorkspace(Engine engine) {
new IrisCodeWorkspace(new IrisProject(engine.getData().getDataFolder())).updateWorkspace();
@@ -0,0 +1,385 @@
package art.arcane.iris.nativegen;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
import art.arcane.iris.engine.framework.NativeStructureOwnershipRecord;
import art.arcane.iris.engine.framework.NativeStructurePlacementPlanner;
import art.arcane.iris.engine.framework.NativeStructureStartPlan;
import art.arcane.iris.engine.framework.NativeStructureVolume;
import art.arcane.iris.engine.object.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.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.function.IntBinaryOperator;
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 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<Long, KList<NativeStructureVolume>> originCache = lru(MAX_CACHED_ORIGIN_CHUNKS);
private final Map<Long, KList<NativeStructureVolume>> queryCache = lru(MAX_CACHED_QUERY_CHUNKS);
private final ConcurrentHashMap<Long, CompletableFuture<KList<NativeStructureVolume>>> originBuilds =
new ConcurrentHashMap<>();
private final ConcurrentHashMap<Long, CompletableFuture<KList<NativeStructureVolume>>> queryBuilds =
new ConcurrentHashMap<>();
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) {
Objects.requireNonNull(engine, "Native structure volume index requires an engine");
INDEXES.put(engine, new NativeStructureVolumeIndex(context));
}
public static void uninstall(Engine engine) {
if (engine != null) {
INDEXES.remove(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;
}
KList<NativeStructureVolume> resolve(Engine engine, int minX, int minZ, int maxX, int maxZ) {
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, 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 chunkX, int chunkZ) {
return cached(queryCache, queryBuilds, chunkKey(chunkX, chunkZ),
() -> buildChunkVolumes(engine, chunkX, chunkZ));
}
private KList<NativeStructureVolume> buildChunkVolumes(Engine engine, int chunkX, int chunkZ) {
int minX = chunkX << 4;
int minZ = chunkZ << 4;
int maxX = minX + 15;
int maxZ = minZ + 15;
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 : originVolumes(engine, originX, originZ)) {
if (!volume.intersectsRect(minX, minZ, maxX, maxZ)) {
continue;
}
if (volumes == null) {
volumes = new KList<>();
}
volumes.add(volume);
}
}
}
return volumes == null ? NativeStructureVolume.NONE : volumes;
}
KList<NativeStructureVolume> originVolumes(Engine engine, int chunkX, int chunkZ) {
return cached(originCache, originBuilds, 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(),
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.getHeight(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;
}
private KList<NativeStructureVolume> cached(Map<Long, KList<NativeStructureVolume>> cache,
ConcurrentHashMap<Long, CompletableFuture<KList<NativeStructureVolume>>> builds,
long 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 existing.join();
}
try {
KList<NativeStructureVolume> built = loader.get();
synchronized (cache) {
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.warn("Native structure volume resolution failed for '" + structureId
+ "'; objects will not be vetoed against it: "
+ error.getClass().getSimpleName() + ":" + error.getMessage());
}
}
private static long chunkKey(int chunkX, int chunkZ) {
return ((long) chunkX << 32) ^ (chunkZ & 0xffffffffL);
}
private static Map<Long, KList<NativeStructureVolume>> lru(int capacity) {
return new LinkedHashMap<>(16, 0.75F, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<Long, KList<NativeStructureVolume>> eldest) {
return size() > capacity;
}
};
}
/**
* 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
) {
}
}
@@ -350,6 +350,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
requireCompletedShutdown(engine);
unloading = false;
Engine bound = bindEngine(level);
nativeStructures.installVolumeIndex(level, bound);
// Bind time: a feature-order cycle is reported here, once, and degrades to features-off. Non-waiting for
// the same reason as repointAndBind: this method owns the generator monitor.
importedFeatures.prepareWithoutWaiting(bound);
@@ -36,6 +36,7 @@ import art.arcane.iris.nativegen.NativeStructureTerrainIntegrator;
import art.arcane.iris.nativegen.NativeStructureVegetationClearer;
import art.arcane.iris.nativegen.NativeStructureVerticalPlacer;
import art.arcane.iris.nativegen.NativeStructureVanillaLocator;
import art.arcane.iris.nativegen.NativeStructureVolumeIndex;
import art.arcane.iris.nativegen.WorldgenTerrainHeightmaps;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.volmlib.util.math.RNG;
@@ -50,10 +51,13 @@ 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.LevelHeightAccessor;
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.biome.BiomeSource;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.GenerationStep;
import net.minecraft.world.level.levelgen.RandomSupport;
import net.minecraft.world.level.levelgen.WorldgenRandom;
@@ -63,6 +67,7 @@ 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.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
@@ -87,6 +92,23 @@ final class ModdedNativeStructureStage {
this.generator = generator;
}
void installVolumeIndex(ServerLevel level, Engine engine) {
WeakReference<ServerLevel> levelReference = new WeakReference<>(level);
WeakReference<ChunkGenerator> generatorReference = new WeakReference<>(generator);
WeakReference<BiomeSource> biomeSourceReference = new WeakReference<>(generator.structureBiomeSource);
NativeStructureVolumeIndex.install(engine, new NativeStructureVolumeIndex.Context(
level.registryAccess(),
level.getServer().getStructureManager(),
level.dimension(),
LevelHeightAccessor.create(level.getMinY(), level.getHeight()),
generatorReference::get,
biomeSourceReference::get,
() -> {
ServerLevel active = levelReference.get();
return active == null ? null : active.getChunkSource().getGeneratorState();
}));
}
Pair<BlockPos, Holder<Structure>> findNearestIrisStructure(ServerLevel level,
HolderSet<Structure> holders,
BlockPos pos, int radius, boolean findUnexplored,
@@ -23,6 +23,7 @@ import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.tools.WorldMaintenance;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EnginePlatformHooks;
import art.arcane.iris.engine.framework.NativeStructureVolume;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisDimensionRuntimeContract;
import art.arcane.iris.engine.object.IrisWorld;
@@ -30,6 +31,7 @@ import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.modded.ModdedDimensionManager;
import art.arcane.iris.modded.ModdedForcedDatapack;
import art.arcane.iris.modded.ModdedWorkspaceGenerator;
import art.arcane.iris.nativegen.NativeStructureVolumeIndex;
import art.arcane.iris.modded.command.ModdedPregenJob;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.io.ReactiveFolder;
@@ -88,6 +90,11 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
writeWorkspace(engine, "workspace refresh");
}
@Override
public KList<NativeStructureVolume> nativeStructureVolumes(Engine engine, int minX, int minZ, int maxX, int maxZ) {
return NativeStructureVolumeIndex.volumes(engine, minX, minZ, maxX, maxZ);
}
@Override
public void refreshDatapackWorkspace(Engine engine) {
IrisData data = engine.getData();