This commit is contained in:
Brian Neumann-Fopiano
2026-07-16 12:26:01 -04:00
parent 55b7460b9a
commit 699247b1de
327 changed files with 30138 additions and 3934 deletions
@@ -66,34 +66,33 @@ public class CustomBiomeSource extends BiomeSource {
this.cacheDimension = engine.getDimension();
}
private static List<Holder<Biome>> getAllBiomes(Registry<Biome> customRegistry, Registry<Biome> registry, Engine engine, Holder<Biome> fallback) {
private static List<Holder<Biome>> getAllBiomes(Registry<Biome> customRegistry, Registry<Biome> registry, Engine engine) {
LinkedHashSet<Holder<Biome>> biomes = new LinkedHashSet<>();
boolean resolutionFailed = false;
for (IrisBiome i : engine.getAllBiomes()) {
Holder<Biome> vanillaHolder = NMSBinding.biomeToBiomeBase(registry, i.getVanillaDerivative());
if (vanillaHolder != null) {
biomes.add(vanillaHolder);
} else if (!i.isCustom()) {
resolutionFailed = true;
if (vanillaHolder == null) {
throw new IllegalStateException("Iris structure biome derivative '"
+ i.getVanillaDerivativeKey() + "' is not registered for biome '" + i.getLoadKey() + "'");
}
biomes.add(vanillaHolder);
if (i.isCustom()) {
for (IrisBiomeCustom j : i.getCustomDerivitives()) {
Holder<Biome> customHolder = resolveCustomBiomeHolder(customRegistry, engine, j.getId());
if (customHolder != null) {
biomes.add(customHolder);
} else {
resolutionFailed = true;
if (customHolder == null) {
throw new IllegalStateException("Iris custom structure biome '"
+ engine.getDimension().getLoadKey() + ":" + j.getId() + "' is not registered");
}
biomes.add(customHolder);
}
}
}
if ((resolutionFailed || biomes.isEmpty()) && fallback != null) {
biomes.add(fallback);
if (biomes.isEmpty()) {
throw new IllegalStateException("Iris pack '" + engine.getName()
+ "' has no registered structure biomes");
}
return new ArrayList<>(biomes);
}
@@ -159,7 +158,7 @@ public class CustomBiomeSource extends BiomeSource {
.lookup(Registries.BIOME).orElse(null);
Registry<Biome> worldRegistry = ((CraftWorld) world).getHandle().registryAccess()
.lookup(Registries.BIOME).orElse(null);
return Set.copyOf(getAllBiomes(customRegistry, worldRegistry, engine, fallbackBiome));
return Set.copyOf(getAllBiomes(customRegistry, worldRegistry, engine));
}
private KMap<String, Holder<Biome>> fillCustomBiomes(Registry<Biome> customRegistry, Engine engine, Holder<Biome> fallback) {
@@ -310,10 +309,16 @@ public class CustomBiomeSource extends BiomeSource {
int blockZ = z << 2;
IrisBiome irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
if (irisBiome == null) {
return getFallbackBiome();
throw new IllegalStateException("Iris returned no surface structure biome at block "
+ blockX + "," + blockZ);
}
Holder<Biome> holder = NMSBinding.biomeToBiomeBase(biomeRegistry, irisBiome.getVanillaDerivative());
return holder == null ? getFallbackBiome() : holder;
if (holder == null) {
throw new IllegalStateException("Iris structure biome derivative '"
+ irisBiome.getVanillaDerivativeKey() + "' is not registered at block "
+ blockX + "," + blockZ);
}
return holder;
}
public Holder<Biome> getVisibleNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
@@ -362,19 +367,17 @@ public class CustomBiomeSource extends BiomeSource {
private Holder<Biome> resolveStructureBiomeHolder(int x, int y, int z) {
BiomeResolution resolution = resolveBiomeResolution(x, y, z);
if (resolution == null) {
return getFallbackBiome();
throw new IllegalStateException("Iris returned no structure biome at quart "
+ x + "," + y + "," + z);
}
Holder<Biome> holder = NMSBinding.biomeToBiomeBase(biomeRegistry, resolution.irisBiome.getVanillaDerivative());
if (holder != null) {
return holder;
if (holder == null) {
throw new IllegalStateException("Iris structure biome derivative '"
+ resolution.irisBiome.getVanillaDerivativeKey() + "' is not registered at block "
+ resolution.blockX + "," + resolution.blockY + "," + resolution.blockZ);
}
if (resolution.irisBiome.isCustom()) {
return resolveCustomHolder(resolution);
}
return getFallbackBiome();
return holder;
}
private Holder<Biome> resolveVisibleBiomeHolder(int x, int y, int z) {
@@ -2,12 +2,15 @@ package art.arcane.iris.core.nms.v26_2_R1;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
import art.arcane.iris.core.structure.NativeStructureLocateCapability;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisImportedStructureControl;
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
import art.arcane.iris.engine.object.IrisVanillaStructureStiltSettings;
import art.arcane.iris.engine.object.IrisStructureStiltSettings;
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
import art.arcane.iris.nativegen.NativeStructureGenerationException;
import art.arcane.iris.nativegen.NativeStructureLocateResults;
import art.arcane.iris.nativegen.NativeStructurePostProcessor;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.IrisCustomData;
import art.arcane.iris.util.common.reflect.WrappedField;
@@ -81,7 +84,6 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
public class IrisChunkGenerator extends CustomChunkGenerator {
private static final String NATIVE_MONUMENT_KEY = "minecraft:monument";
private static final WrappedField<ChunkGenerator, BiomeSource> BIOME_SOURCE;
private static final WrappedReturningMethod<Heightmap, Object> SET_HEIGHT;
private final ChunkGenerator delegate;
@@ -109,74 +111,83 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
@Override
public @Nullable Pair<BlockPos, Holder<Structure>> findNearestMapStructure(ServerLevel level, HolderSet<Structure> holders, BlockPos pos, int radius, boolean findUnexplored) {
try {
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
BlockPos best = null;
Holder<Structure> bestHolder = null;
long bestDist = Long.MAX_VALUE;
for (Holder<Structure> holder : holders) {
Object id = registry.getKey(holder.value());
if (id == null) {
continue;
}
IrisStructureLocator.LocateResult result = IrisStructureLocator.locate(
engine, id.toString(), pos.getX(), pos.getZ(), Math.max(1, radius));
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
continue;
}
if (!result.found()) {
continue;
}
long dx = (long) result.originX() - pos.getX();
long dz = (long) result.originZ() - pos.getZ();
long d = dx * dx + dz * dz;
if (d < bestDist) {
bestDist = d;
best = new BlockPos(result.originX(), result.baseY(), result.originZ());
bestHolder = holder;
}
}
if (best != null) {
return Pair.of(best, bestHolder);
}
} catch (Throwable e) {
IrisLogging.reportError("Iris-placed structure lookup failed near "
+ pos.getX() + ", " + pos.getZ() + ".", e);
}
if (!importedControl().active()) {
return null;
}
Pair<BlockPos, Holder<Structure>> irisPlaced = findNearestIrisStructure(
level, holders, pos, Math.max(1, radius), findUnexplored);
HolderSet<Structure> reachable = filterReachableStructures(level, holders);
if (reachable == null || reachable.size() == 0) {
return null;
}
try {
return delegate.findNearestMapStructure(level, reachable, pos, radius, findUnexplored);
} catch (Throwable e) {
IrisLogging.reportError("Vanilla structure locate failed near "
+ pos.getX() + ", " + pos.getZ() + ".", e);
return null;
}
Pair<BlockPos, Holder<Structure>> nativeLocated = reachable == null || reachable.size() == 0
? null
: delegate.findNearestMapStructure(level, reachable, pos, radius, findUnexplored);
return NativeStructureLocateResults.nearest(pos, irisPlaced, nativeLocated);
}
private HolderSet<Structure> filterReachableStructures(ServerLevel level, HolderSet<Structure> holders) {
Set<String> reachable = reachableStructureKeys(level);
IrisImportedStructureControl control = importedControl();
private Pair<BlockPos, Holder<Structure>> findNearestIrisStructure(ServerLevel level,
HolderSet<Structure> holders,
BlockPos pos, int radius,
boolean findUnexplored) {
if (findUnexplored) {
return null;
}
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
List<Holder<Structure>> kept = new ArrayList<>(holders.size());
BlockPos best = null;
Holder<Structure> bestHolder = null;
long bestDist = Long.MAX_VALUE;
for (Holder<Structure> holder : holders) {
Object id = registry.getKey(holder.value());
if (id == null) {
throw new IllegalStateException("Native structure locate received an unregistered structure holder");
}
String structureId = id.toString();
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine,
structureId, NativeStructurePostProcessor.isUndergroundStep(holder.value().step()));
if (decision.status() != NativeStructureGenerationStatus.REPLACED_BY_IRIS) {
continue;
}
IrisStructureLocator.LocateResult result = IrisStructureLocator.locate(
engine, structureId, pos.getX(), pos.getZ(), radius);
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
throw new IllegalStateException("Iris structure locate reached its safety limit for "
+ structureId + " within " + radius + " chunks");
}
if (!result.found()) {
continue;
}
long dx = (long) result.originX() - pos.getX();
long dz = (long) result.originZ() - pos.getZ();
long d = dx * dx + dz * dz;
if (d < bestDist) {
bestDist = d;
best = new BlockPos(result.originX(), result.baseY(), result.originZ());
bestHolder = holder;
}
}
return best == null ? null : Pair.of(best, bestHolder);
}
private HolderSet<Structure> filterReachableStructures(ServerLevel level, HolderSet<Structure> holders) {
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
List<NativeLocateCandidate> candidates = new ArrayList<>(holders.size());
for (Holder<Structure> holder : holders) {
Object id = registry.getKey(holder.value());
if (id == null) {
throw new IllegalStateException("Native structure filtering received an unregistered structure holder");
}
String key = id.toString();
IrisNativeStructureDecision decision = control.resolve(
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine,
key, NativeStructurePostProcessor.isUndergroundStep(holder.value().step()));
if (NATIVE_MONUMENT_KEY.equals(key) || !decision.generate()
|| IrisStructureLocator.suppressesVanilla(engine, key) || !reachable.contains(key)) {
if (NativeStructureLocateCapability.isPaperUnavailable(key) || !decision.generate()) {
continue;
}
kept.add(holder);
candidates.add(new NativeLocateCandidate(holder, key));
}
if (candidates.isEmpty()) {
return HolderSet.direct(List.of());
}
Set<String> reachable = reachableStructureKeys(level);
List<Holder<Structure>> kept = new ArrayList<>(candidates.size());
for (NativeLocateCandidate candidate : candidates) {
if (reachable.contains(candidate.key())) {
kept.add(candidate.holder());
}
}
if (kept.size() == holders.size()) {
return holders;
@@ -195,14 +206,8 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
if (cached != null && cached.dimension() == dimension) {
return cached.keys();
}
Set<String> reachable;
try {
reachable = Set.copyOf(VanillaStructureBiomes.reachableStructureKeys(level, customBiomeSource));
} catch (Throwable error) {
IrisLogging.reportError("Iris could not resolve native structure biome reachability; "
+ "native locate is disabled until the next hotload.", error);
reachable = Set.of();
}
Set<String> reachable = Set.copyOf(
VanillaStructureBiomes.reachableStructureKeys(level, customBiomeSource));
reachableStructureCache = new ReachableStructureCache(dimension, reachable);
return reachable;
}
@@ -232,9 +237,6 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
@Override
public void createStructures(RegistryAccess registryAccess, ChunkGeneratorStructureState structureState, StructureManager structureManager, ChunkAccess access, StructureTemplateManager templateManager, ResourceKey<Level> levelKey) {
if (!importedControl().active()) {
return;
}
Map<Structure, StructureStart> previousStarts = new HashMap<>(access.getAllStarts());
super.createStructures(registryAccess, structureState, structureManager, access, templateManager, levelKey);
adjustGeneratedStructures(registryAccess, access, previousStarts);
@@ -242,7 +244,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
private void adjustGeneratedStructures(RegistryAccess registryAccess, ChunkAccess access, Map<Structure, StructureStart> previousStarts) {
Registry<Structure> registry = registryAccess.lookupOrThrow(Registries.STRUCTURE);
IrisImportedStructureControl control = importedControl();
ChunkPos chunkPos = access.getPos();
for (Map.Entry<Structure, StructureStart> entry : access.getAllStarts().entrySet()) {
Structure structure = entry.getKey();
StructureStart start = entry.getValue();
@@ -251,32 +253,40 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
}
Identifier id = registry.getKey(structure);
String structureId = id == null ? null : id.toString();
IrisNativeStructureDecision decision = control.resolve(
structureId, NativeStructurePostProcessor.isUndergroundStep(structure.step()));
if (!decision.generate() || IrisStructureLocator.suppressesVanilla(engine, structureId)) {
if (structureId == null) {
throw NativeStructureGenerationException.failure(
"resolution", null, chunkPos.x(), chunkPos.z());
}
boolean undergroundStep = NativeStructurePostProcessor.isUndergroundStep(structure.step());
IrisNativeStructureDecision decision;
try {
decision = NativeStructureGenerationPolicy.resolve(engine,
structureId, undergroundStep);
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"policy resolution", structureId, chunkPos.x(), chunkPos.z(), error);
}
if (!decision.generate()) {
access.setStartForStructure(structure, StructureStart.INVALID_START);
continue;
}
try {
NativeStructurePostProcessor.applyVerticalShift(
NativeStructurePostProcessor.applyVerticalPlacement(
start,
structureId,
decision.yShift(),
getSeaLevel(),
access.getMinY(),
access.getMinY() + access.getHeight());
} catch (RuntimeException error) {
access.setStartForStructure(structure, StructureStart.INVALID_START);
IrisLogging.reportError("Iris rejected native structure " + structureId + " in chunk "
+ access.getPos().x() + "," + access.getPos().z()
+ " because its vertical bounds are invalid.", error);
continue;
access.getMinY() + access.getHeight(),
undergroundStep,
(x, z) -> engine.getHeight(x, z, true) + engine.getMinHeight());
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"vertical adjustment", structureId, chunkPos.x(), chunkPos.z(), error);
}
}
}
private IrisImportedStructureControl importedControl() {
return engine.getDimension().getImportedStructures();
}
@Override
public ChunkGeneratorStructureState createState(HolderLookup<StructureSet> holderlookup, RandomState randomstate, long i, SpigotWorldConfig conf) {
return delegate.createState(holderlookup, randomstate, i, conf);
@@ -347,15 +357,17 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
@Override
public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager, boolean vanilla) {
addVanillaDecorations(generatoraccessseed, ichunkaccess, structuremanager);
if (importedControl().active()) {
placeVanillaStructures(generatoraccessseed, ichunkaccess, structuremanager);
}
placeVanillaStructures(generatoraccessseed, ichunkaccess, structuremanager);
delegate.applyBiomeDecoration(generatoraccessseed, ichunkaccess, structuremanager, false);
}
private void placeVanillaStructures(WorldGenLevel world, ChunkAccess chunk, StructureManager structureManager) {
if (!structureManager.shouldGenerateStructures()) {
return;
ChunkPos disabledChunk = chunk.getPos();
throw new IllegalStateException("Iris cannot generate native structures in chunk "
+ disabledChunk.x() + "," + disabledChunk.z()
+ " because structure generation is disabled outside the pack; enable native structure generation "
+ "and deny individual structures through importedStructures.disabled");
}
ChunkPos chunkPos = chunk.getPos();
SectionPos sectionPos = SectionPos.of(chunkPos, world.getMinSectionY());
@@ -366,42 +378,60 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
long decoSeed = random.setDecorationSeed(world.getSeed(), origin.getX(), origin.getZ());
BoundingBox area = writableArea(chunk);
int steps = GenerationStep.Decoration.values().length;
IrisImportedStructureControl control = importedControl();
List<NativePlacementGroup> placementGroups = new ArrayList<>();
List<StructureStart> nativeStarts = new ArrayList<>();
List<NativeStructurePostProcessor.VegetationTarget> vegetationTargets = new ArrayList<>();
for (int step = 0; step < steps; step++) {
int index = 0;
for (Structure structure : byStep.get(step)) {
Object id = registry.getKey(structure);
String structureId = id == null ? null : id.toString();
IrisNativeStructureDecision decision = control.resolve(
structureId, NativeStructurePostProcessor.isUndergroundStep(structure.step()));
if (decision.generate() && !IrisStructureLocator.suppressesVanilla(engine, structureId)) {
try {
if (structureId == null) {
throw NativeStructureGenerationException.failure(
"resolution", null, chunkPos.x(), chunkPos.z());
}
try {
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine,
structureId, NativeStructurePostProcessor.isUndergroundStep(structure.step()));
if (decision.generate()) {
List<StructureStart> starts = structureManager.startsForStructure(sectionPos, structure);
if (!starts.isEmpty()) {
List<StructureStart> resolvedStarts = List.copyOf(starts);
placementGroups.add(new NativePlacementGroup(
structureId, decision, index, step, resolvedStarts));
nativeStarts.addAll(resolvedStarts);
boolean clearEntireFootprint = NativeStructurePostProcessor
.shouldClearEntireVegetationFootprint(
structure.step(), decision.clearVegetation());
for (StructureStart start : resolvedStarts) {
vegetationTargets.add(new NativeStructurePostProcessor.VegetationTarget(
start, decision.clearVegetation()));
start, clearEntireFootprint));
}
}
} catch (Throwable e) {
IrisLogging.reportError("Iris failed to resolve native structure " + structureId
+ " in chunk " + chunkPos.x() + "," + chunkPos.z() + ".", e);
}
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"resolution", structureId, chunkPos.x(), chunkPos.z(), error);
}
index++;
}
}
try {
NativeStructurePostProcessor.prepareSurfaceStructures(
world, area, nativeStarts,
(x, z) -> engine.getHeight(x, z, true) + engine.getMinHeight());
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"terrain integration", nativeStructureBatchContext(placementGroups),
chunkPos.x(), chunkPos.z(), error);
}
try {
NativeStructurePostProcessor.clearIntersectingVegetation(
world, chunk, area, vegetationTargets);
} catch (Throwable e) {
IrisLogging.reportError("Iris failed to clear vegetation from native structures in chunk "
+ chunkPos.x() + "," + chunkPos.z() + ".", e);
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"vegetation cleanup", nativeStructureBatchContext(placementGroups),
chunkPos.x(), chunkPos.z(), error);
}
for (NativePlacementGroup group : placementGroups) {
random.setFeatureSeed(decoSeed, group.featureIndex(), group.step());
@@ -410,13 +440,27 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
placeVanillaStructure(world, structureManager, random, area, chunkPos,
group.structureId(), start, group.decision());
}
} catch (Throwable e) {
IrisLogging.reportError("Iris failed to place native structure " + group.structureId()
+ " in chunk " + chunkPos.x() + "," + chunkPos.z() + ".", e);
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"placement", group.structureId(), chunkPos.x(), chunkPos.z(), error);
}
}
}
private static String nativeStructureBatchContext(List<NativePlacementGroup> placementGroups) {
if (placementGroups.isEmpty()) {
return "<no resolved native structures>";
}
StringBuilder context = new StringBuilder("[");
for (int i = 0; i < placementGroups.size(); i++) {
if (i > 0) {
context.append(", ");
}
context.append(placementGroups.get(i).structureId());
}
return context.append(']').toString();
}
private void placeVanillaStructure(WorldGenLevel world, StructureManager structureManager, WorldgenRandom random,
BoundingBox area, ChunkPos chunkPos, String structureId, StructureStart start,
IrisNativeStructureDecision decision) {
@@ -451,13 +495,14 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
}
}
private BlockState resolveStiltBlock(IrisVanillaStructureStiltSettings settings, RNG rng, int x, int y, int z) {
private BlockState resolveStiltBlock(IrisStructureStiltSettings settings, RNG rng, int x, int y, int z) {
if (settings.getPalette() == null) {
return Blocks.COBBLESTONE.defaultBlockState();
}
PlatformBlockState platformState = settings.getPalette().get(rng, x, y, z, engine.getData());
if (platformState == null || !(platformState.nativeHandle() instanceof BlockData blockData)) {
return Blocks.COBBLESTONE.defaultBlockState();
throw new IllegalStateException("Configured native structure stilt palette did not resolve a Bukkit block at "
+ x + "," + y + "," + z);
}
if (blockData instanceof IrisCustomData customData) {
blockData = customData.getBase();
@@ -465,7 +510,8 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
if (blockData instanceof CraftBlockData craftBlockData) {
return craftBlockData.getState();
}
return Blocks.COBBLESTONE.defaultBlockState();
throw new IllegalStateException("Configured native structure stilt palette resolved unsupported Bukkit block data "
+ blockData.getClass().getName() + " at " + x + "," + y + "," + z);
}
private BoundingBox writableArea(ChunkAccess chunk) {
@@ -616,4 +662,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
private record NativePlacementGroup(String structureId, IrisNativeStructureDecision decision,
int featureIndex, int step, List<StructureStart> starts) {
}
private record NativeLocateCandidate(Holder<Structure> holder, String key) {
}
}
@@ -15,6 +15,7 @@ import art.arcane.iris.engine.data.chunk.TerrainChunk;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisDimensionRuntimeContract;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.nativegen.NativeStructureGenerationException;
import art.arcane.iris.util.project.agent.Agent;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
@@ -396,6 +397,11 @@ public class NMSBinding implements INMSBinding {
return true;
}
@Override
public boolean supportsIrisWorldGeneration() {
return true;
}
@Override
public int getTrueBiomeBaseId(Object biomeBase) {
return getCustomBiomeRegistry().getId(((Holder<net.minecraft.world.level.biome.Biome>) biomeBase).value());
@@ -470,8 +476,8 @@ public class NMSBinding implements INMSBinding {
KList<String> keys = new KList<>();
try {
registry().lookupOrThrow(Registries.STRUCTURE).keySet().forEach(k -> keys.add(k.toString()));
} catch (Throwable e) {
IrisLogging.reportError(e);
} catch (RuntimeException e) {
throw new IllegalStateException("Iris failed to read registered structure keys from the Minecraft registry", e);
}
return keys;
}
@@ -481,8 +487,8 @@ public class NMSBinding implements INMSBinding {
KList<String> keys = new KList<>();
try {
registry().lookupOrThrow(Registries.STRUCTURE_SET).keySet().forEach(k -> keys.add(k.toString()));
} catch (Throwable e) {
IrisLogging.reportError(e);
} catch (RuntimeException e) {
throw new IllegalStateException("Iris failed to read registered structure-set keys from the Minecraft registry", e);
}
return keys;
}
@@ -494,8 +500,9 @@ public class NMSBinding implements INMSBinding {
ServerLevel level = ((CraftWorld) world).getHandle();
BiomeSource source = level.getChunkSource().getGenerator().getBiomeSource();
keys.addAll(VanillaStructureBiomes.reachableStructureKeys(level, source));
} catch (Throwable e) {
IrisLogging.reportError(e);
} catch (RuntimeException e) {
throw new IllegalStateException("Iris failed to resolve reachable structures for Bukkit world '"
+ (world == null ? "<null>" : world.getName()) + "'", e);
}
return keys;
}
@@ -504,9 +511,14 @@ public class NMSBinding implements INMSBinding {
public KList<String> getStructureBiomeKeys(String structureKey) {
KList<String> keys = new KList<>();
try {
keys.addAll(VanillaStructureBiomes.structureBiomeKeys(registry(), structureKey));
} catch (Throwable e) {
IrisLogging.reportError(e);
RegistryAccess access = registry();
if (access == null) {
throw new IllegalStateException("Minecraft registry access is unavailable");
}
keys.addAll(VanillaStructureBiomes.structureBiomeKeys(access, structureKey));
} catch (RuntimeException e) {
throw new IllegalStateException("Iris failed to resolve biome keys for registered structure '"
+ structureKey + "'", e);
}
return keys;
}
@@ -518,8 +530,9 @@ public class NMSBinding implements INMSBinding {
ServerLevel level = ((CraftWorld) world).getHandle();
BiomeSource source = level.getChunkSource().getGenerator().getBiomeSource();
keys.addAll(VanillaStructureBiomes.possibleBiomeKeys(source));
} catch (Throwable e) {
IrisLogging.reportError(e);
} catch (RuntimeException e) {
throw new IllegalStateException("Iris failed to resolve possible structure biome keys for Bukkit world '"
+ (world == null ? "<null>" : world.getName()) + "'", e);
}
return keys;
}
@@ -636,9 +649,9 @@ public class NMSBinding implements INMSBinding {
}
}
return new int[]{box.minX(), box.minY(), box.minZ(), box.maxX(), box.maxY(), box.maxZ()};
} catch (Throwable e) {
IrisLogging.reportError(e);
return null;
} catch (RuntimeException e) {
throw NativeStructureGenerationException.failure(
"capture placement", structureKey, chunkX, chunkZ, e);
}
}
@@ -21,10 +21,10 @@ final class VanillaStructureBiomes {
}
static Set<String> possibleBiomeKeys(BiomeSource source) {
Set<String> keys = new LinkedHashSet<>();
if (source == null) {
return keys;
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();
@@ -34,18 +34,24 @@ final class VanillaStructureBiomes {
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) {
Set<String> keys = new LinkedHashSet<>();
if (access == null || structureKey == null || structureKey.isEmpty()) {
return keys;
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) {
return keys;
throw new IllegalArgumentException("Registered structure does not exist: " + structureKey);
}
for (Holder<Biome> holder : structure.biomes()) {
Optional<ResourceKey<Biome>> key = holder.unwrapKey();
@@ -53,18 +59,19 @@ final class VanillaStructureBiomes {
keys.add(key.get().identifier().toString());
}
}
if (keys.isEmpty()) {
throw new IllegalStateException("Registered structure '" + structureKey
+ "' exposes no 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> reachable = new LinkedHashSet<>();
if (level == null || source == null) {
return reachable;
}
Set<String> possible = possibleBiomeKeys(source);
if (possible.isEmpty()) {
return reachable;
}
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
for (Map.Entry<ResourceKey<Structure>, Structure> entry : registry.entrySet()) {
for (Holder<Biome> holder : entry.getValue().biomes()) {
@@ -0,0 +1,57 @@
package art.arcane.iris.core.nms.v26_2_R1;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class IrisChunkGeneratorFailureContractTest {
@Test
public void structureLocateDoesNotCatchAndFallThroughToAnotherImplementation() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource")));
int locateStart = source.indexOf("public @Nullable Pair<BlockPos, Holder<Structure>> findNearestMapStructure");
int locateEnd = source.indexOf("private HolderSet<Structure> filterReachableStructures", locateStart);
String locate = source.substring(locateStart, locateEnd);
int reachabilityStart = source.indexOf("private Set<String> reachableStructureKeys");
int reachabilityEnd = source.indexOf("protected MapCodec", reachabilityStart);
String reachability = source.substring(reachabilityStart, reachabilityEnd);
assertTrue(locate.contains("reached its safety limit"));
assertTrue(locate.contains("unregistered structure holder"));
assertFalse(locate.contains("catch (Throwable"));
assertFalse(locate.contains("IrisLogging.reportError"));
assertFalse(reachability.contains("catch (Throwable"));
assertFalse(reachability.contains("reachable = Set.of()"));
}
@Test
public void structureGenerationAbortsInsteadOfLoggingAndContinuing() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource")));
int adjustmentStart = source.indexOf("private void adjustGeneratedStructures");
int adjustmentEnd = source.indexOf("public ChunkGeneratorStructureState createState", adjustmentStart);
String adjustment = source.substring(adjustmentStart, adjustmentEnd);
int placementStart = source.indexOf("private void placeVanillaStructures");
int placementEnd = source.indexOf("private void placeVanillaStructure(", placementStart + 1);
String placement = source.substring(placementStart, placementEnd);
assertTrue(adjustment.contains("NativeStructureGenerationException.failure("));
assertTrue(adjustment.contains("\"vertical adjustment\""));
assertFalse(adjustment.contains("IrisLogging.reportError"));
assertTrue(placement.contains("\"resolution\""));
assertTrue(placement.contains("\"terrain integration\""));
assertTrue(placement.contains("\"vegetation cleanup\""));
assertTrue(placement.contains("\"placement\""));
assertTrue(placement.contains("because structure generation is disabled outside the pack"));
assertTrue(placement.contains("prepareSurfaceStructures"));
assertTrue(placement.contains("clearIntersectingVegetation"));
assertTrue(placement.indexOf("prepareSurfaceStructures")
< placement.indexOf("clearIntersectingVegetation"));
assertTrue(placement.indexOf("clearIntersectingVegetation")
< placement.indexOf("for (NativePlacementGroup group"));
assertFalse(placement.contains("IrisLogging.reportError"));
}
}
@@ -1,5 +1,10 @@
package art.arcane.iris.core.nms.v26_2_R1;
import art.arcane.iris.nativegen.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.io.IOException;
@@ -8,35 +13,70 @@ import java.nio.file.Path;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
public class IrisChunkGeneratorMonumentLocateContractTest {
@Test
public void irisPlacementRunsBeforeExactNativeMonumentIsRemovedFromDelegateLookup() throws IOException {
public void nativeLocateAllowsExplicitReplacementBeforeNativeCapabilityGate() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource")));
int findStart = source.indexOf("findNearestMapStructure(ServerLevel level");
assertTrue(findStart >= 0);
int filterStart = source.indexOf("private HolderSet<Structure> filterReachableStructures", findStart);
assertTrue(filterStart > findStart);
String findMethod = source.substring(findStart, filterStart);
int irisLocate = findMethod.indexOf("IrisStructureLocator.locate(");
int searchLimit = findMethod.indexOf("LocateStatus.SEARCH_LIMIT_REACHED", irisLocate);
int limitSkip = findMethod.indexOf("continue;", searchLimit);
int nativeFilter = findMethod.indexOf("filterReachableStructures(level, holders)");
int delegateLocate = findMethod.indexOf("delegate.findNearestMapStructure(level, reachable");
int irisHelperStart = source.indexOf("private Pair<BlockPos, Holder<Structure>> findNearestIrisStructure(", findStart);
int filterStart = source.indexOf("private HolderSet<Structure> filterReachableStructures", irisHelperStart);
assertTrue(irisHelperStart > findStart);
assertTrue(filterStart > irisHelperStart);
String outerMethod = source.substring(findStart, irisHelperStart);
String irisHelper = source.substring(irisHelperStart, filterStart);
int policyResolution = irisHelper.indexOf("NativeStructureGenerationPolicy.resolve(engine,");
int unexploredGuard = irisHelper.indexOf("if (findUnexplored)");
int registryLookup = irisHelper.indexOf("level.registryAccess().lookupOrThrow(Registries.STRUCTURE)");
int replacementCheck = irisHelper.indexOf(
"decision.status() != NativeStructureGenerationStatus.REPLACED_BY_IRIS");
int irisLocate = irisHelper.indexOf("IrisStructureLocator.locate(", replacementCheck);
int searchLimit = irisHelper.indexOf("LocateStatus.SEARCH_LIMIT_REACHED", irisLocate);
int limitSkip = irisHelper.indexOf("continue;", searchLimit);
int nativeFilter = outerMethod.indexOf("filterReachableStructures(level, holders)");
int delegateLocate = outerMethod.indexOf("delegate.findNearestMapStructure(level, reachable");
int nearestSelection = outerMethod.indexOf(
"NativeStructureLocateResults.nearest(pos, irisPlaced, nativeLocated)");
int reachabilityStart = source.indexOf("private Set<String> reachableStructureKeys", filterStart);
assertTrue(reachabilityStart > filterStart);
String filterMethod = source.substring(filterStart, reachabilityStart);
int monumentReject = filterMethod.indexOf("if (NATIVE_MONUMENT_KEY.equals(key)");
int monumentReject = filterMethod.indexOf("if (NativeStructureLocateCapability.isPaperUnavailable(key)");
int rejectContinue = filterMethod.indexOf("continue;", monumentReject);
int emptyNativePartition = filterMethod.indexOf("if (candidates.isEmpty())", rejectContinue);
int reachabilityLookup = filterMethod.indexOf("reachableStructureKeys(level)", emptyNativePartition);
assertTrue(irisLocate >= 0);
assertTrue(policyResolution >= 0);
assertTrue(unexploredGuard >= 0);
assertTrue(registryLookup > unexploredGuard);
assertTrue(policyResolution > registryLookup);
assertTrue(replacementCheck > policyResolution);
assertTrue(irisLocate > replacementCheck);
assertTrue(searchLimit > irisLocate);
assertTrue(limitSkip > searchLimit);
assertTrue(nativeFilter > limitSkip);
assertTrue(nativeFilter >= 0);
assertTrue(delegateLocate > nativeFilter);
assertTrue(nearestSelection > delegateLocate);
assertTrue(monumentReject >= 0);
assertTrue(rejectContinue > monumentReject);
assertTrue(findMethod.contains("new BlockPos(result.originX(), result.baseY(), result.originZ())"));
assertTrue(emptyNativePartition > rejectContinue);
assertTrue(reachabilityLookup > emptyNativePartition);
assertFalse(irisHelper.contains("NativeStructureLocateCapability.isPaperUnavailable(structureId)"));
assertTrue(irisHelper.contains("new BlockPos(result.originX(), result.baseY(), result.originZ())"));
}
@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
@@ -54,18 +94,34 @@ public class IrisChunkGeneratorMonumentLocateContractTest {
}
@Test
public void verticalShiftMovesPiecesJigsawJunctionsAndCachedBoundsTogether() throws IOException {
public void verticalPlacementMovesPiecesMonumentChildrenJigsawJunctionsAndCachedBoundsTogether() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nativeStructurePostProcessorSource")));
int method = source.indexOf("public static int applyVerticalShift");
int clamp = source.indexOf("StructureVerticalBounds.clampOffset", method);
int pieceMove = source.indexOf("piece.move(0, offsetY, 0)", clamp);
int junctionMove = source.indexOf("junction.getSourceGroundY() + offsetY", pieceMove);
int boundsMove = source.indexOf("bounds.move(0, offsetY, 0)", junctionMove);
int placementStart = source.indexOf("public static int applyVerticalPlacement");
int shiftStart = source.indexOf("public static int applyVerticalShift", placementStart);
int alignmentStart = source.indexOf("static int alignOceanMonumentToSeaLevel", shiftStart);
int moveStart = source.indexOf("private static void moveStructureStart", alignmentStart);
int movePieceStart = source.indexOf("private static void moveStructurePiece", moveStart);
int monumentChildrenStart = source.indexOf("static List<StructurePiece> monumentChildPieces", movePieceStart);
assertTrue(method >= 0);
assertTrue(clamp > method);
assertTrue(pieceMove > clamp);
assertTrue(junctionMove > pieceMove);
assertTrue(boundsMove > junctionMove);
assertTrue(placementStart >= 0);
assertTrue(shiftStart > placementStart);
assertTrue(alignmentStart > shiftStart);
assertTrue(moveStart > alignmentStart);
assertTrue(movePieceStart > moveStart);
assertTrue(monumentChildrenStart > movePieceStart);
String placementMethod = source.substring(placementStart, shiftStart);
String shiftMethod = source.substring(shiftStart, alignmentStart);
String moveMethod = source.substring(moveStart, movePieceStart);
String movePieceMethod = source.substring(movePieceStart, monumentChildrenStart);
assertTrue(placementMethod.contains("return alignOceanMonumentToSeaLevel("));
assertTrue(placementMethod.contains("return applyVerticalShift("));
assertTrue(shiftMethod.contains("StructureVerticalBounds.clampOffset"));
assertTrue(shiftMethod.contains("moveStructureStart(start, bounds, offsetY)"));
assertTrue(moveMethod.contains("moveStructurePiece(piece, offsetY)"));
assertTrue(moveMethod.contains("cachedBounds.move(0, offsetY, 0)"));
assertTrue(movePieceMethod.contains("piece.move(0, offsetY, 0)"));
assertTrue(movePieceMethod.contains("child.move(0, offsetY, 0)"));
assertTrue(movePieceMethod.contains("junction.getSourceGroundY() + offsetY"));
}
}
@@ -0,0 +1,29 @@
package art.arcane.iris.core.nms.v26_2_R1;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class NMSBindingPlacementFailureContractTest {
@Test
public void structurePlacementPropagatesRuntimeFailuresWithContext() throws IOException {
Path chunkGeneratorSource = Path.of(System.getProperty("iris.nmsChunkGeneratorSource"));
String source = Files.readString(chunkGeneratorSource.resolveSibling("NMSBinding.java"));
int methodStart = source.indexOf("public int[] placeStructure(");
int methodEnd = source.indexOf("\n @Override", methodStart + 1);
String method = source.substring(methodStart, methodEnd);
int catchStart = method.indexOf("catch (RuntimeException e)");
assertTrue(catchStart >= 0);
String failurePath = method.substring(catchStart);
assertTrue(failurePath.contains("throw NativeStructureGenerationException.failure("));
assertTrue(failurePath.contains("\"capture placement\", structureKey, chunkX, chunkZ, e"));
assertFalse(failurePath.contains("return null;"));
assertFalse(failurePath.contains("catch (Throwable"));
}
}
@@ -0,0 +1,43 @@
package art.arcane.iris.core.nms.v26_2_R1;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class NMSBindingStructureFailureContractTest {
@Test
public void structureRegistryHooksThrowInsteadOfReturningEmptyOnFailure() throws IOException {
Path chunkGeneratorSource = Path.of(System.getProperty("iris.nmsChunkGeneratorSource"));
String source = Files.readString(chunkGeneratorSource.resolveSibling("NMSBinding.java"));
List<String> methodNames = List.of(
"getStructureKeys",
"getStructureSetKeys",
"getReachableStructureKeys",
"getStructureBiomeKeys",
"getPossibleBiomeKeys");
for (String methodName : methodNames) {
String method = methodSource(source, methodName);
assertTrue(methodName + " must preserve failure context", method.contains("throw new IllegalStateException("));
assertTrue(methodName + " must retain its root cause", method.contains(", e);"));
assertFalse(methodName + " must not log and return an empty registry result",
method.contains("IrisLogging.reportError"));
assertFalse(methodName + " must not catch fatal JVM errors", method.contains("catch (Throwable"));
}
}
private static String methodSource(String source, String methodName) {
int start = source.indexOf("public KList<String> " + methodName + "(");
int end = source.indexOf("\n @Override", start + 1);
if (start < 0 || end < 0) {
throw new IllegalArgumentException("Unable to find NMS binding method " + methodName);
}
return source.substring(start, end);
}
}
@@ -0,0 +1,140 @@
package art.arcane.iris.nativegen;
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.List;
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 = NativeStructurePostProcessor.applyVerticalPlacement(
start, "minecraft:monument", 0, 63, -64, 320, false, (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 = NativeStructurePostProcessor.monumentChildPieces(building);
int[] childMinY = minimumYs(children);
BoundingBox cachedBounds = start.getBoundingBox();
int offset = NativeStructurePostProcessor.applyVerticalPlacement(
start, "minecraft:monument", 0, 50, -256, 512, false, (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 = NativeStructurePostProcessor.applyVerticalPlacement(
start, "minecraft:monument", 0, 50, -256, 512, false, (x, z) -> 0);
assertEquals(0, repeatedOffset);
}
@Test
public void configuredOffsetIsRelativeToTheActualSeaLevel() {
StructureStart start = monumentStart(1337L);
int offset = NativeStructurePostProcessor.applyVerticalPlacement(
start, "minecraft:monument", 3, 50, -256, 512, false, (x, z) -> 0);
assertEquals(-10, offset);
assertEquals(29, start.getBoundingBox().minY());
assertEquals(51, start.getBoundingBox().maxY());
}
@Test
public void vanillaReloadRegenerationIsRealignedBeforePlacement() {
long seed = 1337L;
ChunkPos chunkPos = new ChunkPos(0, 0);
StructureStart initial = monumentStart(seed);
NativeStructurePostProcessor.applyVerticalPlacement(
initial, "minecraft:monument", 0, 50, -256, 512, false, (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 = NativeStructurePostProcessor.applyVerticalPlacement(
reloaded, "minecraft:monument", 0, 50, -256, 512, false, (x, z) -> 0);
assertEquals(-13, offset);
assertEquals(26, reloaded.getBoundingBox().minY());
assertEquals(48, reloaded.getBoundingBox().maxY());
}
@Test
public void impossibleSeaLevelAlignmentFailsInsteadOfClippingTheMonument() {
StructureStart start = monumentStart(1337L);
try {
NativeStructurePostProcessor.applyVerticalPlacement(
start, "minecraft:monument", 0, -50, -64, 320, false, (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,327 @@
package art.arcane.iris.nativegen;
import com.mojang.datafixers.util.Either;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.server.Bootstrap;
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.levelgen.GenerationStep;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.TerrainAdjustment;
import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
import org.junit.BeforeClass;
import org.junit.Test;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
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.assertTrue;
public class NativeStructurePostProcessorSurfaceTerrainTest {
@BeforeClass
public static void bootstrapMinecraft() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void onlySurfaceBeardThinStructuresPrepareTerrain() {
assertTrue(NativeStructurePostProcessor.shouldPrepareSurfaceTerrain(
TerrainAdjustment.BEARD_THIN, GenerationStep.Decoration.SURFACE_STRUCTURES));
assertFalse(NativeStructurePostProcessor.shouldPrepareSurfaceTerrain(
TerrainAdjustment.BURY, GenerationStep.Decoration.SURFACE_STRUCTURES));
assertFalse(NativeStructurePostProcessor.shouldPrepareSurfaceTerrain(
TerrainAdjustment.BEARD_BOX, GenerationStep.Decoration.SURFACE_STRUCTURES));
assertFalse(NativeStructurePostProcessor.shouldPrepareSurfaceTerrain(
TerrainAdjustment.ENCAPSULATE, GenerationStep.Decoration.SURFACE_STRUCTURES));
}
@Test
public void undergroundAdjustmentsNeverPrepareSurfaceTerrain() {
for (TerrainAdjustment adjustment : List.of(
TerrainAdjustment.BEARD_THIN,
TerrainAdjustment.BURY,
TerrainAdjustment.BEARD_BOX,
TerrainAdjustment.ENCAPSULATE)) {
assertFalse(NativeStructurePostProcessor.shouldPrepareSurfaceTerrain(
adjustment, GenerationStep.Decoration.UNDERGROUND_STRUCTURES));
}
}
@Test
public void surfaceAnchorIsFlushInsideAndUnchangedAtRadius() {
NativeStructurePostProcessor.SurfaceAnchor anchor = anchor(80, 2);
assertEquals(80, NativeStructurePostProcessor.resolveSurfaceTarget(
List.of(anchor), 2, 2, 64));
assertEquals(64, NativeStructurePostProcessor.resolveSurfaceTarget(
List.of(anchor), 16, 2, 64));
assertEquals(64, NativeStructurePostProcessor.resolveSurfaceTarget(
List.of(anchor), 17, 2, 64));
}
@Test
public void surfaceAnchorRaisesAndLowersThroughTheTaper() {
NativeStructurePostProcessor.SurfaceAnchor raised = anchor(80, 2);
NativeStructurePostProcessor.SurfaceAnchor lowered = anchor(64, 2);
assertEquals(68, NativeStructurePostProcessor.resolveSurfaceTarget(
List.of(raised), 10, 2, 64));
assertEquals(76, NativeStructurePostProcessor.resolveSurfaceTarget(
List.of(lowered), 10, 2, 80));
}
@Test
public void containingRigidFloorsHaveDeterministicPriority() {
NativeStructurePostProcessor.SurfaceAnchor rigid = anchor(70, 2);
NativeStructurePostProcessor.SurfaceAnchor junction = anchor(90, 1);
assertEquals(70, NativeStructurePostProcessor.resolveSurfaceTarget(
List.of(rigid, junction), 2, 2, 64));
assertEquals(70, NativeStructurePostProcessor.resolveSurfaceTarget(
List.of(junction, rigid), 2, 2, 64));
NativeStructurePostProcessor.SurfaceAnchor weakTie = anchor(48, 1);
NativeStructurePostProcessor.SurfaceAnchor strongTie = anchor(80, 2);
assertEquals(80, NativeStructurePostProcessor.resolveSurfaceTarget(
List.of(weakTie, strongTie), 2, 2, 64));
assertEquals(80, NativeStructurePostProcessor.resolveSurfaceTarget(
List.of(strongTie, weakTie), 2, 2, 64));
}
@Test
public void containingFootprintOverridesAnAdjacentPiecesFalloff() {
NativeStructurePostProcessor.SurfaceAnchor local =
new NativeStructurePostProcessor.SurfaceAnchor(0, 4, 0, 4, 65, 2);
NativeStructurePostProcessor.SurfaceAnchor adjacent =
new NativeStructurePostProcessor.SurfaceAnchor(5, 9, 0, 4, 80, 2);
assertEquals(77, NativeStructurePostProcessor.resolveSurfaceTarget(
List.of(adjacent), 4, 2, 64));
assertEquals(65, NativeStructurePostProcessor.resolveSurfaceTarget(
List.of(local, adjacent), 4, 2, 64));
assertEquals(65, NativeStructurePostProcessor.resolveSurfaceTarget(
List.of(adjacent, local), 4, 2, 64));
}
@Test
public void opposingFalloffsBlendWithoutAnAbruptMidpointSeam() {
NativeStructurePostProcessor.SurfaceAnchor high =
new NativeStructurePostProcessor.SurfaceAnchor(0, 0, 0, 0, 80, 2);
NativeStructurePostProcessor.SurfaceAnchor low =
new NativeStructurePostProcessor.SurfaceAnchor(12, 12, 0, 0, 48, 2);
int previous = NativeStructurePostProcessor.resolveSurfaceTarget(
List.of(high, low), 0, 0, 64);
for (int x = 1; x <= 12; x++) {
int forward = NativeStructurePostProcessor.resolveSurfaceTarget(
List.of(high, low), x, 0, 64);
int reversed = NativeStructurePostProcessor.resolveSurfaceTarget(
List.of(low, high), x, 0, 64);
assertEquals(forward, reversed);
assertTrue(Math.abs(forward - previous) <= 4);
previous = forward;
}
assertEquals(64, NativeStructurePostProcessor.resolveSurfaceTarget(
List.of(high, low), 6, 0, 64));
}
@Test
public void surfaceColumnsMutateTerrainAndRemoveUnsupportedDecoration() {
Map<BlockPos, BlockState> lowered = new HashMap<>();
put(lowered, 0, 61, 0, Blocks.STONE.defaultBlockState());
put(lowered, 0, 62, 0, Blocks.DIRT.defaultBlockState());
put(lowered, 0, 63, 0, Blocks.DIRT.defaultBlockState());
put(lowered, 0, 64, 0, Blocks.GRASS_BLOCK.defaultBlockState());
put(lowered, 0, 65, 0, Blocks.DANDELION.defaultBlockState());
NativeStructurePostProcessor.applySurfaceColumn(
world(lowered), new BlockPos.MutableBlockPos(),
0, 0, 64, 62, -64, 319);
assertEquals(Blocks.STONE.defaultBlockState(), state(lowered, 0, 61, 0));
assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(lowered, 0, 62, 0));
assertEquals(Blocks.AIR.defaultBlockState(), state(lowered, 0, 63, 0));
assertEquals(Blocks.AIR.defaultBlockState(), state(lowered, 0, 64, 0));
assertEquals(Blocks.AIR.defaultBlockState(), state(lowered, 0, 65, 0));
Map<BlockPos, BlockState> raised = new HashMap<>();
put(raised, 0, 63, 0, Blocks.DIRT.defaultBlockState());
put(raised, 0, 64, 0, Blocks.GRASS_BLOCK.defaultBlockState());
put(raised, 0, 65, 0, Blocks.DANDELION.defaultBlockState());
NativeStructurePostProcessor.applySurfaceColumn(
world(raised), new BlockPos.MutableBlockPos(),
0, 0, 64, 68, -64, 319);
assertEquals(Blocks.DIRT.defaultBlockState(), state(raised, 0, 65, 0));
assertEquals(Blocks.DIRT.defaultBlockState(), state(raised, 0, 66, 0));
assertEquals(Blocks.DIRT.defaultBlockState(), state(raised, 0, 67, 0));
assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(raised, 0, 68, 0));
}
@Test
public void loweredFluidColumnsRemainFluidFilled() {
Map<BlockPos, BlockState> blocks = new HashMap<>();
put(blocks, 0, 62, 0, Blocks.DIRT.defaultBlockState());
put(blocks, 0, 63, 0, Blocks.DIRT.defaultBlockState());
put(blocks, 0, 64, 0, Blocks.GRAVEL.defaultBlockState());
put(blocks, 0, 65, 0, Blocks.WATER.defaultBlockState());
NativeStructurePostProcessor.applySurfaceColumn(
world(blocks), new BlockPos.MutableBlockPos(),
0, 0, 64, 62, -64, 319);
assertEquals(Blocks.WATER.defaultBlockState(), state(blocks, 0, 63, 0));
assertEquals(Blocks.WATER.defaultBlockState(), state(blocks, 0, 64, 0));
assertEquals(Blocks.WATER.defaultBlockState(), state(blocks, 0, 65, 0));
assertEquals(Blocks.GRAVEL.defaultBlockState(), state(blocks, 0, 62, 0));
}
@Test
public void singlePoolTemplateFieldMatchesTheRuntimeContract() {
Field field = NativeStructurePostProcessor.resolveSinglePoolTemplateField();
assertEquals(SinglePoolElement.class, field.getDeclaringClass());
assertEquals(Either.class, field.getType());
assertTrue(Modifier.isProtected(field.getModifiers()));
assertTrue(Modifier.isFinal(field.getModifiers()));
assertFalse(Modifier.isStatic(field.getModifiers()));
assertTrue(field.trySetAccessible());
}
@Test
public void runtimeTemplatesAndLegacyAirUseTheExactContract() {
StructureTemplate runtimeTemplate = new StructureTemplate();
assertEquals(runtimeTemplate, NativeStructurePostProcessor.resolveTemplateReference(
Either.right(runtimeTemplate), null));
assertFalse(NativeStructurePostProcessor.shouldClearLegacyAir(79, 80, false));
assertTrue(NativeStructurePostProcessor.shouldClearLegacyAir(80, 80, false));
assertTrue(NativeStructurePostProcessor.shouldClearLegacyAir(96, 80, false));
assertFalse(NativeStructurePostProcessor.shouldClearLegacyAir(96, 80, true));
}
@Test
public void rotatedTemplateAirClearsOnlyInsideTheChunkAndAboveTheFloor() throws Exception {
StructureTemplate.StructureBlockInfo clear = new StructureTemplate.StructureBlockInfo(
new BlockPos(1, 0, 0), Blocks.AIR.defaultBlockState(), null);
StructureTemplate.StructureBlockInfo belowFloor = new StructureTemplate.StructureBlockInfo(
new BlockPos(0, -1, 0), Blocks.AIR.defaultBlockState(), null);
StructureTemplate.StructureBlockInfo outsideChunk = new StructureTemplate.StructureBlockInfo(
new BlockPos(3, 0, 0), Blocks.AIR.defaultBlockState(), null);
StructureTemplate template = template(List.of(clear, belowFloor, outsideChunk));
BlockPos origin = new BlockPos(10, 80, 10);
StructurePlaceSettings settings = new StructurePlaceSettings().setRotation(Rotation.CLOCKWISE_90);
BlockPos clearPosition = origin.offset(StructureTemplate.calculateRelativePosition(settings, clear.pos()));
BlockPos belowFloorPosition = origin.offset(
StructureTemplate.calculateRelativePosition(settings, belowFloor.pos()));
BlockPos outsidePosition = origin.offset(
StructureTemplate.calculateRelativePosition(settings, outsideChunk.pos()));
BoundingBox area = new BoundingBox(
Math.min(clearPosition.getX(), belowFloorPosition.getX()),
Math.min(clearPosition.getY(), belowFloorPosition.getY()),
Math.min(clearPosition.getZ(), belowFloorPosition.getZ()),
Math.max(clearPosition.getX(), belowFloorPosition.getX()),
Math.max(clearPosition.getY(), belowFloorPosition.getY()),
Math.max(clearPosition.getZ(), belowFloorPosition.getZ()));
settings.setBoundingBox(area);
assertFalse(area.isInside(outsidePosition));
Map<BlockPos, BlockState> blocks = new HashMap<>();
blocks.put(clearPosition, Blocks.DIRT.defaultBlockState());
blocks.put(belowFloorPosition, Blocks.DIRT.defaultBlockState());
blocks.put(outsidePosition, Blocks.DIRT.defaultBlockState());
NativeStructurePostProcessor.clearTemplateAir(
world(blocks), template, origin, 80, settings);
assertEquals(Blocks.AIR.defaultBlockState(), blocks.get(clearPosition));
assertEquals(Blocks.DIRT.defaultBlockState(), blocks.get(belowFloorPosition));
assertEquals(Blocks.DIRT.defaultBlockState(), blocks.get(outsidePosition));
}
@Test
public void unrelatedPieceBoundsAreRejectedBeforeTemplateScanning() {
BoundingBox area = new BoundingBox(0, -64, 0, 15, 319, 15);
assertTrue(NativeStructurePostProcessor.intersects(
new BoundingBox(15, 60, 15, 30, 90, 30), area));
assertFalse(NativeStructurePostProcessor.intersects(
new BoundingBox(16, 60, 16, 30, 90, 30), area));
assertFalse(NativeStructurePostProcessor.intersects(
new BoundingBox(0, 320, 0, 15, 350, 15), area));
}
@Test
public void vegetationCleanupUsesTheSameCircularTaperAsTerrain() {
BoundingBox piece = new BoundingBox(0, 60, 0, 4, 80, 4);
assertTrue(NativeStructurePostProcessor.withinSurfaceTerrainRadius(16, 2, piece, 12));
assertFalse(NativeStructurePostProcessor.withinSurfaceTerrainRadius(16, 16, piece, 12));
}
private static NativeStructurePostProcessor.SurfaceAnchor anchor(int meetY, int strength) {
return new NativeStructurePostProcessor.SurfaceAnchor(0, 4, 0, 4, meetY, strength);
}
private static StructureTemplate template(
List<StructureTemplate.StructureBlockInfo> blocks) throws Exception {
Constructor<StructureTemplate.Palette> constructor =
StructureTemplate.Palette.class.getDeclaredConstructor(List.class);
assertTrue(constructor.trySetAccessible());
StructureTemplate.Palette palette = constructor.newInstance(blocks);
StructureTemplate template = new StructureTemplate();
template.palettes.add(palette);
return template;
}
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("hashCode")) {
return System.identityHashCode(proxy);
}
if (methodName.equals("equals")) {
return proxy == arguments[0];
}
if (methodName.equals("toString")) {
return "surface-test-world";
}
throw new UnsupportedOperationException(method.toString());
};
return (WorldGenLevel) Proxy.newProxyInstance(
WorldGenLevel.class.getClassLoader(), new Class<?>[]{WorldGenLevel.class}, handler);
}
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());
}
}
@@ -1,8 +1,10 @@
package art.arcane.iris.nativegen;
import net.minecraft.world.level.levelgen.GenerationStep;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -24,6 +26,22 @@ public class NativeStructurePostProcessorVegetationTest {
assertTrue(NativeStructurePostProcessor.shouldClearVegetationColumn(20, 100, true));
}
@Test
public void surfaceStructuresClearTheirEntireFootprintByDefault() {
assertTrue(NativeStructurePostProcessor.shouldClearEntireVegetationFootprint(
GenerationStep.Decoration.SURFACE_STRUCTURES, false));
assertTrue(NativeStructurePostProcessor.shouldClearEntireVegetationFootprint(
GenerationStep.Decoration.SURFACE_STRUCTURES, true));
}
@Test
public void undergroundStructuresPreserveSurfaceVegetationUnlessConfigured() {
assertFalse(NativeStructurePostProcessor.shouldClearEntireVegetationFootprint(
GenerationStep.Decoration.UNDERGROUND_STRUCTURES, false));
assertTrue(NativeStructurePostProcessor.shouldClearEntireVegetationFootprint(
GenerationStep.Decoration.UNDERGROUND_STRUCTURES, true));
}
@Test
public void allUndergroundGenerationStepsShareOneClassification() {
assertTrue(NativeStructurePostProcessor.isUndergroundStep(
@@ -35,4 +53,25 @@ public class NativeStructurePostProcessorVegetationTest {
assertFalse(NativeStructurePostProcessor.isUndergroundStep(
GenerationStep.Decoration.SURFACE_STRUCTURES));
}
@Test
public void undergroundStructuresUseTheLowestTerrainColumn() {
BoundingBox bounds = new BoundingBox(0, 60, 0, 1, 80, 0);
int offset = NativeStructurePostProcessor.resolveBuriedOffset(
bounds, 0, -64, 320, (x, z) -> x == 1 ? 76 : 100);
assertEquals(-5, offset);
}
@Test
public void undergroundStructuresFailWhenBurialWouldCrossWorldFloor() {
BoundingBox bounds = new BoundingBox(0, 60, 0, 1, 80, 0);
try {
NativeStructurePostProcessor.resolveBuriedOffset(
bounds, 0, 58, 320, (x, z) -> x == 1 ? 76 : 100);
} catch (IllegalStateException e) {
assertTrue(e.getMessage().contains("world minimum"));
return;
}
throw new AssertionError("Expected underground burial to fail below the world floor");
}
}