This commit is contained in:
Brian Neumann-Fopiano committed 2026-07-16 12:26:01 -04:00
1 parent 55b7460b9a
commit 699247b1de
327 files changed
+30138 -3934

No files matched your search

Whitespace-only changes.
@@ -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");
}
}
@@ -1,35 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.commands;
final class BukkitNativeStructureLocatePolicy {
private static final String MONUMENT_KEY = "minecraft:monument";
private static final String UNAVAILABLE_MESSAGE = "Native monument locating is unavailable on Paper, Purpur, and Folia because a cold search can stall the server thread. The monument lookup was skipped; monument generation is unaffected.";
private BukkitNativeStructureLocatePolicy() {
}
static boolean isUnavailable(String structureKey) {
return structureKey != null && MONUMENT_KEY.equalsIgnoreCase(structureKey.trim());
}
static String unavailableMessage() {
return UNAVAILABLE_MESSAGE;
}
}
@@ -21,12 +21,16 @@ package art.arcane.iris.core.commands;
import art.arcane.iris.Iris;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.core.service.ObjectStudioSaveService;
import art.arcane.iris.core.structure.NativeStructureLocateCapability;
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.engine.platform.EngineBukkitOps;
import art.arcane.iris.engine.framework.StructureReachability;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.util.common.director.specialhandlers.ObjectHandler;
import art.arcane.iris.util.common.director.specialhandlers.StructureHandler;
@@ -117,13 +121,34 @@ public class CommandFind implements DirectorExecutor {
return;
}
if (IrisStructureLocator.isPlaced(e, structure)) {
locateIrisStructure(e, structure, commandSender);
String structureKey = structure == null ? "" : structure.trim();
Structure nativeStructure = resolveNativeStructure(structureKey);
if (nativeStructure != null) {
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(
e, structureKey, false);
if (!decision.generate()
&& decision.status() != NativeStructureGenerationStatus.REPLACED_BY_IRIS) {
commandSender.sendMessage(C.RED + NativeStructureGenerationPolicy.generationStatusMessage(
structureKey, decision.status()));
return;
}
if (decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS) {
locateIrisStructure(e, structureKey, commandSender);
return;
}
if (NativeStructureLocateCapability.isPaperUnavailable(structureKey)) {
commandSender.sendMessage(C.RED + NativeStructureLocateCapability.unavailableMessage());
return;
}
}
if (nativeStructure == null && IrisStructureLocator.isPlaced(e, structureKey)) {
locateIrisStructure(e, structureKey, commandSender);
return;
}
if (BukkitNativeStructureLocatePolicy.isUnavailable(structure)) {
commandSender.sendMessage(C.RED + BukkitNativeStructureLocatePolicy.unavailableMessage());
if (nativeStructure == null) {
commandSender.sendMessage(C.RED + "Unknown structure: " + structureKey);
return;
}
@@ -135,42 +160,44 @@ public class CommandFind implements DirectorExecutor {
World targetWorld = target.getWorld();
Location origin = target.getLocation();
commandSender.sendMessage(C.GRAY + "Locating " + structure + "...");
commandSender.sendMessage(C.GRAY + "Locating " + structureKey + "...");
J.s(() -> {
try {
Registry<Structure> structureRegistry = RegistryAccess.registryAccess().getRegistry(RegistryKey.STRUCTURE);
Structure match = null;
for (Structure candidate : structureRegistry) {
NamespacedKey key = structureRegistry.getKey(candidate);
if (key != null && key.toString().equalsIgnoreCase(structure)) {
match = candidate;
break;
}
}
if (match == null) {
sendStructureMessage(target, commandSender, C.RED + "Unknown structure: " + structure);
return;
}
if (!StructureReachability.isReachable(e, structure)) {
KList<String> miss = StructureReachability.missingBiomeKeys(e, structure);
if (!StructureReachability.isReachable(e, structureKey)) {
KList<String> miss = StructureReachability.missingBiomeKeys(e, structureKey);
sendStructureMessage(target, commandSender,
C.YELLOW + structure + " cannot generate in this world (its required biomes are not produced by this pack"
C.YELLOW + structureKey + " cannot generate in this world (its required biomes are not produced by this pack"
+ (miss.isEmpty() ? "" : ": needs " + String.join("/", miss)) + ").");
return;
}
StructureSearchResult result = targetWorld.locateNearestStructure(origin, match, 100, false);
StructureSearchResult result = targetWorld.locateNearestStructure(
origin, nativeStructure, 100, false);
if (result == null || result.getLocation() == null) {
sendStructureMessage(target, commandSender, C.YELLOW + "No " + structure + " found within range of you.");
sendStructureMessage(target, commandSender,
C.YELLOW + "No " + structureKey + " found within range of you.");
return;
}
prepareStructureTeleport(target, targetWorld, commandSender, structure, result.getLocation(), false);
prepareStructureTeleport(
target, targetWorld, commandSender, structureKey, result.getLocation(), false);
} catch (Throwable t) {
sendStructureMessage(target, commandSender, C.RED + "Could not locate " + structure + ": " + t.getClass().getSimpleName());
Iris.reportError("Could not locate structure '" + structure + "'.", t);
sendStructureMessage(target, commandSender,
C.RED + "Could not locate " + structureKey + ": " + t.getClass().getSimpleName());
Iris.reportError("Could not locate structure '" + structureKey + "'.", t);
}
});
}
private static Structure resolveNativeStructure(String structureKey) {
Registry<Structure> structureRegistry = RegistryAccess.registryAccess().getRegistry(RegistryKey.STRUCTURE);
for (Structure candidate : structureRegistry) {
NamespacedKey key = structureRegistry.getKey(candidate);
if (key != null && key.toString().equalsIgnoreCase(structureKey)) {
return candidate;
}
}
return null;
}
private void locateIrisStructure(Engine engine, String structure, VolmitSender commandSender) {
Player target = player();
if (target == null) {
@@ -22,15 +22,13 @@ import art.arcane.iris.Iris;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.core.link.WorldEditLink;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.core.runtime.ObjectStudioActivation;
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
import art.arcane.iris.core.service.ObjectSVC;
import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.core.service.WandSVC;
import art.arcane.iris.core.tools.IrisConverter;
import art.arcane.iris.core.tools.PlausibilizeMode;
import art.arcane.iris.core.tools.TreePlausibilizer;
import art.arcane.iris.core.tools.TreePlausibilizeBatch;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IObjectPlacer;
import art.arcane.iris.engine.object.IrisDimension;
@@ -353,199 +351,63 @@ public class CommandObject implements DirectorExecutor {
}
}
@Director(description = "Make tree leaves vanilla-decay-plausible (every leaf within 6 blocks of a log)",
@Director(description = "Grow organic branches through the canopy so every leaf survives vanilla decay",
origin = DirectorOrigin.BOTH)
public void plausibilize(
@Param(description = "Object key, prefix (trees/), or filesystem path",
customHandler = ObjectTargetHandler.class)
String target,
@Param(description = "DEFAULT: tentacle logs, delete orphans. NORMALIZE: + flip persistent=false. FOLIAGE_OVERATURE: add leaves to bridge orphans, no deletions. SMOKE: wipe & repaint canopy shell.",
defaultValue = "DEFAULT")
PlausibilizeMode mode,
@Param(description = "Analyze only, do not write", defaultValue = "false")
@Param(name = "dryrun", description = "dryrun=true analyzes only, writes nothing", defaultValue = "false")
boolean dryRun,
@Param(description = "Canopy shell radius (SMOKE only), clamped [0,5]", defaultValue = "2")
int radius
@Param(name = "reach", description = "reach=N max branch length in blocks from existing wood; farther leaf clusters are pinned persistent instead. reach=0 grows unlimited", defaultValue = "12")
int reach
) {
List<Target> targets = resolveTargets(target);
IrisData nearest = data();
List<TreePlausibilizeBatch.Target> targets = TreePlausibilizeBatch.resolve(target, nearest);
if (targets.isEmpty() && nearest == null) {
targets = resolveFromPacks(target);
}
if (targets.isEmpty()) {
sender().sendMessage(C.RED + "No objects matched: " + target);
return;
}
sender().sendMessage(C.IRIS + "Plausibilize [" + mode.name()
+ (dryRun ? " DRY" : "")
+ (mode == PlausibilizeMode.SMOKE ? " r=" + radius : "")
sender().sendMessage(C.IRIS + "Plausibilize [reach=" + reach + (dryRun ? ", DRY" : "")
+ "] queued " + targets.size() + " object(s)");
org.bukkit.command.CommandSender s = sender();
J.a(() -> runPlausibilize(targets, dryRun, mode, radius, s));
List<TreePlausibilizeBatch.Target> queued = targets;
J.a(() -> TreePlausibilizeBatch.run(queued, dryRun, reach, nearest, (String line) ->
s.sendMessage(line.startsWith("Done:") || line.startsWith("Totals:") || line.startsWith("[")
? C.IRIS + line
: C.GRAY + " " + line)));
}
private List<Target> resolveTargets(String target) {
List<Target> out = new ArrayList<>();
if (target == null || target.isEmpty()) {
return out;
}
File direct = new File(target);
if (direct.isFile() && target.toLowerCase().endsWith(".iob")) {
out.add(new Target(direct.getName().replaceAll("\\.iob$", ""), direct));
return out;
}
if (direct.isDirectory()) {
walkIob(direct, direct, out);
return out;
}
IrisData irisData = data();
if (irisData != null) {
ResourceLoader<IrisObject> loader = irisData.getObjectLoader();
if (!target.endsWith("/") && loader.findFile(target) != null) {
out.add(new Target(target, null));
return out;
}
String prefix = target.endsWith("/") ? target : target + "/";
for (String k : loader.getPossibleKeys()) {
if (k.startsWith(prefix)) {
out.add(new Target(k, null));
}
}
return out;
}
private static List<TreePlausibilizeBatch.Target> resolveFromPacks(String target) {
List<TreePlausibilizeBatch.Target> out = new ArrayList<>();
File packsFolder = Iris.instance.getDataFolder("packs");
File[] packs = packsFolder.listFiles(File::isDirectory);
if (packs != null) {
for (File pack : packs) {
File objectsRoot = new File(pack, "objects");
if (!objectsRoot.isDirectory()) continue;
File candidate = new File(objectsRoot, target + ".iob");
if (candidate.isFile()) {
out.add(new Target(pack.getName() + "/" + target, candidate));
continue;
}
File candidateDir = new File(objectsRoot, target);
if (candidateDir.isDirectory()) {
walkIob(candidateDir, objectsRoot, out);
}
if (packs == null) {
return out;
}
for (File pack : packs) {
File objectsRoot = new File(pack, "objects");
if (!objectsRoot.isDirectory()) {
continue;
}
File candidate = new File(objectsRoot, target + ".iob");
if (candidate.isFile()) {
out.add(new TreePlausibilizeBatch.Target(pack.getName() + "/" + target, candidate));
continue;
}
File candidateDir = new File(objectsRoot, target);
if (candidateDir.isDirectory()) {
TreePlausibilizeBatch.walkIob(candidateDir, objectsRoot, out);
}
}
return out;
}
private static void walkIob(File root, File keyRoot, List<Target> out) {
File[] kids = root.listFiles();
if (kids == null) return;
for (File f : kids) {
if (f.isDirectory()) {
walkIob(f, keyRoot, out);
} else if (f.getName().toLowerCase().endsWith(".iob")) {
String rel = keyRoot.toPath().relativize(f.toPath()).toString()
.replace(File.separatorChar, '/')
.replaceAll("\\.iob$", "");
out.add(new Target(rel, f));
}
}
}
private record Target(String key, File file) {
}
private static void runPlausibilize(
List<Target> targets,
boolean dryRun,
PlausibilizeMode mode,
int radius,
org.bukkit.command.CommandSender s
) {
int processed = 0;
int skipped = 0;
int failed = 0;
int changed = 0;
long totalLogsAdded = 0L;
long totalLeavesAdded = 0L;
long totalLeavesRemoved = 0L;
long totalNormalized = 0L;
long totalUnreachableAfter = 0L;
int progressStep = Math.max(1, targets.size() / 20);
int index = 0;
for (Target t : targets) {
index++;
try {
IrisObject o = loadTarget(t);
if (o == null) {
s.sendMessage(C.YELLOW + " skip " + t.key() + ": failed to load");
skipped++;
continue;
}
TreePlausibilizer.Result r = dryRun
? TreePlausibilizer.analyze(o, mode, radius)
: TreePlausibilizer.apply(o, mode, radius);
if (r.skipReason() != null) {
s.sendMessage(C.YELLOW + " skip " + t.key() + ": " + r.skipReason());
skipped++;
continue;
}
boolean touched = r.logsAdded() > 0 || r.leavesAdded() > 0
|| r.leavesRemoved() > 0 || r.leavesNormalized() > 0;
if (!dryRun && touched) {
File dest = o.getLoadFile() != null ? o.getLoadFile() : t.file();
if (dest != null) {
o.write(dest);
changed++;
}
}
processed++;
totalLogsAdded += r.logsAdded();
totalLeavesAdded += r.leavesAdded();
totalLeavesRemoved += r.leavesRemoved();
totalNormalized += r.leavesNormalized();
totalUnreachableAfter += r.unreachableAfter();
if (touched || targets.size() == 1) {
s.sendMessage(C.GRAY + " " + t.key()
+ C.WHITE + " +" + r.logsAdded() + " logs"
+ C.WHITE + " +" + r.leavesAdded() + " leaves"
+ C.WHITE + " -" + r.leavesRemoved() + " removed"
+ (r.leavesNormalized() > 0 ? C.WHITE + " ~" + r.leavesNormalized() + " normalized" : "")
+ (r.unreachableAfter() > 0 ? C.YELLOW + " " + r.unreachableAfter() + " unreachable" : ""));
}
if (targets.size() > 1 && index % progressStep == 0) {
s.sendMessage(C.IRIS + " [" + index + "/" + targets.size() + "]");
}
} catch (Throwable e) {
s.sendMessage(C.RED + " fail " + t.key() + ": " + e.getClass().getSimpleName()
+ ": " + e.getMessage());
e.printStackTrace();
failed++;
}
}
s.sendMessage(C.IRIS + "Done: " + processed + " processed, " + changed + " changed, "
+ skipped + " skipped, " + failed + " failed");
s.sendMessage(C.IRIS + "Totals: +" + totalLogsAdded + " logs, +" + totalLeavesAdded + " leaves, -"
+ totalLeavesRemoved + " removed, ~" + totalNormalized + " normalized, "
+ totalUnreachableAfter + " unreachable");
}
private static IrisObject loadTarget(Target t) throws IOException {
if (t.file() != null) {
IrisObject o = new IrisObject();
o.read(t.file());
o.setLoadFile(t.file());
return o;
}
return IrisData.loadAnyObject(t.key(), null);
}
@Director(description = "Convert .schem files in the 'convert' folder to .iob files.")
public void convert () {
try {
@@ -21,18 +21,23 @@ package art.arcane.iris.core.commands;
import art.arcane.iris.Iris;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.structure.BulkStructureImporter;
import art.arcane.iris.core.structure.NativeStructureLocateCapability;
import art.arcane.iris.core.structure.StructureCaptureImporter;
import art.arcane.iris.core.structure.StructureImporter;
import art.arcane.iris.core.structure.StructureIndexService;
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.engine.framework.PlacedStructurePiece;
import art.arcane.iris.engine.framework.StructureAssembler;
import art.arcane.iris.engine.framework.StructureReachability;
import art.arcane.iris.engine.object.IObjectPlacer;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.iris.engine.object.IrisStructure;
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
import art.arcane.iris.engine.object.ObjectPlaceMode;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.util.common.director.DirectorExecutor;
@@ -158,14 +163,23 @@ public class CommandStructure implements DirectorExecutor {
private void runVerification(Engine engine, KList<String> structureKeys, int centerX, int centerZ,
int searchRadius, VolmitSender commandSender, Player target) {
KList<String> messages = new KList<>();
Set<String> reachable;
try {
reachable = StructureReachability.reachableKeys(engine);
} catch (Throwable error) {
messages.add(C.RED + "Structure verification could not resolve native biome reachability.");
sendVerificationMessages(commandSender, target, messages);
Iris.reportError("Could not resolve native structure biome reachability for verification.", error);
return;
Map<String, IrisNativeStructureDecision> decisions = new HashMap<>(structureKeys.size());
boolean requiresNativeReachability = false;
for (String keyName : structureKeys) {
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine, keyName, false);
decisions.put(keyName, decision);
requiresNativeReachability |= decision.generate();
}
Set<String> reachable = Set.of();
if (requiresNativeReachability) {
try {
reachable = StructureReachability.reachableKeys(engine);
} catch (Throwable error) {
messages.add(C.RED + "Structure verification could not resolve native biome reachability.");
sendVerificationMessages(commandSender, target, messages);
Iris.reportError("Could not resolve native structure biome reachability for verification.", error);
return;
}
}
int located = 0;
int nativeEligible = 0;
@@ -175,8 +189,8 @@ public class CommandStructure implements DirectorExecutor {
int searchLimited = 0;
int errors = 0;
for (String keyName : structureKeys) {
boolean irisPlaced = IrisStructureLocator.isPlaced(engine, keyName);
if (irisPlaced) {
IrisNativeStructureDecision decision = decisions.get(keyName);
if (decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS) {
try {
IrisStructureLocator.LocateResult result =
IrisStructureLocator.locate(engine, keyName, centerX, centerZ, searchRadius);
@@ -201,7 +215,7 @@ public class CommandStructure implements DirectorExecutor {
}
continue;
}
if (!engine.getDimension().getImportedStructures().shouldGenerate(keyName)) {
if (!decision.generate()) {
disabled++;
messages.add(C.GRAY + "[disabled] " + C.WHITE + keyName);
continue;
@@ -213,20 +227,20 @@ public class CommandStructure implements DirectorExecutor {
+ (missing.isEmpty() ? "" : C.YELLOW + " needs " + String.join("/", missing)));
continue;
}
if (BukkitNativeStructureLocatePolicy.isUnavailable(keyName)) {
nativeEligible++;
if (NativeStructureLocateCapability.isPaperUnavailable(keyName)) {
unavailable++;
messages.add(C.RED + "[unavailable] " + C.WHITE + keyName + C.RED + ": "
+ BukkitNativeStructureLocatePolicy.unavailableMessage());
messages.add(C.YELLOW + "[native-eligible/locate-unavailable] " + C.WHITE + keyName + C.YELLOW + ": "
+ NativeStructureLocateCapability.unavailableMessage());
continue;
}
nativeEligible++;
messages.add(C.GREEN + "[native-eligible] " + C.WHITE + keyName);
}
messages.add(C.GREEN + "Structure verify: " + C.WHITE + located + C.GREEN + " Iris placements located, "
+ C.WHITE + nativeEligible + C.GREEN + " native structures eligible, "
+ C.WHITE + disabled + C.GREEN + " disabled by policy, "
+ C.WHITE + unreachable + C.GREEN + " biome-unreachable, "
+ C.WHITE + unavailable + C.GREEN + " unavailable on this platform, "
+ C.WHITE + unavailable + C.GREEN + " generation-eligible but unavailable to synchronous locate, "
+ C.WHITE + searchLimited + C.GREEN + " density searches safety-limited, "
+ C.WHITE + errors + C.GREEN + " errors. Native eligibility is checked without running blocking live locates.");
sendVerificationMessages(commandSender, target, messages);
@@ -285,12 +299,13 @@ public class CommandStructure implements DirectorExecutor {
sender().sendMessage(C.RED + "Could not resolve the pack for dimension " + dimension.getLoadKey());
return;
}
IrisStructure s = IrisData.loadAnyStructure(structure, data);
IrisStructure s = data.load(IrisStructure.class, structure, false);
if (s == null) {
sender().sendMessage(C.RED + "No iris structure '" + structure + "' in this pack");
return;
}
StructureAssembler assembler = new StructureAssembler(data, s, 0, 64, 0);
StructureAssembler assembler = StructureAssembler.forData(
data, s, new IrisPosition(0, 64, 0));
KList<PlacedStructurePiece> pieces = assembler.assemble(new RNG(1234));
if (pieces == null || pieces.isEmpty()) {
sender().sendMessage(C.RED + "Structure '" + structure + "' assembled 0 pieces (check startPool '" + s.getStartPool() + "')");
@@ -321,13 +336,14 @@ public class CommandStructure implements DirectorExecutor {
sender().sendMessage(C.RED + "Could not resolve the pack for dimension " + dimension.getLoadKey());
return;
}
IrisStructure s = IrisData.loadAnyStructure(structure, data);
IrisStructure s = data.load(IrisStructure.class, structure, false);
if (s == null) {
sender().sendMessage(C.RED + "No iris structure '" + structure + "' in this pack");
return;
}
Location loc = player().getLocation();
StructureAssembler assembler = new StructureAssembler(data, s, loc.getBlockX(), loc.getBlockY(), loc.getBlockZ());
StructureAssembler assembler = StructureAssembler.forData(
data, s, new IrisPosition(loc.getBlockX(), loc.getBlockY(), loc.getBlockZ()));
RNG rng = new RNG((long) loc.getBlockX() * 341873128712L + loc.getBlockZ());
KList<PlacedStructurePiece> pieces = assembler.assemble(rng);
if (pieces == null || pieces.isEmpty()) {
@@ -26,6 +26,7 @@ import art.arcane.iris.core.gui.NoiseExplorerGUI;
import art.arcane.iris.core.gui.VisionGUI;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.project.IrisProject;
import art.arcane.iris.core.service.BoardSVC;
import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.core.structure.BulkStructureImporter;
import art.arcane.iris.core.structure.FeatureImporter;
@@ -193,6 +194,25 @@ public class CommandStudio implements DirectorExecutor {
}));
}
@Director(description = "Toggle your Studio debug scoreboard", aliases = {"board", "sidebar", "sb"}, origin = DirectorOrigin.PLAYER)
public void scoreboard() {
Player target = player();
VolmitSender commandSender = sender();
if (!J.runEntity(target, () -> {
PlatformChunkGenerator generator = IrisToolbelt.access(target.getWorld());
if (generator == null || !generator.isStudio()) {
commandSender.sendMessage(C.RED + "You must be in a Studio world to toggle the debug scoreboard.");
return;
}
boolean visible = Iris.service(BoardSVC.class).toggle(target);
commandSender.sendMessage((visible ? C.GREEN : C.YELLOW)
+ "Studio debug scoreboard " + (visible ? "enabled." : "disabled."));
})) {
commandSender.sendMessage(C.RED + "Could not update the Studio debug scoreboard right now.");
}
}
@Director(description = "Create a new studio project", aliases = "+", sync = true)
public void create(
@Param(description = "The name of this new Iris Project.", defaultValue = "studio")
@@ -259,7 +279,7 @@ public class CommandStudio implements DirectorExecutor {
Inventory inv = Bukkit.createInventory(null, 27 * 2);
try {
EngineBukkitOps.addItems(engine(), true, inv, RNG.r, tables, InventorySlotType.STORAGE, player().getWorld(), player().getLocation().getBlockX(), player().getLocation().getBlockY(), player().getLocation().getBlockZ(), 1);
EngineBukkitOps.addItems(engine(), true, inv, tables, InventorySlotType.STORAGE, player().getWorld(), player().getLocation().getBlockX(), player().getLocation().getBlockY(), player().getLocation().getBlockZ());
} catch (Throwable e) {
Iris.reportError(e);
sender().sendMessage(C.RED + "Cannot add items to virtual inventory because of: " + e.getMessage());
@@ -286,7 +306,7 @@ public class CommandStudio implements DirectorExecutor {
inv.clear();
}
EngineBukkitOps.addItems(engine, true, inv, new RNG(RNG.r.imax()), tables, InventorySlotType.STORAGE, player.getWorld(), player.getLocation().getBlockX(), player.getLocation().getBlockY(), player.getLocation().getBlockZ(), 1);
EngineBukkitOps.addItems(engine, true, inv, tables, InventorySlotType.STORAGE, player.getWorld(), player.getLocation().getBlockX(), player.getLocation().getBlockY(), player.getLocation().getBlockZ());
}, fast ? 5 : 35));
sender().sendMessage(C.GREEN + "Opening inventory now!");
@@ -829,7 +849,7 @@ public class CommandStudio implements DirectorExecutor {
sender().sendMessage(C.RED + "No studio world is open!");
return true;
}
if (!engine().isStudio()) {
if (!IrisToolbelt.isStudio(world())) {
sender().sendMessage(C.RED + "You must be in a studio world!");
return true;
}
@@ -1,32 +0,0 @@
package art.arcane.iris.core.commands;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class BukkitNativeStructureLocatePolicyTest {
@Test
public void nativeMonumentLocateIsUnavailable() {
assertTrue(BukkitNativeStructureLocatePolicy.isUnavailable("minecraft:monument"));
assertTrue(BukkitNativeStructureLocatePolicy.isUnavailable(" MINECRAFT:MONUMENT "));
}
@Test
public void unrelatedAndNonCanonicalKeysRemainAvailable() {
assertFalse(BukkitNativeStructureLocatePolicy.isUnavailable(null));
assertFalse(BukkitNativeStructureLocatePolicy.isUnavailable(""));
assertFalse(BukkitNativeStructureLocatePolicy.isUnavailable("minecraft:stronghold"));
assertFalse(BukkitNativeStructureLocatePolicy.isUnavailable("minecraft:ocean_monument"));
assertFalse(BukkitNativeStructureLocatePolicy.isUnavailable("minecraft:ocean_monuments"));
}
@Test
public void rejectionExplainsSafetyAndGenerationBehavior() {
String message = BukkitNativeStructureLocatePolicy.unavailableMessage().toLowerCase();
assertTrue(message.contains("unavailable"));
assertTrue(message.contains("stall the server thread"));
assertTrue(message.contains("generation is unaffected"));
}
}
@@ -1,11 +1,14 @@
package art.arcane.iris.core.commands;
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -21,6 +24,47 @@ public class IrisStructureLocateCommandContractTest {
assertFalse(source.contains("at[2] + 8"));
}
@Test
public void findRoutesOnlyExplicitNativeReplacementThroughIrisLocate() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.commandFindSource")));
int methodStart = source.indexOf("public void structure(");
int methodEnd = source.indexOf("private static Structure resolveNativeStructure", methodStart);
String method = source.substring(methodStart, methodEnd);
int nativeResolution = method.indexOf("resolveNativeStructure(structureKey)");
int policyResolution = method.indexOf("NativeStructureGenerationPolicy.resolve(", nativeResolution);
int replacementCheck = method.indexOf(
"decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS", policyResolution);
int replacementLocate = method.indexOf("locateIrisStructure(e, structureKey, commandSender)", replacementCheck);
int capabilityCheck = method.indexOf("NativeStructureLocateCapability.isPaperUnavailable(structureKey)", replacementLocate);
int genericIrisLookup = method.indexOf(
"nativeStructure == null && IrisStructureLocator.isPlaced(e, structureKey)", capabilityCheck);
int nativeLocate = method.indexOf("targetWorld.locateNearestStructure(", genericIrisLookup);
assertTrue(nativeResolution >= 0);
assertTrue(policyResolution > nativeResolution);
assertTrue(replacementCheck > policyResolution);
assertTrue(replacementLocate > replacementCheck);
assertTrue(capabilityCheck > replacementLocate);
assertTrue(genericIrisLookup > capabilityCheck);
assertTrue(nativeLocate > policyResolution);
assertTrue(method.contains("decision.status() != NativeStructureGenerationStatus.REPLACED_BY_IRIS"));
}
@Test
public void findNativePolicyMessagesMatchModdedDiagnostics() {
assertEquals(
"Native structure minecraft:village_plains is disabled by this dimension's importedStructures settings.",
NativeStructureGenerationPolicy.generationStatusMessage(
"minecraft:village_plains", NativeStructureGenerationStatus.DISABLED_BY_PACK));
assertEquals(
"Native structure minecraft:ancient_city is replaced by an Iris placement in this pack and locates through that explicit replacement.",
NativeStructureGenerationPolicy.generationStatusMessage(
"minecraft:ancient_city", NativeStructureGenerationStatus.REPLACED_BY_IRIS));
assertEquals(
"Native structure minecraft:stronghold generates natively.",
NativeStructureGenerationPolicy.generationStatusMessage(
"minecraft:stronghold", NativeStructureGenerationStatus.GENERATE_NATIVE));
}
@Test
public void structureVerifyReportsDensityLimitAndUsesExactLocatedOrigin() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.commandStructureSource")));
@@ -31,4 +75,21 @@ public class IrisStructureLocateCommandContractTest {
assertFalse(source.contains("at[0] + 8"));
assertFalse(source.contains("at[2] + 8"));
}
@Test
public void structureVerifyPartitionsPolicyBeforeNativeReachability() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.commandStructureSource")));
int methodStart = source.indexOf("private void runVerification(");
int methodEnd = source.indexOf("private void sendVerificationMessages(", methodStart);
String method = source.substring(methodStart, methodEnd);
int policyResolution = method.indexOf("NativeStructureGenerationPolicy.resolve(engine, keyName, false)");
int nativeRequirement = method.indexOf("requiresNativeReachability |= decision.generate()", policyResolution);
int reachabilityGuard = method.indexOf("if (requiresNativeReachability)", nativeRequirement);
int reachabilityLookup = method.indexOf("StructureReachability.reachableKeys(engine)", reachabilityGuard);
assertTrue(policyResolution >= 0);
assertTrue(nativeRequirement > policyResolution);
assertTrue(reachabilityGuard > nativeRequirement);
assertTrue(reachabilityLookup > reachabilityGuard);
}
}
@@ -0,0 +1,25 @@
package art.arcane.iris.core.commands;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import org.junit.Test;
import java.lang.reflect.Method;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class StudioScoreboardCommandTest {
@Test
public void scoreboardCommandIsPlayerScopedAndEntityScheduled() throws NoSuchMethodException {
Method command = CommandStudio.class.getDeclaredMethod("scoreboard");
Director director = command.getAnnotation(Director.class);
assertEquals(DirectorOrigin.PLAYER, director.origin());
assertFalse(director.sync());
assertTrue(List.of(director.aliases()).contains("board"));
assertTrue(List.of(director.aliases()).contains("sidebar"));
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+152 -13
View File
@@ -1,9 +1,102 @@
[21:25:27] [Test worker/INFO]: Iris registered custom content provider 'iris_deferred_test'
[21:25:27] [Test worker/ERROR]: [worldcheck] server stop request failed
[05:44:33] [Test worker/INFO]: Iris registered custom content provider 'iris_deferred_test'
[05:44:33] [Test worker/ERROR]: Iris service rollback failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
java.lang.RuntimeException: cleanup failed
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47)
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65)
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19)
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88)
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source)
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63)
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122)
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
[05:44:33] [Test worker/ERROR]: Iris service onEnable failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
java.lang.RuntimeException: enable failed
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:31)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47)
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65)
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19)
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88)
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source)
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63)
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122)
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
Suppressed: java.lang.RuntimeException: cleanup failed
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32)
... 42 more
[05:44:33] [Test worker/ERROR]: [worldcheck] server stop request failed
java.lang.IllegalStateException: stop request failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$stopRequestFailureForcesNonzeroResult$1(ModdedWorldCheckTest.java:218)
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:171)
at art.arcane.iris.modded.ModdedWorldCheckTest.stopRequestFailureForcesNonzeroResult(ModdedWorldCheckTest.java:215)
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$stopRequestFailureForcesNonzeroResult$1(ModdedWorldCheckTest.java:235)
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:176)
at art.arcane.iris.modded.ModdedWorldCheckTest.stopRequestFailureForcesNonzeroResult(ModdedWorldCheckTest.java:232)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
@@ -46,11 +139,11 @@ java.lang.IllegalStateException: stop request failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
[21:25:27] [Test worker/ERROR]: [worldcheck] waiting for server shutdown failed
[05:44:33] [Test worker/ERROR]: [worldcheck] waiting for server shutdown failed
java.lang.IllegalStateException: shutdown wait failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$shutdownWaitFailureForcesNonzeroExit$0(ModdedWorldCheckTest.java:244)
at art.arcane.iris.modded.ModdedWorldCheck.awaitStopAndExit(ModdedWorldCheck.java:186)
at art.arcane.iris.modded.ModdedWorldCheckTest.shutdownWaitFailureForcesNonzeroExit(ModdedWorldCheckTest.java:242)
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$shutdownWaitFailureForcesNonzeroExit$0(ModdedWorldCheckTest.java:261)
at art.arcane.iris.modded.ModdedWorldCheck.awaitStopAndExit(ModdedWorldCheck.java:191)
at art.arcane.iris.modded.ModdedWorldCheckTest.shutdownWaitFailureForcesNonzeroExit(ModdedWorldCheckTest.java:259)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
@@ -93,11 +186,57 @@ java.lang.IllegalStateException: shutdown wait failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
[21:25:27] [Test worker/ERROR]: [worldcheck] check failed
[05:44:33] [Test worker/ERROR]: [worldcheck] check failed
java.lang.IllegalStateException: check failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$thrownCheckStillRequestsStop$0(ModdedWorldCheckTest.java:204)
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:166)
at art.arcane.iris.modded.ModdedWorldCheckTest.thrownCheckStillRequestsStop(ModdedWorldCheckTest.java:202)
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$thrownCheckStillRequestsStop$0(ModdedWorldCheckTest.java:221)
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:171)
at art.arcane.iris.modded.ModdedWorldCheckTest.thrownCheckStillRequestsStop(ModdedWorldCheckTest.java:219)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47)
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65)
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19)
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88)
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source)
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63)
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122)
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
[05:44:33] [Test worker/INFO]: Iris registered custom content provider 'iris_discovery_success'
[05:44:33] [Test worker/ERROR]: Iris custom content provider discovery failed
java.lang.RuntimeException: provider init failed
at art.arcane.iris.modded.api.ModdedCustomContentRegistryTest.failedDiscoveryPublishesNothingAndPreservesTheCause(ModdedCustomContentRegistryTest.java:39)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
@@ -21,28 +21,20 @@ package art.arcane.iris.fabric;
import art.arcane.iris.modded.ModdedForcedDatapack;
import net.minecraft.server.packs.repository.PackRepository;
import net.minecraft.server.packs.repository.RepositorySource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.LinkedHashSet;
import java.util.Objects;
import java.util.Set;
public final class FabricForcedDatapackSources {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private FabricForcedDatapackSources() {
}
public static void attach(PackRepository repository) {
if (repository == null) {
return;
}
try {
Set<RepositorySource> merged = new LinkedHashSet<>(repository.sources);
merged.add(ModdedForcedDatapack.repositorySource());
repository.sources = merged;
} catch (Throwable e) {
LOGGER.error("Iris failed to attach the forced startup datapack source to the server data pack repository", e);
}
PackRepository activeRepository = Objects.requireNonNull(repository,
"Iris cannot attach the forced startup datapack source to a null repository");
Set<RepositorySource> merged = new LinkedHashSet<>(activeRepository.sources);
merged.add(ModdedForcedDatapack.repositorySource());
activeRepository.sources = merged;
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+155 -16
View File
@@ -1,12 +1,105 @@
[13Jul2026 21:25:26.811] [Test worker/DEBUG] [io.netty.util.internal.logging.InternalLoggerFactory/]: Using SLF4J as the default logging framework
[13Jul2026 21:25:26.813] [Test worker/DEBUG] [io.netty.util.ResourceLeakDetector/]: -Dio.netty.leakDetection.level: simple
[13Jul2026 21:25:26.813] [Test worker/DEBUG] [io.netty.util.ResourceLeakDetector/]: -Dio.netty.leakDetection.targetRecords: 4
[13Jul2026 21:25:28.681] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_deferred_test'
[13Jul2026 21:25:28.758] [Test worker/ERROR] [Iris/]: [worldcheck] server stop request failed
[16Jul2026 05:44:24.794] [Test worker/DEBUG] [io.netty.util.internal.logging.InternalLoggerFactory/]: Using SLF4J as the default logging framework
[16Jul2026 05:44:24.795] [Test worker/DEBUG] [io.netty.util.ResourceLeakDetector/]: -Dio.netty.leakDetection.level: simple
[16Jul2026 05:44:24.795] [Test worker/DEBUG] [io.netty.util.ResourceLeakDetector/]: -Dio.netty.leakDetection.targetRecords: 4
[16Jul2026 05:44:26.716] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_deferred_test'
[16Jul2026 05:44:26.771] [Test worker/ERROR] [Iris/]: Iris service rollback failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
java.lang.RuntimeException: cleanup failed
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.run(ParentRunner.java:413) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:137) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:115) ~[junit-4.13.2.jar:4.13.2]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65) ~[?:?]
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53) ~[?:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19) ~[?:?]
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) ~[?:?]
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88) ~[?:?]
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) ~[?:?]
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[16Jul2026 05:44:26.779] [Test worker/ERROR] [Iris/]: Iris service onEnable failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
java.lang.RuntimeException: enable failed
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:31) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.run(ParentRunner.java:413) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:137) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:115) ~[junit-4.13.2.jar:4.13.2]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65) ~[?:?]
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53) ~[?:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19) ~[?:?]
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) ~[?:?]
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88) ~[?:?]
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) ~[?:?]
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
Suppressed: java.lang.RuntimeException: cleanup failed
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32) ~[test/:?]
... 42 more
[16Jul2026 05:44:26.814] [Test worker/ERROR] [Iris/]: [worldcheck] server stop request failed
java.lang.IllegalStateException: stop request failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$stopRequestFailureForcesNonzeroResult$1(ModdedWorldCheckTest.java:218) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:171) ~[main/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.stopRequestFailureForcesNonzeroResult(ModdedWorldCheckTest.java:215) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$stopRequestFailureForcesNonzeroResult$1(ModdedWorldCheckTest.java:235) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:176) ~[main/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.stopRequestFailureForcesNonzeroResult(ModdedWorldCheckTest.java:232) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
@@ -49,11 +142,11 @@ java.lang.IllegalStateException: stop request failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[13Jul2026 21:25:28.765] [Test worker/ERROR] [Iris/]: [worldcheck] waiting for server shutdown failed
[16Jul2026 05:44:26.821] [Test worker/ERROR] [Iris/]: [worldcheck] waiting for server shutdown failed
java.lang.IllegalStateException: shutdown wait failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$shutdownWaitFailureForcesNonzeroExit$0(ModdedWorldCheckTest.java:244) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.awaitStopAndExit(ModdedWorldCheck.java:186) ~[main/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.shutdownWaitFailureForcesNonzeroExit(ModdedWorldCheckTest.java:242) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$shutdownWaitFailureForcesNonzeroExit$0(ModdedWorldCheckTest.java:261) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.awaitStopAndExit(ModdedWorldCheck.java:191) ~[main/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.shutdownWaitFailureForcesNonzeroExit(ModdedWorldCheckTest.java:259) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
@@ -96,11 +189,57 @@ java.lang.IllegalStateException: shutdown wait failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[13Jul2026 21:25:28.769] [Test worker/ERROR] [Iris/]: [worldcheck] check failed
[16Jul2026 05:44:26.826] [Test worker/ERROR] [Iris/]: [worldcheck] check failed
java.lang.IllegalStateException: check failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$thrownCheckStillRequestsStop$0(ModdedWorldCheckTest.java:204) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:166) ~[main/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.thrownCheckStillRequestsStop(ModdedWorldCheckTest.java:202) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$thrownCheckStillRequestsStop$0(ModdedWorldCheckTest.java:221) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:171) ~[main/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.thrownCheckStillRequestsStop(ModdedWorldCheckTest.java:219) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.run(ParentRunner.java:413) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:137) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:115) ~[junit-4.13.2.jar:4.13.2]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65) ~[?:?]
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53) ~[?:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19) ~[?:?]
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) ~[?:?]
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88) ~[?:?]
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) ~[?:?]
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[16Jul2026 05:44:26.832] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_discovery_success'
[16Jul2026 05:44:26.833] [Test worker/ERROR] [Iris/]: Iris custom content provider discovery failed
java.lang.RuntimeException: provider init failed
at art.arcane.iris.modded.api.ModdedCustomContentRegistryTest.failedDiscoveryPublishesNothingAndPreservesTheCause(ModdedCustomContentRegistryTest.java:39) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
+152 -13
View File
@@ -1,9 +1,102 @@
[13Jul2026 21:25:28.681] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_deferred_test'
[13Jul2026 21:25:28.758] [Test worker/ERROR] [Iris/]: [worldcheck] server stop request failed
[16Jul2026 05:44:26.716] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_deferred_test'
[16Jul2026 05:44:26.771] [Test worker/ERROR] [Iris/]: Iris service rollback failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
java.lang.RuntimeException: cleanup failed
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.run(ParentRunner.java:413) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:137) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:115) ~[junit-4.13.2.jar:4.13.2]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65) ~[?:?]
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53) ~[?:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19) ~[?:?]
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) ~[?:?]
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88) ~[?:?]
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) ~[?:?]
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[16Jul2026 05:44:26.779] [Test worker/ERROR] [Iris/]: Iris service onEnable failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
java.lang.RuntimeException: enable failed
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:31) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.run(ParentRunner.java:413) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:137) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:115) ~[junit-4.13.2.jar:4.13.2]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65) ~[?:?]
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53) ~[?:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19) ~[?:?]
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) ~[?:?]
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88) ~[?:?]
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) ~[?:?]
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
Suppressed: java.lang.RuntimeException: cleanup failed
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32) ~[test/:?]
... 42 more
[16Jul2026 05:44:26.814] [Test worker/ERROR] [Iris/]: [worldcheck] server stop request failed
java.lang.IllegalStateException: stop request failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$stopRequestFailureForcesNonzeroResult$1(ModdedWorldCheckTest.java:218) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:171) ~[main/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.stopRequestFailureForcesNonzeroResult(ModdedWorldCheckTest.java:215) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$stopRequestFailureForcesNonzeroResult$1(ModdedWorldCheckTest.java:235) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:176) ~[main/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.stopRequestFailureForcesNonzeroResult(ModdedWorldCheckTest.java:232) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
@@ -46,11 +139,11 @@ java.lang.IllegalStateException: stop request failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[13Jul2026 21:25:28.765] [Test worker/ERROR] [Iris/]: [worldcheck] waiting for server shutdown failed
[16Jul2026 05:44:26.821] [Test worker/ERROR] [Iris/]: [worldcheck] waiting for server shutdown failed
java.lang.IllegalStateException: shutdown wait failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$shutdownWaitFailureForcesNonzeroExit$0(ModdedWorldCheckTest.java:244) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.awaitStopAndExit(ModdedWorldCheck.java:186) ~[main/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.shutdownWaitFailureForcesNonzeroExit(ModdedWorldCheckTest.java:242) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$shutdownWaitFailureForcesNonzeroExit$0(ModdedWorldCheckTest.java:261) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.awaitStopAndExit(ModdedWorldCheck.java:191) ~[main/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.shutdownWaitFailureForcesNonzeroExit(ModdedWorldCheckTest.java:259) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
@@ -93,11 +186,57 @@ java.lang.IllegalStateException: shutdown wait failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[13Jul2026 21:25:28.769] [Test worker/ERROR] [Iris/]: [worldcheck] check failed
[16Jul2026 05:44:26.826] [Test worker/ERROR] [Iris/]: [worldcheck] check failed
java.lang.IllegalStateException: check failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$thrownCheckStillRequestsStop$0(ModdedWorldCheckTest.java:204) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:166) ~[main/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.thrownCheckStillRequestsStop(ModdedWorldCheckTest.java:202) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$thrownCheckStillRequestsStop$0(ModdedWorldCheckTest.java:221) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:171) ~[main/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.thrownCheckStillRequestsStop(ModdedWorldCheckTest.java:219) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.run(ParentRunner.java:413) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:137) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:115) ~[junit-4.13.2.jar:4.13.2]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65) ~[?:?]
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53) ~[?:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19) ~[?:?]
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) ~[?:?]
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88) ~[?:?]
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) ~[?:?]
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[16Jul2026 05:44:26.832] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_discovery_success'
[16Jul2026 05:44:26.833] [Test worker/ERROR] [Iris/]: Iris custom content provider discovery failed
java.lang.RuntimeException: provider init failed
at art.arcane.iris.modded.api.ModdedCustomContentRegistryTest.failedDiscoveryPublishesNothingAndPreservesTheCause(ModdedCustomContentRegistryTest.java:39) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
@@ -0,0 +1,35 @@
package art.arcane.iris.nativegen;
public final class NativeStructureGenerationException extends IllegalStateException {
private NativeStructureGenerationException(FailureContext context, Throwable cause) {
super(message(context), cause);
}
private NativeStructureGenerationException(FailureContext context) {
super(message(context));
}
public static NativeStructureGenerationException failure(String operation, String structureContext,
int chunkX, int chunkZ, Throwable cause) {
return new NativeStructureGenerationException(
new FailureContext(operation, structureContext, chunkX, chunkZ), cause);
}
public static NativeStructureGenerationException failure(String operation, String structureContext,
int chunkX, int chunkZ) {
return new NativeStructureGenerationException(
new FailureContext(operation, structureContext, chunkX, chunkZ));
}
private static String message(FailureContext context) {
String resolvedContext = context.structureContext() == null || context.structureContext().isBlank()
? "<unregistered structure>"
: context.structureContext();
return "Iris native structure " + context.operation() + " failed for " + resolvedContext
+ " in chunk " + context.chunkX() + "," + context.chunkZ()
+ "; chunk generation was aborted to prevent partial structure state";
}
private record FailureContext(String operation, String structureContext, int chunkX, int chunkZ) {
}
}
@@ -0,0 +1,45 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.nativegen;
import com.mojang.datafixers.util.Pair;
import net.minecraft.core.BlockPos;
public final class NativeStructureLocateResults {
private NativeStructureLocateResults() {
}
public static <T> Pair<BlockPos, T> nearest(BlockPos origin, Pair<BlockPos, T> replacement,
Pair<BlockPos, T> nativeResult) {
if (replacement == null) {
return nativeResult;
}
if (nativeResult == null) {
return replacement;
}
return horizontalDistanceSquared(origin, nativeResult.getFirst())
<= horizontalDistanceSquared(origin, replacement.getFirst()) ? nativeResult : replacement;
}
private static long horizontalDistanceSquared(BlockPos origin, BlockPos target) {
long dx = (long) target.getX() - origin.getX();
long dz = (long) target.getZ() - origin.getZ();
return dx * dx + dz * dz;
}
}
@@ -2,9 +2,12 @@ package art.arcane.iris.nativegen;
import art.arcane.iris.engine.framework.StructureVerticalBounds;
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
import art.arcane.iris.engine.object.IrisVanillaStructureStiltSettings;
import art.arcane.iris.engine.object.IrisObjectVacuum;
import art.arcane.iris.engine.object.IrisStructureStiltSettings;
import art.arcane.volmlib.util.math.RNG;
import com.mojang.datafixers.util.Either;
import net.minecraft.core.BlockPos;
import net.minecraft.resources.Identifier;
import net.minecraft.tags.BlockTags;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.StructureManager;
@@ -20,15 +23,36 @@ import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.TerrainAdjustment;
import net.minecraft.world.level.levelgen.structure.pools.JigsawJunction;
import net.minecraft.world.level.levelgen.structure.pools.LegacySinglePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.ListPoolElement;
import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentPieces;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.List;
import java.util.Objects;
import java.util.function.IntBinaryOperator;
public final class NativeStructurePostProcessor {
private static final int FOUNDATION_VERTICAL_TOLERANCE = 1;
private static final int MAX_BURIAL_COLUMNS = 2_000_000;
private static final int MONUMENT_BASE_BELOW_SEA_LEVEL = 24;
private static final String OCEAN_MONUMENT_ID = "minecraft:monument";
private static final double SURFACE_TERRAIN_FALLOFF = 2.0;
private static final long SURFACE_TERRAIN_INFLUENCE_SCALE = 1_000_000L;
private static final int SURFACE_TERRAIN_RADIUS = 12;
private static final int UNDERGROUND_SURFACE_CLEARANCE = 1;
private NativeStructurePostProcessor() {
}
@@ -37,38 +61,185 @@ public final class NativeStructurePostProcessor {
WorldgenRandom random, BoundingBox area, ChunkPos chunkPos, String structureId,
StructureStart start, IrisNativeStructureDecision decision,
StiltBlockResolver stiltBlockResolver) {
IrisVanillaStructureStiltSettings stilt = decision.stilt();
IrisStructureStiltSettings stilt = decision.stilt();
ensureMonumentSeaLevelAlignment(start, structureId, decision.yShift(), generator.getSeaLevel(),
area.minY(), area.maxY() + 1);
start.placeInChunk(world, structureManager, generator, random, area, chunkPos);
if (stilt != null) {
placeStilts(world, area, structureId, start, stilt, stiltBlockResolver);
}
}
public static int applyVerticalPlacement(StructureStart start, String structureId, int requestedOffset,
int seaLevel, int worldMinY, int worldMaxYExclusive,
boolean underground, IntBinaryOperator surfaceHeight) {
if (isOceanMonument(structureId)) {
return alignOceanMonumentToSeaLevel(
start, requestedOffset, seaLevel, worldMinY, worldMaxYExclusive);
}
return applyVerticalShift(
start, requestedOffset, worldMinY, worldMaxYExclusive, underground, surfaceHeight);
}
public static int applyVerticalShift(StructureStart start, int requestedOffset, int worldMinY,
int worldMaxYExclusive) {
int worldMaxYExclusive, boolean underground,
IntBinaryOperator surfaceHeight) {
BoundingBox bounds = start.getBoundingBox();
int resolvedOffset = underground
? resolveBuriedOffset(bounds, requestedOffset, worldMinY, worldMaxYExclusive, surfaceHeight)
: requestedOffset;
int offsetY = StructureVerticalBounds.clampOffset(
bounds.minY(), bounds.maxY(), requestedOffset, worldMinY, worldMaxYExclusive);
bounds.minY(), bounds.maxY(), resolvedOffset, worldMinY, worldMaxYExclusive);
if (underground && offsetY > resolvedOffset) {
throw new IllegalStateException("Underground native structure cannot fit below terrain and inside world bounds");
}
if (offsetY == 0) {
return 0;
}
moveStructureStart(start, bounds, offsetY);
return offsetY;
}
static int alignOceanMonumentToSeaLevel(StructureStart start, int configuredOffset, int seaLevel,
int worldMinY, int worldMaxYExclusive) {
OceanMonumentPieces.MonumentBuilding building = requireOceanMonumentBuilding(start);
BoundingBox bounds = start.getBoundingBox();
int targetMinY = Math.addExact(
Math.subtractExact(seaLevel, MONUMENT_BASE_BELOW_SEA_LEVEL), configuredOffset);
int requestedOffset = Math.subtractExact(targetMinY, building.getBoundingBox().minY());
int offsetY = StructureVerticalBounds.clampOffset(
bounds.minY(), bounds.maxY(), requestedOffset, worldMinY, worldMaxYExclusive);
if (offsetY != requestedOffset) {
throw new IllegalStateException("Ocean monument cannot align to sea level " + seaLevel
+ " with configured offset " + configuredOffset + " inside world bounds ["
+ worldMinY + "," + worldMaxYExclusive + ")");
}
if (offsetY == 0) {
return 0;
}
moveStructureStart(start, bounds, offsetY);
return offsetY;
}
private static void ensureMonumentSeaLevelAlignment(StructureStart start, String structureId,
int configuredOffset, int seaLevel,
int worldMinY, int worldMaxYExclusive) {
if (isOceanMonument(structureId)) {
alignOceanMonumentToSeaLevel(
start, configuredOffset, seaLevel, worldMinY, worldMaxYExclusive);
}
}
private static boolean isOceanMonument(String structureId) {
return OCEAN_MONUMENT_ID.equals(structureId);
}
private static OceanMonumentPieces.MonumentBuilding requireOceanMonumentBuilding(StructureStart start) {
Objects.requireNonNull(start, "Ocean monument start must not be null");
List<StructurePiece> pieces = start.getPieces();
if (pieces.size() != 1 || !(pieces.get(0) instanceof OceanMonumentPieces.MonumentBuilding building)) {
throw new IllegalStateException("minecraft:monument must contain exactly one MonumentBuilding, found "
+ pieces.size() + " top-level pieces");
}
return building;
}
private static void moveStructureStart(StructureStart start, BoundingBox cachedBounds, int offsetY) {
for (StructurePiece piece : start.getPieces()) {
piece.move(0, offsetY, 0);
if (piece instanceof PoolElementStructurePiece poolPiece) {
List<JigsawJunction> junctions = poolPiece.getJunctions();
for (int i = 0; i < junctions.size(); i++) {
JigsawJunction junction = junctions.get(i);
junctions.set(i, new JigsawJunction(
junction.getSourceX(),
junction.getSourceGroundY() + offsetY,
junction.getSourceZ(),
junction.getDeltaY(),
junction.getDestProjection()));
}
moveStructurePiece(piece, offsetY);
}
cachedBounds.move(0, offsetY, 0);
}
private static void moveStructurePiece(StructurePiece piece, int offsetY) {
piece.move(0, offsetY, 0);
if (piece instanceof OceanMonumentPieces.MonumentBuilding building) {
for (StructurePiece child : monumentChildPieces(building)) {
child.move(0, offsetY, 0);
}
}
bounds.move(0, offsetY, 0);
return offsetY;
if (piece instanceof PoolElementStructurePiece poolPiece) {
List<JigsawJunction> junctions = poolPiece.getJunctions();
for (int i = 0; i < junctions.size(); i++) {
JigsawJunction junction = junctions.get(i);
junctions.set(i, new JigsawJunction(
junction.getSourceX(),
junction.getSourceGroundY() + offsetY,
junction.getSourceZ(),
junction.getDeltaY(),
junction.getDestProjection()));
}
}
}
static List<StructurePiece> monumentChildPieces(OceanMonumentPieces.MonumentBuilding building) {
Object value;
try {
value = MonumentChildPiecesAccess.FIELD.get(building);
} catch (IllegalAccessException error) {
throw new IllegalStateException("Cannot read Ocean Monument child pieces", error);
}
if (!(value instanceof List<?> children)) {
throw new IllegalStateException("Ocean Monument child-pieces field is not a list");
}
List<StructurePiece> pieces = new ArrayList<>(children.size());
for (Object child : children) {
if (!(child instanceof StructurePiece monumentPiece)
|| child.getClass().getEnclosingClass() != OceanMonumentPieces.class) {
throw new IllegalStateException("Ocean Monument child-pieces list contains "
+ (child == null ? "null" : child.getClass().getName()));
}
pieces.add(monumentPiece);
}
return List.copyOf(pieces);
}
private static Field resolveMonumentChildPiecesField() {
Field resolved = null;
for (Field field : OceanMonumentPieces.MonumentBuilding.class.getDeclaredFields()) {
if (Modifier.isStatic(field.getModifiers()) || field.getType() != List.class) {
continue;
}
if (resolved != null) {
throw new IllegalStateException("Ocean Monument has multiple instance List fields");
}
resolved = field;
}
if (resolved == null) {
throw new IllegalStateException("Ocean Monument child-pieces List field is missing");
}
if (!Modifier.isPrivate(resolved.getModifiers()) || !Modifier.isFinal(resolved.getModifiers())) {
throw new IllegalStateException("Ocean Monument child-pieces field has an unexpected access contract");
}
if (!resolved.trySetAccessible()) {
throw new IllegalStateException("Ocean Monument child-pieces field is inaccessible");
}
return resolved;
}
static int resolveBuriedOffset(BoundingBox bounds, int requestedOffset, int worldMinY,
int worldMaxYExclusive, IntBinaryOperator surfaceHeight) {
Objects.requireNonNull(bounds, "Native structure bounds must not be null");
Objects.requireNonNull(surfaceHeight, "Underground native structure requires a terrain height resolver");
long width = (long) bounds.maxX() - bounds.minX() + 1L;
long depth = (long) bounds.maxZ() - bounds.minZ() + 1L;
if (width <= 0L || depth <= 0L || width > MAX_BURIAL_COLUMNS / depth) {
throw new IllegalStateException("Underground native structure burial footprint is invalid or exceeds "
+ MAX_BURIAL_COLUMNS + " columns");
}
int maximumOffset = requestedOffset;
for (int x = bounds.minX(); x <= bounds.maxX(); x++) {
for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) {
int allowedTopY = surfaceHeight.applyAsInt(x, z) - UNDERGROUND_SURFACE_CLEARANCE;
maximumOffset = Math.min(maximumOffset, allowedTopY - bounds.maxY());
}
}
int clampedOffset = StructureVerticalBounds.clampOffset(
bounds.minY(), bounds.maxY(), maximumOffset, worldMinY, worldMaxYExclusive);
if (clampedOffset > maximumOffset) {
throw new IllegalStateException("Underground native structure cannot be buried without crossing world minimum Y");
}
return clampedOffset;
}
public static boolean isUndergroundStep(GenerationStep.Decoration step) {
@@ -77,6 +248,365 @@ public final class NativeStructurePostProcessor {
|| step == GenerationStep.Decoration.STRONGHOLDS;
}
public static boolean shouldClearEntireVegetationFootprint(GenerationStep.Decoration step,
boolean configured) {
return configured || !isUndergroundStep(step);
}
public static void prepareSurfaceStructures(WorldGenLevel world, BoundingBox area,
List<StructureStart> starts,
IntBinaryOperator surfaceHeight) {
if (starts == null || starts.isEmpty()) {
return;
}
Objects.requireNonNull(surfaceHeight, "Surface structure terrain fitting requires an Iris height resolver");
List<SurfaceAnchor> anchors = collectSurfaceAnchors(starts);
if (!anchors.isEmpty()) {
fitSurfaceTerrain(world, area, anchors, surfaceHeight);
}
StructureTemplateManager templateManager = world.getLevel().getStructureManager();
for (StructureStart start : starts) {
if (requiresSurfaceTerrain(start)) {
clearLegacyTemplateAir(world, area, start, templateManager);
}
}
}
static boolean shouldPrepareSurfaceTerrain(TerrainAdjustment adjustment,
GenerationStep.Decoration step) {
return adjustment == TerrainAdjustment.BEARD_THIN
&& step == GenerationStep.Decoration.SURFACE_STRUCTURES;
}
static int resolveSurfaceTarget(List<SurfaceAnchor> anchors, int worldX, int worldZ,
int originalY) {
int localTargetY = originalY;
SurfaceAnchor selectedLocal = null;
long totalInfluence = 0L;
long weightedMeetY = 0L;
long maximumInfluence = 0L;
for (SurfaceAnchor anchor : anchors) {
int outX = IrisObjectVacuum.outset(worldX, anchor.minX(), anchor.maxX());
int outZ = IrisObjectVacuum.outset(worldZ, anchor.minZ(), anchor.maxZ());
long distanceSquared = (long) outX * outX + (long) outZ * outZ;
if (distanceSquared > (long) SURFACE_TERRAIN_RADIUS * SURFACE_TERRAIN_RADIUS) {
continue;
}
boolean containsColumn = outX == 0 && outZ == 0;
if (containsColumn) {
if (precedes(anchor, selectedLocal)) {
localTargetY = anchor.meetY();
selectedLocal = anchor;
}
continue;
}
double factor = IrisObjectVacuum.columnInfluence(
worldX, worldZ,
anchor.minX(), anchor.maxX(), anchor.minZ(), anchor.maxZ(),
SURFACE_TERRAIN_RADIUS, SURFACE_TERRAIN_FALLOFF);
long influence = Math.round(factor * SURFACE_TERRAIN_INFLUENCE_SCALE);
if (influence <= 0L) {
continue;
}
long weightedInfluence = influence * Math.max(1, anchor.strength());
totalInfluence += weightedInfluence;
weightedMeetY += weightedInfluence * anchor.meetY();
maximumInfluence = Math.max(maximumInfluence, influence);
}
if (selectedLocal != null) {
return localTargetY;
}
if (totalInfluence == 0L) {
return originalY;
}
double blendedMeetY = weightedMeetY / (double) totalInfluence;
double factor = maximumInfluence / (double) SURFACE_TERRAIN_INFLUENCE_SCALE;
return (int) Math.round(originalY + ((blendedMeetY - originalY) * factor));
}
private static boolean precedes(SurfaceAnchor candidate, SurfaceAnchor selected) {
if (selected == null) {
return true;
}
if (candidate.strength() != selected.strength()) {
return candidate.strength() > selected.strength();
}
if (candidate.meetY() != selected.meetY()) {
return candidate.meetY() < selected.meetY();
}
if (candidate.minX() != selected.minX()) {
return candidate.minX() < selected.minX();
}
if (candidate.minZ() != selected.minZ()) {
return candidate.minZ() < selected.minZ();
}
if (candidate.maxX() != selected.maxX()) {
return candidate.maxX() < selected.maxX();
}
if (candidate.maxZ() != selected.maxZ()) {
return candidate.maxZ() < selected.maxZ();
}
return false;
}
private static List<SurfaceAnchor> collectSurfaceAnchors(List<StructureStart> starts) {
List<SurfaceAnchor> anchors = new ArrayList<>();
for (StructureStart start : starts) {
if (!requiresSurfaceTerrain(start)) {
continue;
}
for (StructurePiece piece : start.getPieces()) {
if (piece instanceof PoolElementStructurePiece poolPiece) {
if (poolPiece.getElement().getProjection() == StructureTemplatePool.Projection.RIGID) {
BoundingBox bounds = poolPiece.getBoundingBox();
anchors.add(new SurfaceAnchor(
bounds.minX(), bounds.maxX(), bounds.minZ(), bounds.maxZ(),
bounds.minY() + poolPiece.getGroundLevelDelta() - 1, 2));
}
for (JigsawJunction junction : poolPiece.getJunctions()) {
anchors.add(new SurfaceAnchor(
junction.getSourceX(), junction.getSourceX(),
junction.getSourceZ(), junction.getSourceZ(),
junction.getSourceGroundY() - 1, 1));
}
continue;
}
BoundingBox bounds = piece.getBoundingBox();
anchors.add(new SurfaceAnchor(
bounds.minX(), bounds.maxX(), bounds.minZ(), bounds.maxZ(),
bounds.minY() - 1, 2));
}
}
return List.copyOf(anchors);
}
private static boolean requiresSurfaceTerrain(StructureStart start) {
return start != null
&& start.isValid()
&& shouldPrepareSurfaceTerrain(
start.getStructure().terrainAdaptation(), start.getStructure().step());
}
private static void fitSurfaceTerrain(WorldGenLevel world, BoundingBox area,
List<SurfaceAnchor> anchors,
IntBinaryOperator surfaceHeight) {
int width = area.getXSpan();
int depth = area.getZSpan();
int[] originalHeights = new int[width * depth];
int[] targetHeights = new int[width * depth];
for (int z = area.minZ(); z <= area.maxZ(); z++) {
for (int x = area.minX(); x <= area.maxX(); x++) {
int column = (z - area.minZ()) * width + x - area.minX();
int originalY = Math.max(area.minY(), Math.min(
area.maxY(), surfaceHeight.applyAsInt(x, z)));
originalHeights[column] = originalY;
targetHeights[column] = Math.max(area.minY(), Math.min(
area.maxY(), resolveSurfaceTarget(anchors, x, z, originalY)));
}
}
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
for (int z = area.minZ(); z <= area.maxZ(); z++) {
for (int x = area.minX(); x <= area.maxX(); x++) {
int column = (z - area.minZ()) * width + x - area.minX();
applySurfaceColumn(world, position, x, z,
originalHeights[column], targetHeights[column], area.minY(), area.maxY());
}
}
}
static void applySurfaceColumn(WorldGenLevel world, BlockPos.MutableBlockPos position,
int x, int z, int originalY, int targetY,
int worldMinY, int worldMaxY) {
if (targetY == originalY) {
return;
}
SurfaceMaterials materials = resolveSurfaceMaterials(world, position, x, z, originalY, worldMinY);
if (targetY < originalY) {
BlockState clearedState = clearSurfaceDecorationAndResolveFill(
world, position, x, z, originalY, worldMaxY);
for (int y = originalY; y > targetY; y--) {
world.setBlock(position.set(x, y, z), clearedState, 2);
}
world.setBlock(position.set(x, targetY, z), materials.surface(), 2);
return;
}
for (int y = originalY + 1; y < targetY; y++) {
world.setBlock(position.set(x, y, z), materials.subsurface(), 2);
}
world.setBlock(position.set(x, targetY, z), materials.surface(), 2);
}
private static BlockState clearSurfaceDecorationAndResolveFill(
WorldGenLevel world, BlockPos.MutableBlockPos position,
int x, int z, int originalY, int worldMaxY) {
BlockState air = Blocks.AIR.defaultBlockState();
for (int y = originalY + 1; y <= worldMaxY; y++) {
BlockState state = world.getBlockState(position.set(x, y, z));
if (state.isAir()) {
return air;
}
if (!state.getFluidState().isEmpty()) {
BlockState fluid = state.getFluidState().createLegacyBlock();
if (state != fluid) {
world.setBlock(position, fluid, 2);
}
return fluid;
}
if (state.isSolid() || isTreeBlock(state)) {
return air;
}
world.setBlock(position, air, 2);
}
return air;
}
private static SurfaceMaterials resolveSurfaceMaterials(WorldGenLevel world,
BlockPos.MutableBlockPos position,
int x, int z, int originalY,
int worldMinY) {
BlockState surface = world.getBlockState(position.set(x, originalY, z));
BlockState subsurface = null;
for (int y = originalY - 1; y >= worldMinY; y--) {
BlockState candidate = world.getBlockState(position.set(x, y, z));
if (isTerrainBlock(candidate)) {
subsurface = candidate;
break;
}
}
if (subsurface == null) {
subsurface = isTerrainBlock(surface) ? surface : Blocks.STONE.defaultBlockState();
}
if (!isTerrainBlock(surface)) {
surface = subsurface;
}
return new SurfaceMaterials(surface, subsurface);
}
private static boolean isTerrainBlock(BlockState state) {
return state.isSolid() && !isTreeBlock(state);
}
private static void clearLegacyTemplateAir(WorldGenLevel world, BoundingBox area,
StructureStart start,
StructureTemplateManager templateManager) {
for (StructurePiece piece : start.getPieces()) {
if (!(piece instanceof PoolElementStructurePiece poolPiece)
|| poolPiece.getElement().getProjection() != StructureTemplatePool.Projection.RIGID
|| !intersects(poolPiece.getBoundingBox(), area)) {
continue;
}
int groundY = poolPiece.getBoundingBox().minY() + poolPiece.getGroundLevelDelta();
StructurePlaceSettings settings = new StructurePlaceSettings()
.setRotation(poolPiece.getRotation())
.setBoundingBox(area);
clearLegacyTemplateAir(world, poolPiece.getElement(), poolPiece.getPosition(),
groundY, settings, templateManager);
}
}
private static void clearLegacyTemplateAir(WorldGenLevel world, StructurePoolElement element,
BlockPos position, int groundY,
StructurePlaceSettings settings,
StructureTemplateManager templateManager) {
if (element instanceof ListPoolElement listElement) {
for (StructurePoolElement child : listElement.getElements()) {
clearLegacyTemplateAir(world, child, position, groundY, settings, templateManager);
}
return;
}
if (!(element instanceof LegacySinglePoolElement legacyElement)) {
return;
}
StructureTemplate template = resolveTemplate(legacyElement, templateManager);
clearTemplateAir(world, template, position, groundY, settings);
}
static void clearTemplateAir(WorldGenLevel world, StructureTemplate template,
BlockPos position, int groundY,
StructurePlaceSettings settings) {
List<StructureTemplate.StructureBlockInfo> airBlocks = template.filterBlocks(
position, settings, Blocks.AIR);
BlockState air = Blocks.AIR.defaultBlockState();
for (StructureTemplate.StructureBlockInfo airBlock : airBlocks) {
BlockPos airPosition = airBlock.pos();
if (shouldClearLegacyAir(
airPosition.getY(), groundY, world.getBlockState(airPosition).isAir())) {
world.setBlock(airPosition, air, 2);
}
}
}
static boolean shouldClearLegacyAir(int airY, int groundY, boolean existingAir) {
return airY >= groundY && !existingAir;
}
static boolean intersects(BoundingBox first, BoundingBox second) {
return first.maxX() >= second.minX() && first.minX() <= second.maxX()
&& first.maxY() >= second.minY() && first.minY() <= second.maxY()
&& first.maxZ() >= second.minZ() && first.minZ() <= second.maxZ();
}
private static StructureTemplate resolveTemplate(SinglePoolElement element,
StructureTemplateManager templateManager) {
Object value;
try {
value = SinglePoolTemplateAccess.FIELD.get(element);
} catch (IllegalAccessException error) {
throw new IllegalStateException("Cannot read native structure pool template", error);
}
if (!(value instanceof Either<?, ?> reference)) {
throw new IllegalStateException("Native structure pool template field is not an Either");
}
return resolveTemplateReference(reference, templateManager);
}
static StructureTemplate resolveTemplateReference(Either<?, ?> reference,
StructureTemplateManager templateManager) {
return reference.map(
location -> resolveNamedTemplate(location, templateManager),
NativeStructurePostProcessor::requireRuntimeTemplate);
}
private static StructureTemplate resolveNamedTemplate(Object value,
StructureTemplateManager templateManager) {
if (!(value instanceof Identifier identifier)) {
throw new IllegalStateException("Native structure pool template identifier is "
+ (value == null ? "null" : value.getClass().getName()));
}
return templateManager.getOrCreate(identifier);
}
private static StructureTemplate requireRuntimeTemplate(Object value) {
if (!(value instanceof StructureTemplate template)) {
throw new IllegalStateException("Native runtime structure pool template is "
+ (value == null ? "null" : value.getClass().getName()));
}
return template;
}
static Field resolveSinglePoolTemplateField() {
Field resolved = null;
for (Field field : SinglePoolElement.class.getDeclaredFields()) {
if (Modifier.isStatic(field.getModifiers()) || field.getType() != Either.class) {
continue;
}
if (resolved != null) {
throw new IllegalStateException("SinglePoolElement has multiple instance Either fields");
}
resolved = field;
}
if (resolved == null) {
throw new IllegalStateException("SinglePoolElement template Either field is missing");
}
if (!Modifier.isProtected(resolved.getModifiers()) || !Modifier.isFinal(resolved.getModifiers())) {
throw new IllegalStateException("SinglePoolElement template field has an unexpected access contract");
}
if (!resolved.trySetAccessible()) {
throw new IllegalStateException("SinglePoolElement template field is inaccessible");
}
return resolved;
}
public static void clearIntersectingVegetation(WorldGenLevel world, ChunkAccess chunk, BoundingBox area,
List<VegetationTarget> targets) {
if (targets == null || targets.isEmpty()) {
@@ -141,19 +671,25 @@ public final class NativeStructurePostProcessor {
private static void markVegetationColumns(BoundingBox area, VegetationSnapshot snapshot,
VegetationTarget target, boolean[] clearColumns) {
int width = area.getXSpan();
int padding = requiresSurfaceTerrain(target.start()) ? SURFACE_TERRAIN_RADIUS : 0;
int[] pieceTops = new int[clearColumns.length];
Arrays.fill(pieceTops, Integer.MIN_VALUE);
for (StructurePiece piece : target.start().getPieces()) {
BoundingBox bounds = piece.getBoundingBox();
int minX = Math.max(area.minX(), bounds.minX());
int maxX = Math.min(area.maxX(), bounds.maxX());
int minZ = Math.max(area.minZ(), bounds.minZ());
int maxZ = Math.min(area.maxZ(), bounds.maxZ());
int minX = Math.max(area.minX(), bounds.minX() - padding);
int maxX = Math.min(area.maxX(), bounds.maxX() + padding);
int minZ = Math.max(area.minZ(), bounds.minZ() - padding);
int maxZ = Math.min(area.maxZ(), bounds.maxZ() + padding);
if (minX > maxX || minZ > maxZ) {
continue;
}
for (int z = minZ; z <= maxZ; z++) {
for (int x = minX; x <= maxX; x++) {
if (padding > 0) {
if (!withinSurfaceTerrainRadius(x, z, bounds, padding)) {
continue;
}
}
int column = (z - area.minZ()) * width + x - area.minX();
pieceTops[column] = Math.max(pieceTops[column], bounds.maxY());
}
@@ -169,6 +705,12 @@ public final class NativeStructurePostProcessor {
}
}
static boolean withinSurfaceTerrainRadius(int x, int z, BoundingBox bounds, int radius) {
int outX = IrisObjectVacuum.outset(x, bounds.minX(), bounds.maxX());
int outZ = IrisObjectVacuum.outset(z, bounds.minZ(), bounds.maxZ());
return (long) outX * outX + (long) outZ * outZ <= (long) radius * radius;
}
static boolean shouldClearVegetationColumn(int pieceTopY, int lowestTreeY, boolean force) {
return force || pieceTopY >= lowestTreeY;
}
@@ -255,7 +797,7 @@ public final class NativeStructurePostProcessor {
}
private static void placeStilts(WorldGenLevel world, BoundingBox area, String structureId,
StructureStart start, IrisVanillaStructureStiltSettings settings,
StructureStart start, IrisStructureStiltSettings settings,
StiltBlockResolver stiltBlockResolver) {
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
int structureHash = structureId == null ? 0 : structureId.hashCode();
@@ -273,12 +815,55 @@ public final class NativeStructurePostProcessor {
if (existingState.isSolid() && !vegetation) {
break;
}
BlockState stilt = stiltBlockResolver.resolve(settings, rng, column.x(), y, column.z());
world.setBlock(position, stilt == null ? Blocks.COBBLESTONE.defaultBlockState() : stilt, 2);
BlockState stilt = Objects.requireNonNull(
stiltBlockResolver.resolve(settings, rng, column.x(), y, column.z()),
"Stilt palette returned no block for " + structureId + " at "
+ column.x() + "," + y + "," + column.z());
world.setBlock(position, stilt, 2);
}
}
}
public static StiltSupportAudit auditStiltSupport(WorldGenLevel world, BoundingBox area,
StructureStart start, BlockState expectedStilt) {
Objects.requireNonNull(expectedStilt, "Expected stilt state must not be null");
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
int baseColumns = 0;
int stiltBlocks = 0;
int stiltColumns = 0;
int unsupportedColumns = 0;
for (FoundationColumn column : foundationEnvelope(area, start)) {
int foundationY = findFoundationY(world, column, position);
if (foundationY == Integer.MIN_VALUE) {
continue;
}
baseColumns++;
boolean grounded = foundationY == area.minY();
boolean stiltColumn = false;
for (int y = foundationY - 1; y >= area.minY(); y--) {
BlockState state = world.getBlockState(position.set(column.x(), y, column.z()));
if (state.is(expectedStilt.getBlock())) {
stiltBlocks++;
stiltColumn = true;
if (y == area.minY()) {
grounded = true;
}
continue;
}
boolean vegetation = state.is(BlockTags.LOGS) || state.is(BlockTags.LEAVES);
grounded = state.isSolid() && !vegetation;
break;
}
if (stiltColumn) {
stiltColumns++;
}
if (!grounded) {
unsupportedColumns++;
}
}
return new StiltSupportAudit(baseColumns, stiltBlocks, stiltColumns, unsupportedColumns);
}
private static int findFoundationY(WorldGenLevel world, FoundationColumn column,
BlockPos.MutableBlockPos position) {
for (int cell = 0; cell < column.ys().length; cell++) {
@@ -293,7 +878,7 @@ public final class NativeStructurePostProcessor {
@FunctionalInterface
public interface StiltBlockResolver {
BlockState resolve(IrisVanillaStructureStiltSettings settings, RNG rng, int x, int y, int z);
BlockState resolve(IrisStructureStiltSettings settings, RNG rng, int x, int y, int z);
}
private record FoundationColumn(int x, int z, int[] ys) {
@@ -302,6 +887,30 @@ public final class NativeStructurePostProcessor {
public record VegetationTarget(StructureStart start, boolean force) {
}
public record StiltSupportAudit(int baseColumns, int stiltBlocks, int stiltColumns,
int unsupportedColumns) {
}
private record VegetationSnapshot(BitSet[] columns, int[] lowestY, int treeBlockCount) {
}
record SurfaceAnchor(int minX, int maxX, int minZ, int maxZ, int meetY, int strength) {
}
private record SurfaceMaterials(BlockState surface, BlockState subsurface) {
}
private static final class MonumentChildPiecesAccess {
private static final Field FIELD = resolveMonumentChildPiecesField();
private MonumentChildPiecesAccess() {
}
}
private static final class SinglePoolTemplateAccess {
private static final Field FIELD = resolveSinglePoolTemplateField();
private SinglePoolTemplateAccess() {
}
}
}
@@ -75,7 +75,7 @@ final class IrisModdedBiomeSource extends BiomeSource {
if (registry == null) {
throw new IllegalStateException("Iris cannot create structure state without the biome registry");
}
Set<String> generatedBiomeKeys = exactStructureBiomeKeys();
Set<String> generatedBiomeKeys = requireConfiguredStructureBiomeKeys(exactStructureBiomeKeys());
Set<String> missingBiomeKeys = new LinkedHashSet<>(generatedBiomeKeys);
missingBiomeKeys.removeAll(registeredBiomeKeys(registry));
if (!missingBiomeKeys.isEmpty()) {
@@ -101,21 +101,32 @@ final class IrisModdedBiomeSource extends BiomeSource {
@Override
protected Stream<Holder<Biome>> collectPossibleBiomes() {
Set<String> generatedBiomeKeys = requireConfiguredStructureBiomeKeys(exactStructureBiomeKeys());
Registry<Biome> registry = biomeRegistry();
if (registry == null) {
return serializedSource.possibleBiomes().stream();
}
Set<String> generatedBiomeKeys = possibleStructureBiomeKeys();
LinkedHashSet<Holder<Biome>> possible = new LinkedHashSet<>();
Holder<Biome> fallback = fallbackHolder(registry);
registry.listElements().forEach((Holder.Reference<Biome> reference) -> {
String key = holderKey(reference);
if (isGeneratedBiomeKey(key, generatedBiomeKeys)) {
possible.add(reference);
if (registry == null) {
for (Holder<Biome> biome : serializedSource.possibleBiomes()) {
if (isGeneratedBiomeKey(holderKey(biome), generatedBiomeKeys)) {
possible.add(biome);
}
}
});
if (possible.isEmpty() && fallback != null) {
possible.add(fallback);
} else {
Set<String> missingBiomeKeys = new LinkedHashSet<>(generatedBiomeKeys);
missingBiomeKeys.removeAll(registeredBiomeKeys(registry));
if (!missingBiomeKeys.isEmpty()) {
throw new IllegalStateException("Iris structure biomes are not registered: "
+ missingBiomeKeys);
}
registry.listElements().forEach((Holder.Reference<Biome> reference) -> {
if (isGeneratedBiomeKey(holderKey(reference), generatedBiomeKeys)) {
possible.add(reference);
}
});
}
if (possible.isEmpty()) {
String phase = registry == null ? "serialized biome bootstrap" : "biome registry";
throw new IllegalStateException("Iris configured structure biomes are absent from the "
+ phase + ": " + generatedBiomeKeys);
}
return possible.stream();
}
@@ -220,31 +231,44 @@ final class IrisModdedBiomeSource extends BiomeSource {
private Holder<Biome> resolveSurfaceStructureBiome(Engine engine, int quartX, int quartZ,
Climate.Sampler sampler) {
Registry<Biome> registry = biomeRegistry();
if (!isReady(engine) || registry == null) {
return serializedSource.getNoiseBiome(quartX, 0, quartZ, sampler);
if (!isReady(engine)) {
throw new IllegalStateException("Iris structure biome lookup ran before engine binding");
}
if (registry == null) {
throw new IllegalStateException("Iris structure biome lookup has no biome registry");
}
IrisBiome irisBiome = engine.getComplex().getTrueBiomeStream().get(quartX << 2, quartZ << 2);
Holder<Biome> resolved = irisBiome == null ? null : resolveHolder(registry, irisBiome.getVanillaDerivativeKey());
return resolved == null ? serializedSource.getNoiseBiome(quartX, 0, quartZ, sampler) : resolved;
if (irisBiome == null) {
throw new IllegalStateException("Iris returned no surface structure biome at quart "
+ quartX + "," + quartZ);
}
Holder<Biome> resolved = resolveHolder(registry, irisBiome.getVanillaDerivativeKey());
if (resolved == null) {
throw new IllegalStateException("Iris structure biome derivative '"
+ irisBiome.getVanillaDerivativeKey() + "' is not registered at quart "
+ quartX + "," + quartZ);
}
return resolved;
}
private Holder<Biome> resolveStructureBiome(Engine engine, int quartX, int quartY, int quartZ,
Climate.Sampler sampler) {
Registry<Biome> registry = biomeRegistry();
BiomeResolution resolution = resolveBiomeResolution(engine, quartX, quartY, quartZ);
if (resolution == null || registry == null) {
return serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler);
if (resolution == null) {
throw new IllegalStateException("Iris returned no structure biome at quart "
+ quartX + "," + quartY + "," + quartZ);
}
if (registry == null) {
throw new IllegalStateException("Iris structure biome lookup has no biome registry");
}
Holder<Biome> resolved = resolveHolder(registry, resolution.irisBiome().getVanillaDerivativeKey());
if (resolved == null && resolution.irisBiome().isCustom()) {
IrisBiomeCustom customBiome = resolution.irisBiome().getCustomBiome(
resolution.rng(), engine, resolution.blockX(), resolution.blockY(), resolution.blockZ());
if (customBiome != null) {
resolved = resolveHolder(registry, engine.getDimension().getLoadKey().toLowerCase(Locale.ROOT)
+ ":" + customBiome.getId().toLowerCase(Locale.ROOT));
}
if (resolved == null) {
throw new IllegalStateException("Iris structure biome derivative '"
+ resolution.irisBiome().getVanillaDerivativeKey() + "' is not registered at block "
+ resolution.blockX() + "," + resolution.blockY() + "," + resolution.blockZ());
}
return resolved == null ? fallbackBiome(registry, quartX, quartY, quartZ, sampler) : resolved;
return resolved;
}
private Holder<Biome> resolveVisibleBiome(Engine engine, int quartX, int quartY, int quartZ,
@@ -349,6 +373,13 @@ final class IrisModdedBiomeSource extends BiomeSource {
return key != null && generatedBiomeKeys.contains(key.toLowerCase(Locale.ROOT));
}
static Set<String> requireConfiguredStructureBiomeKeys(Set<String> configuredBiomeKeys) {
if (configuredBiomeKeys == null || configuredBiomeKeys.isEmpty()) {
throw new IllegalStateException("Iris has no configured structure biomes");
}
return configuredBiomeKeys;
}
static boolean isGuaranteedSurfaceBiome(int quartY, int minHeight) {
int internalY = (quartY << 2) - minHeight;
int caveSwitchY = Math.max(-8 - minHeight, 40);
@@ -377,15 +408,15 @@ final class IrisModdedBiomeSource extends BiomeSource {
if (cached != null) {
return cached;
}
LinkedHashSet<String> possible = new LinkedHashSet<>(exactStructureBiomeKeys());
Set<String> possible = requireConfiguredStructureBiomeKeys(exactStructureBiomeKeys());
Registry<Biome> registry = biomeRegistry();
if (registry != null) {
Set<String> registered = registeredBiomeKeys(registry);
Holder<Biome> fallback = fallbackHolder(registry);
String fallbackKey = fallback == null ? null : holderKey(fallback);
if (fallbackKey != null && requiresPossibleBiomeFallback(possible, registered)) {
possible.add(fallbackKey);
}
if (registry == null) {
throw new IllegalStateException("Iris cannot resolve structure biomes without the biome registry");
}
Set<String> missing = new LinkedHashSet<>(possible);
missing.removeAll(registeredBiomeKeys(registry));
if (!missing.isEmpty()) {
throw new IllegalStateException("Iris structure biomes are not registered: " + missing);
}
Set<String> resolved = Set.copyOf(possible);
possibleStructureBiomeKeys = resolved;
@@ -393,34 +424,31 @@ final class IrisModdedBiomeSource extends BiomeSource {
}
private Set<String> exactStructureBiomeKeys() {
IrisModdedChunkGenerator current = generator;
if (current == null) {
return Set.of();
}
Engine engine = current.structureEngineOrNull();
if (!isReady(engine)) {
return current.configuredStructureBiomeKeys();
}
LinkedHashSet<String> possible = new LinkedHashSet<>();
Engine engine = engineOrNull();
if (isReady(engine)) {
String namespace = engine.getDimension().getLoadKey().toLowerCase(Locale.ROOT);
for (IrisBiome irisBiome : engine.getAllBiomes()) {
String derivative = normalizeKey(irisBiome.getVanillaDerivativeKey());
if (derivative != null) {
possible.add(derivative);
}
if (!irisBiome.isCustom()) {
continue;
}
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
possible.add(namespace + ":" + customBiome.getId().toLowerCase(Locale.ROOT));
}
String namespace = engine.getDimension().getLoadKey().toLowerCase(Locale.ROOT);
for (IrisBiome irisBiome : engine.getAllBiomes()) {
String derivative = normalizeKey(irisBiome.getVanillaDerivativeKey());
if (derivative != null) {
possible.add(derivative);
}
if (!irisBiome.isCustom()) {
continue;
}
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
possible.add(namespace + ":" + customBiome.getId().toLowerCase(Locale.ROOT));
}
}
IrisModdedChunkGenerator current = generator;
if (current != null) {
possible.addAll(current.configuredStructureBiomeKeys());
}
return Set.copyOf(possible);
}
static boolean requiresPossibleBiomeFallback(Set<String> configured, Set<String> registered) {
return configured.isEmpty() || !registered.containsAll(configured);
}
private static Set<String> registeredBiomeKeys(Registry<Biome> registry) {
Set<String> registered = new HashSet<>();
registry.listElements().forEach((Holder.Reference<Biome> reference) -> {
@@ -459,18 +487,6 @@ final class IrisModdedBiomeSource extends BiomeSource {
return identifier == null ? null : identifier.toString().toLowerCase(Locale.ROOT);
}
private static Holder<Biome> fallbackHolder(Registry<Biome> registry) {
if (registry == null) {
return null;
}
Holder<Biome> plains = resolveHolder(registry, "minecraft:plains");
if (plains != null) {
return plains;
}
return registry.listElements().findFirst().<Holder<Biome>>map(
(Holder.Reference<Biome> reference) -> reference).orElse(null);
}
private Holder<Biome> fallbackBiome(Registry<Biome> registry, int quartX, int quartY, int quartZ,
Climate.Sampler sampler) {
Holder<Biome> plains = resolveHolder(registry, "minecraft:plains");
@@ -19,15 +19,19 @@
package art.arcane.iris.modded;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.GenerationSessionException;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisImportedStructureControl;
import art.arcane.iris.engine.object.IrisDimension;
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.IrisPlatforms;
import art.arcane.iris.spi.PlatformBiome;
@@ -99,6 +103,7 @@ import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
@@ -208,11 +213,62 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
public synchronized void repoint(String pack, String packDimensionKey, long seed) {
ServerLevel level = boundLevel();
if (level != null) {
ModdedWorldEngines.evict(level);
repointAndBind(level, pack, packDimensionKey, seed);
return;
}
applyUnboundConfiguration(pack, packDimensionKey, seed);
}
synchronized void repointAndBind(ServerLevel level, String pack, String packDimensionKey, long seed) {
if (level.getChunkSource().getGenerator() != this) {
throw new IllegalArgumentException("ServerLevel does not use Iris generator '" + dimensionKey + "'");
}
requireGlobalStructureGeneration(
level.getServer().getWorldGenSettings().options().generateStructures(), dimensionKey);
Engine replacement = ModdedWorldEngines.prepareReplacement(level, pack, packDimensionKey, seed);
try {
ModdedWorldEngines.installReplacement(level, replacement);
} catch (Throwable error) {
try {
ModdedWorldEngines.closeUnregistered(replacement);
} catch (Throwable cleanupError) {
error.addSuppressed(cleanupError);
}
if (error instanceof RuntimeException runtimeException) {
throw runtimeException;
}
if (error instanceof Error fatalError) {
throw fatalError;
}
throw new IllegalStateException("Iris generator '" + dimensionKey + "' failed to replace its engine", error);
}
this.activePack = pack;
this.activeDimensionKey = packDimensionKey;
this.seedOverride = seed;
this.engine = replacement;
this.configuredStructureBiomeKeys = null;
this.configuredPack = null;
this.engineBinding.reset();
this.engineBinding.complete(replacement);
this.announced.set(false);
this.structureBiomeSource.clearCaches();
this.worldCheckStructureShifts.clear();
resetVanillaSpawnBiomes();
}
public synchronized void unbindEngine() {
ServerLevel level = boundLevel();
unbindEngine(level);
}
synchronized void unbindEngine(ServerLevel level) {
if (level != null) {
ModdedWorldEngines.evictOrThrow(level);
}
clearEngineBinding();
}
private void clearEngineBinding() {
this.engine = null;
this.configuredStructureBiomeKeys = null;
this.configuredPack = null;
@@ -223,11 +279,10 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
resetVanillaSpawnBiomes();
}
public synchronized void unbindEngine() {
ServerLevel level = boundLevel();
if (level != null) {
ModdedWorldEngines.evict(level);
}
private void applyUnboundConfiguration(String pack, String packDimensionKey, long seed) {
this.activePack = pack;
this.activeDimensionKey = packDimensionKey;
this.seedOverride = seed;
this.engine = null;
this.configuredStructureBiomeKeys = null;
this.configuredPack = null;
@@ -265,33 +320,14 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
public Pair<BlockPos, Holder<Structure>> findNearestMapStructure(ServerLevel level, HolderSet<Structure> holders,
BlockPos pos, int radius,
boolean findUnexplored) {
Engine current = engineOrNull();
if (current == null) {
return super.findNearestMapStructure(level, holders, pos, radius, findUnexplored);
}
try {
Pair<BlockPos, Holder<Structure>> irisPlaced = findNearestIrisStructure(
level, holders, pos, Math.max(1, radius), current);
if (irisPlaced != null) {
return irisPlaced;
}
} catch (Throwable e) {
LOGGER.error("Iris-placed structure locate failed near {},{}", pos.getX(), pos.getZ(), e);
}
IrisImportedStructureControl control = current.getDimension().getImportedStructures();
if (control == null || !control.active()) {
return null;
}
HolderSet<Structure> reachable = filterReachableNativeStructures(level, holders, current, control);
if (reachable.size() == 0) {
return null;
}
try {
return super.findNearestMapStructure(level, reachable, pos, radius, findUnexplored);
} catch (Throwable e) {
LOGGER.error("Native structure locate failed near {},{}", pos.getX(), pos.getZ(), e);
return null;
}
Engine current = engine();
Pair<BlockPos, Holder<Structure>> irisPlaced = findNearestIrisStructure(
level, holders, pos, Math.max(1, radius), findUnexplored, current);
HolderSet<Structure> reachable = filterReachableNativeStructures(level, holders, current);
Pair<BlockPos, Holder<Structure>> nativeLocated = reachable.size() == 0
? null
: super.findNearestMapStructure(level, reachable, pos, radius, findUnexplored);
return NativeStructureLocateResults.nearest(pos, irisPlaced, nativeLocated);
}
public boolean isNativeStructureReachable(Holder<Structure> structure) {
@@ -300,8 +336,11 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
private Pair<BlockPos, Holder<Structure>> findNearestIrisStructure(ServerLevel level,
HolderSet<Structure> holders,
BlockPos pos, int radius,
BlockPos pos, int radius, boolean findUnexplored,
Engine current) {
if (findUnexplored) {
return null;
}
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
BlockPos best = null;
Holder<Structure> bestHolder = null;
@@ -309,12 +348,19 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
for (Holder<Structure> holder : holders) {
Identifier 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(current,
structureId, NativeStructurePostProcessor.isUndergroundStep(holder.value().step()));
if (decision.status() != NativeStructureGenerationStatus.REPLACED_BY_IRIS) {
continue;
}
IrisStructureLocator.LocateResult result = IrisStructureLocator.locate(
current, id.toString(), pos.getX(), pos.getZ(), radius);
current, structureId, pos.getX(), pos.getZ(), radius);
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
continue;
throw new IllegalStateException("Iris structure locate reached its safety limit for "
+ structureId + " within " + radius + " chunks");
}
if (!result.found()) {
continue;
@@ -332,28 +378,23 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
}
private HolderSet<Structure> filterReachableNativeStructures(ServerLevel level, HolderSet<Structure> holders,
Engine current,
IrisImportedStructureControl control) {
try {
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
List<Holder<Structure>> kept = new ArrayList<>(holders.size());
for (Holder<Structure> holder : holders) {
Identifier id = registry.getKey(holder.value());
String key = id == null ? null : id.toString();
IrisNativeStructureDecision decision = control.resolve(
key, NativeStructurePostProcessor.isUndergroundStep(holder.value().step()));
if (key == null || !decision.generate()
|| IrisStructureLocator.suppressesVanilla(current, key)
|| !structureBiomeSource.isStructureReachable(holder)) {
continue;
}
kept.add(holder);
Engine current) {
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
List<Holder<Structure>> kept = new ArrayList<>(holders.size());
for (Holder<Structure> holder : holders) {
Identifier id = registry.getKey(holder.value());
if (id == null) {
throw new IllegalStateException("Native structure filtering received an unregistered structure holder");
}
return kept.size() == holders.size() ? holders : HolderSet.direct(kept);
} catch (Throwable error) {
LOGGER.error("Iris could not resolve native structure biome reachability; native locate failed closed", error);
return HolderSet.direct(List.of());
String key = id.toString();
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(current,
key, NativeStructurePostProcessor.isUndergroundStep(holder.value().step()));
if (!decision.generate() || !structureBiomeSource.isStructureReachable(holder)) {
continue;
}
kept.add(holder);
}
return kept.size() == holders.size() ? holders : HolderSet.direct(kept);
}
private ServerLevel boundLevel() {
@@ -389,6 +430,13 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
}
private Engine bindEngine(ServerLevel level) {
try {
requireGlobalStructureGeneration(
level.getServer().getWorldGenSettings().options().generateStructures(), dimensionKey);
} catch (RuntimeException error) {
engineBinding.fail(error);
throw error;
}
Engine cached = engine;
if (cached != null && !cached.isClosed() && cached.getComplex() != null) {
engineBinding.complete(cached);
@@ -424,6 +472,15 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
}
}
static void requireGlobalStructureGeneration(boolean enabled, String dimensionKey) {
if (enabled) {
return;
}
throw new IllegalStateException("Iris generator '" + dimensionKey
+ "' cannot bind while generate-structures=false; set generate-structures=true, restart the server, "
+ "and deny individual structures through importedStructures.disabled");
}
private Engine engineOrNull() {
Engine cached = engine;
if (cached != null) {
@@ -437,7 +494,13 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
}
Engine structureEngineOrNull() {
return engineOrNull();
engineBinding.throwIfFailed(dimensionKey);
Engine cached = engine;
if (cached != null && !cached.isClosed() && cached.getComplex() != null) {
return cached;
}
ServerLevel level = boundLevel();
return level == null ? null : bindEngine(level);
}
Engine awaitStructureEngine() {
@@ -523,6 +586,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
if (configuredPack != null) {
return configuredPack;
}
PackValidationRegistry.requireLoadable(activePack);
File packDirectory = ModdedWorldEngines.resolvePack(activePack, activeDimensionKey);
IrisData data = IrisData.get(packDirectory);
IrisDimension dimension = data.getDimensionLoader().load(activeDimensionKey);
@@ -807,19 +871,15 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
@Override
public void applyBiomeDecoration(WorldGenLevel level, ChunkAccess chunk, StructureManager structureManager) {
if (importedControl().active()) {
placeVanillaStructures(level, chunk, structureManager);
}
placeVanillaStructures(level, chunk, structureManager);
}
@Override
public void createStructures(RegistryAccess registryAccess, ChunkGeneratorStructureState structureState, StructureManager structureManager, ChunkAccess chunk, StructureTemplateManager templateManager, ResourceKey<Level> levelKey) {
if (!importedControl().active()) {
return;
}
Engine current = engine();
Map<Structure, StructureStart> previousStarts = new HashMap<>(chunk.getAllStarts());
super.createStructures(registryAccess, structureState, structureManager, chunk, templateManager, levelKey);
adjustGeneratedStructures(registryAccess, chunk, previousStarts);
adjustGeneratedStructures(registryAccess, chunk, previousStarts, current);
}
@Override
@@ -828,9 +888,9 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
}
private void adjustGeneratedStructures(RegistryAccess registryAccess, ChunkAccess chunk,
Map<Structure, StructureStart> previousStarts) {
Map<Structure, StructureStart> previousStarts, Engine current) {
Registry<Structure> registry = registryAccess.lookupOrThrow(Registries.STRUCTURE);
IrisImportedStructureControl control = importedControl();
ChunkPos chunkPos = chunk.getPos();
for (Map.Entry<Structure, StructureStart> entry : chunk.getAllStarts().entrySet()) {
Structure structure = entry.getKey();
StructureStart start = entry.getValue();
@@ -839,24 +899,37 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
}
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(current,
structureId, undergroundStep);
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"policy resolution", structureId, chunkPos.x(), chunkPos.z(), error);
}
if (!decision.generate()) {
chunk.setStartForStructure(structure, StructureStart.INVALID_START);
continue;
}
int offsetY;
try {
offsetY = NativeStructurePostProcessor.applyVerticalShift(
offsetY = NativeStructurePostProcessor.applyVerticalPlacement(
start,
structureId,
decision.yShift(),
getSeaLevel(),
chunk.getMinY(),
chunk.getMinY() + chunk.getHeight());
} catch (RuntimeException error) {
chunk.setStartForStructure(structure, StructureStart.INVALID_START);
LOGGER.error("Iris rejected native structure {} in chunk {},{} because its vertical bounds are invalid",
structureId, chunk.getPos().x(), chunk.getPos().z(), error);
continue;
chunk.getMinY() + chunk.getHeight(),
undergroundStep,
(x, z) -> current.getHeight(x, z, true) + current.getMinHeight());
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"vertical adjustment", structureId, chunkPos.x(), chunkPos.z(), error);
}
recordWorldCheckStructureShift(structureId, start.getChunkPos(), offsetY);
}
@@ -864,7 +937,11 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
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 generate-structures=false disables them outside the pack; set generate-structures=true, "
+ "restart the server, and deny individual structures through importedStructures.disabled");
}
ChunkPos chunkPos = chunk.getPos();
SectionPos sectionPos = SectionPos.of(chunkPos, world.getMinSectionY());
@@ -875,42 +952,61 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
long decorationSeed = random.setDecorationSeed(world.getSeed(), origin.getX(), origin.getZ());
BoundingBox area = writableArea(chunk);
int steps = GenerationStep.Decoration.values().length;
IrisImportedStructureControl control = importedControl();
Engine current = engine();
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)) {
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)) {
try {
if (structureId == null) {
throw NativeStructureGenerationException.failure(
"resolution", null, chunkPos.x(), chunkPos.z());
}
try {
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(current,
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) {
LOGGER.error("Iris failed to resolve native structure {} in chunk {},{}",
structureId, 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) -> current.getHeight(x, z, true) + current.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) {
LOGGER.error("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(decorationSeed, group.featureIndex(), group.step());
@@ -919,13 +1015,27 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
placeVanillaStructure(world, structureManager, random, area, chunkPos,
group.structureId(), start, group.decision());
}
} catch (Throwable e) {
LOGGER.error("Iris failed to place native structure {} in chunk {},{}",
group.structureId(), 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,
@@ -978,22 +1088,19 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
return worldCheckStructureShifts.get(new NativeStructureStartKey(structureId, startChunk.pack()));
}
private BlockState resolveStiltBlock(IrisVanillaStructureStiltSettings settings, RNG rng,
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 BlockState blockState)) {
return Blocks.COBBLESTONE.defaultBlockState();
throw new IllegalStateException("Configured native structure stilt palette did not resolve a Minecraft block at "
+ x + "," + y + "," + z);
}
return blockState;
}
private IrisImportedStructureControl importedControl() {
return engine().getDimension().getImportedStructures();
}
private BoundingBox writableArea(ChunkAccess chunk) {
ChunkPos chunkPos = chunk.getPos();
int minX = chunkPos.getMinBlockX();
@@ -1128,6 +1235,19 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
future.completeExceptionally(error);
}
void throwIfFailed(String dimensionKey) {
CompletableFuture<T> current = future;
if (!current.isCompletedExceptionally()) {
return;
}
try {
current.join();
} catch (CompletionException error) {
throw new IllegalStateException("Iris generator '" + dimensionKey + "' failed to bind",
error.getCause());
}
}
void reset() {
future = new CompletableFuture<>();
}
@@ -20,6 +20,7 @@ package art.arcane.iris.modded;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.LootResolver;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBlockData;
import art.arcane.iris.engine.object.IrisBlockDrops;
@@ -145,7 +146,7 @@ public final class ModdedBlockBreakHandler {
for (IrisBlockDrops provider : providers) {
replaceVanillaDrops |= provider.isReplaceVanillaDrops();
for (IrisLoot loot : provider.getDrops()) {
if (RNG.r.i(1, loot.getRarity()) != loot.getRarity()) {
if (!LootResolver.oneIn(RNG.r, loot.getRarity())) {
continue;
}
ItemStack stack = ModdedItemTranslator.stack(loot, RNG.r, level);
@@ -38,7 +38,6 @@ import java.util.concurrent.ConcurrentHashMap;
public final class ModdedDeathLoot {
private static final String TAG_PREFIX = "iris_loot|";
private static final int LOOT_RNG_SALT = 345911;
private static final Set<String> WARNED_TABLES = ConcurrentHashMap.newKeySet();
private static final Set<String> WARNED_ENTITY_TYPES = ConcurrentHashMap.newKeySet();
@@ -112,8 +111,7 @@ public final class ModdedDeathLoot {
}
continue;
}
RNG lootRng = new RNG(binding.seed()).nextParallelRNG(LOOT_RNG_SALT);
drops.addAll(ModdedItemTranslator.loot(table, lootRng, InventorySlotType.STORAGE, level,
drops.addAll(ModdedItemTranslator.loot(table, engine.getSeedManager().getLoot(), InventorySlotType.STORAGE, level,
binding.spawnX(), binding.spawnY(), binding.spawnZ()));
}
return drops;
@@ -68,7 +68,13 @@ public final class ModdedDimensionManager {
private ModdedDimensionManager() {
}
public static void bindAccess(ModdedServerAccess serverAccess) {
public static synchronized ModdedServerAccess bindAccess(ModdedServerAccess serverAccess) {
ModdedServerAccess previous = access;
access = serverAccess;
return previous;
}
public static synchronized void restoreAccess(ModdedServerAccess serverAccess) {
access = serverAccess;
}
@@ -110,12 +116,13 @@ public final class ModdedDimensionManager {
}
public static Handle create(MinecraftServer server, String dimensionId, String pack, String packDimensionKey, long seed) {
ModdedStartup.requirePackForWorldCreation(pack);
ModdedServerAccess serverAccess = requireAccess();
synchronized (LOCK) {
ResourceKey<Level> key = levelKey(dimensionId);
Handle existing = HANDLES.get(dimensionId);
if (existing != null && serverAccess.hasLevel(server, key)) {
existing.generator().repoint(pack, packDimensionKey, seed);
existing.generator().repointAndBind(existing.level(), pack, packDimensionKey, seed);
Handle refreshed = new Handle(dimensionId, pack, packDimensionKey, seed, existing.level(), existing.generator());
HANDLES.put(dimensionId, refreshed);
return refreshed;
@@ -126,7 +133,7 @@ public final class ModdedDimensionManager {
throw new IllegalStateException("Iris cannot inject dimension '" + dimensionId + "': a non-Iris level with that id is already loaded");
}
LOGGER.warn("Iris dimension '{}' is already present in the running server; reusing it", dimensionId);
generator.repoint(pack, packDimensionKey, seed);
generator.repointAndBind(present, pack, packDimensionKey, seed);
Handle handle = new Handle(dimensionId, pack, packDimensionKey, seed, present, generator);
HANDLES.put(dimensionId, handle);
return handle;
@@ -231,10 +238,6 @@ public final class ModdedDimensionManager {
private static Holder<DimensionType> resolveDimensionType(RegistryAccess registryAccess, String pack, String packDimensionKey) {
Registry<DimensionType> registry = registryAccess.lookupOrThrow(Registries.DIMENSION_TYPE);
IrisDimension dimension = loadPackDimension(pack, packDimensionKey);
if (dimension == null) {
throw new IllegalStateException("Iris cannot resolve dimension type for missing pack dimension '"
+ packDimensionKey + "' in pack '" + pack + "'");
}
String typeRef = ModdedForcedDatapack.dimensionTypeRef(dimension);
ResourceKey<DimensionType> typeKey = ResourceKey.create(Registries.DIMENSION_TYPE, Identifier.parse(typeRef));
Holder.Reference<DimensionType> packType = ModdedForcedDatapack.requireRegisteredDimensionType(
@@ -246,12 +249,22 @@ public final class ModdedDimensionManager {
try {
File packFolder = ModdedWorldEngines.packFolder(pack);
if (!packFolder.isDirectory()) {
return null;
throw new IllegalStateException("Iris pack directory is missing: " + packFolder.getAbsolutePath());
}
return IrisData.get(packFolder).getDimensionLoader().load(packDimensionKey);
IrisDimension dimension = IrisData.get(packFolder).getDimensionLoader().load(packDimensionKey);
if (dimension == null) {
throw new IllegalStateException("Iris pack '" + pack
+ "' does not contain dimension '" + packDimensionKey + "'");
}
return dimension;
} catch (Throwable e) {
LOGGER.warn("Iris could not load pack '{}' dimension '{}' for dimension type resolution: {}", pack, packDimensionKey, e.toString());
return null;
LOGGER.error("Iris could not load pack '{}' dimension '{}' for dimension type resolution",
pack, packDimensionKey, e);
if (e instanceof Error fatalError) {
throw fatalError;
}
throw new IllegalStateException("Iris could not load pack '" + pack + "' dimension '"
+ packDimensionKey + "' for dimension type resolution", e);
}
}
@@ -285,10 +298,56 @@ public final class ModdedDimensionManager {
List.of(),
false);
serverAccess.putLevel(server, key, level);
generator.bindLevel(level);
server.getPlayerList().addWorldborderListener(level);
return new Handle(dimensionId, pack, packDimensionKey, seed, level, generator);
try {
generator.bindLevel(level);
Handle handle = new Handle(dimensionId, pack, packDimensionKey, seed, level, generator);
ServerLevel previous = serverAccess.putLevelIfAbsent(server, key, level);
if (previous != null) {
throw new IllegalStateException("Iris cannot inject dimension '" + dimensionId
+ "': the level was registered concurrently");
}
server.getPlayerList().addWorldborderListener(level);
return handle;
} catch (Throwable error) {
rollbackInjection(server, serverAccess, key, level, generator, error);
if (error instanceof RuntimeException runtimeException) {
throw runtimeException;
}
if (error instanceof Error fatalError) {
throw fatalError;
}
throw new IllegalStateException("Iris runtime dimension injection failed for " + dimensionId, error);
}
}
private static void rollbackInjection(MinecraftServer server, ModdedServerAccess serverAccess,
ResourceKey<Level> key, ServerLevel level,
IrisModdedChunkGenerator generator, Throwable failure) {
try {
if (serverAccess.hasLevel(server, key)) {
ServerLevel removed = serverAccess.removeLevel(server, key);
if (removed != null && removed != level) {
serverAccess.putLevel(server, key, removed);
throw new IllegalStateException("Iris injection rollback encountered another registered level for "
+ key.identifier());
}
}
} catch (Throwable cleanupError) {
failure.addSuppressed(cleanupError);
LOGGER.error("Iris failed to remove a partially injected level for {}", key.identifier(), cleanupError);
}
try {
generator.unbindEngine(level);
} catch (Throwable cleanupError) {
failure.addSuppressed(cleanupError);
LOGGER.error("Iris failed to close a partially bound engine for {}", key.identifier(), cleanupError);
}
try {
level.close();
} catch (Throwable cleanupError) {
failure.addSuppressed(cleanupError);
LOGGER.error("Iris failed to close a partially injected level for {}", key.identifier(), cleanupError);
}
}
public static int evacuate(MinecraftServer server, ServerLevel from) {
@@ -42,6 +42,7 @@ import art.arcane.iris.modded.service.ModdedLogFilterService;
import art.arcane.iris.modded.service.ModdedPreservationService;
import art.arcane.iris.modded.service.ModdedSettingsHotloadService;
import art.arcane.iris.modded.service.ModdedStudioHotloadService;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.IrisServices;
import net.minecraft.core.BlockPos;
@@ -53,6 +54,8 @@ import net.minecraft.world.level.storage.LevelData;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayDeque;
public final class ModdedEngineBootstrap {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final String[] CORE_SELF_TEST_CLASSES = {
@@ -61,9 +64,9 @@ public final class ModdedEngineBootstrap {
"art.arcane.iris.core.loader.IrisData"
};
private static final Object LOCK = new Object();
private static final ModdedServiceManager SERVICE_MANAGER = new ModdedServiceManager();
private static final ModdedServiceManager UNBOUND_SERVICE_MANAGER = new ModdedServiceManager();
private static volatile ModdedLoader loader;
private static volatile ModdedPlatform platform;
private static volatile BoundRuntime runtime;
private static volatile MinecraftServer currentServer;
private static volatile MinecraftServer spawnCaptureServer;
private static volatile boolean initialSpawnWasDefault;
@@ -72,19 +75,20 @@ public final class ModdedEngineBootstrap {
}
public static ModdedServiceManager services() {
return SERVICE_MANAGER;
BoundRuntime bound = runtime;
return bound == null ? UNBOUND_SERVICE_MANAGER : bound.services();
}
public static ModdedScheduler schedulerOrNull() {
ModdedPlatform bound = platform;
return bound == null ? null : bound.moddedScheduler();
BoundRuntime bound = runtime;
return bound == null ? null : bound.platform().moddedScheduler();
}
public static void tick(MinecraftServer server) {
ModdedScheduler.tick(server);
ModdedStartup.runOnce(server);
ModdedPrimaryWorldRouter.tick(server);
SERVICE_MANAGER.tick(server);
services().tick(server);
ModdedPregenBossBar.tick(server);
ModdedProtocolHandler.tickDimensionSync(server);
}
@@ -93,14 +97,15 @@ public final class ModdedEngineBootstrap {
captureInitialSpawn(server);
currentServer = server;
bind();
bindWorldGenerators(server);
ModdedStartup.reset();
ModdedScheduler scheduler = schedulerOrNull();
if (scheduler != null) {
scheduler.reset();
}
ModdedStartup.prepareForStartup();
IrisModdedChunkGenerator.startGenPool();
SERVICE_MANAGER.enableAll();
bindWorldGenerators(server);
ModdedStartup.runOnce(server);
services().enableAll();
ModdedProtocolHandler.start(server);
ModdedSentry.start(loader());
}
@@ -115,6 +120,7 @@ public final class ModdedEngineBootstrap {
public static void serverAboutToStart(MinecraftServer server) {
captureInitialSpawn(server);
ModdedStartup.prepareForStartup();
}
public static void serverStarted(MinecraftServer server) {
@@ -140,7 +146,7 @@ public final class ModdedEngineBootstrap {
ModdedStudioCommands.clear();
ModdedWorldEngines.shutdown();
ModdedPrimaryWorldRouter.clear();
SERVICE_MANAGER.disableAll();
services().disableAll();
ModdedDimensionManager.clear();
ModdedScheduler scheduler = schedulerOrNull();
if (scheduler != null) {
@@ -256,45 +262,141 @@ public final class ModdedEngineBootstrap {
}
public static ModdedPlatform bind() {
ModdedPlatform bound = platform;
BoundRuntime bound = runtime;
if (bound != null) {
return bound;
return bound.platform();
}
synchronized (LOCK) {
if (platform != null) {
return platform;
BoundRuntime synchronizedBound = runtime;
if (synchronizedBound != null) {
return synchronizedBound.platform();
}
ModdedLoader boundLoader = loader();
GuiHost.suppressDesktop(boundLoader.clientEnvironment());
ModdedPlatform created = new ModdedPlatform(boundLoader);
IrisPlatforms.bind(created);
ModdedDimensionManager.bindAccess(new ModdedServerLevels());
IrisObjectRotation.bindFallbackRotator(new ModdedStateRotator());
BlockDataMergeSupport.bindFallbackMerger(new ModdedStateMerger());
TileData.bindFallbackReader(new ModdedTileReader(boundLoader::currentServer));
TileData.bindFallbackFactory(ModdedTileData::fromProperties);
ModdedGuiHost.install();
ModdedDecoratorHooks decoratorHooks = new ModdedDecoratorHooks();
DecoratorPlatformHooks.bind(decoratorHooks, decoratorHooks);
ModdedPreservationService preservation = SERVICE_MANAGER.register(ModdedPreservationService.class, new ModdedPreservationService());
SERVICE_MANAGER.register(ModdedLogFilterService.class, new ModdedLogFilterService());
SERVICE_MANAGER.register(ModdedEngineMaintenanceService.class, new ModdedEngineMaintenanceService());
SERVICE_MANAGER.register(ModdedSettingsHotloadService.class, new ModdedSettingsHotloadService());
ModdedStudioHotloadService studioHotloadService = SERVICE_MANAGER.register(ModdedStudioHotloadService.class, new ModdedStudioHotloadService());
SERVICE_MANAGER.register(ModdedChunkUpdateService.class, new ModdedChunkUpdateService());
SERVICE_MANAGER.register(ModdedEntitySpawnService.class, new ModdedEntitySpawnService());
IrisServices.register(PreservationRegistry.class, preservation);
IrisServices.register(EngineEffectsProvider.class, (EngineEffectsProvider) ModdedEngineEffects::new);
IrisServices.register(EnginePlatformHooks.class, studioHotloadService);
IrisServices.register(EngineWorldManagerProvider.class, (EngineWorldManagerProvider) (Engine engine) -> new ModdedWorldManager(engine));
ModdedCustomContentRegistry.discover();
platform = created;
SERVICE_MANAGER.enableAll();
if (boundLoader.clientEnvironment()) {
ModdedStartup.prefetchDefaultPack();
ModdedServiceManager createdServices = new ModdedServiceManager();
BindRollback rollback = new BindRollback();
try {
boolean previousDesktopSuppression = GuiHost.isDesktopSuppressed();
GuiHost.suppressDesktop(boundLoader.clientEnvironment());
rollback.add(() -> GuiHost.suppressDesktop(previousDesktopSuppression));
IrisPlatform previousPlatform = IrisPlatforms.isBound() ? IrisPlatforms.get() : null;
IrisPlatforms.bind(created);
rollback.add(() -> restorePlatform(previousPlatform));
ModdedServerAccess previousServerAccess = ModdedDimensionManager.bindAccess(new ModdedServerLevels());
rollback.add(() -> ModdedDimensionManager.restoreAccess(previousServerAccess));
IrisObjectRotation.StateRotator previousRotator = IrisObjectRotation.bindPlatformRotator(new ModdedStateRotator());
rollback.add(() -> IrisObjectRotation.restorePlatformRotator(previousRotator));
BlockDataMergeSupport.StateMerger previousMerger = BlockDataMergeSupport.bindPlatformMerger(new ModdedStateMerger());
rollback.add(() -> BlockDataMergeSupport.restorePlatformMerger(previousMerger));
TileData.TileReader previousTileReader = TileData.bindPlatformReader(new ModdedTileReader(boundLoader::currentServer));
rollback.add(() -> TileData.restorePlatformReader(previousTileReader));
TileData.TileFactory previousTileFactory = TileData.bindPlatformFactory(ModdedTileData::fromProperties);
rollback.add(() -> TileData.restorePlatformFactory(previousTileFactory));
GuiHost.Provider previousGuiHost = GuiHost.get();
ModdedGuiHost.install();
rollback.add(() -> GuiHost.set(previousGuiHost));
ModdedDecoratorHooks decoratorHooks = new ModdedDecoratorHooks();
DecoratorPlatformHooks.Bindings previousDecoratorBindings = DecoratorPlatformHooks.bind(decoratorHooks, decoratorHooks);
rollback.add(() -> DecoratorPlatformHooks.restore(previousDecoratorBindings));
ModdedPreservationService preservation = createdServices.register(
ModdedPreservationService.class, new ModdedPreservationService());
createdServices.register(ModdedLogFilterService.class, new ModdedLogFilterService());
createdServices.register(ModdedEngineMaintenanceService.class, new ModdedEngineMaintenanceService());
createdServices.register(ModdedSettingsHotloadService.class, new ModdedSettingsHotloadService());
ModdedStudioHotloadService studioHotloadService = createdServices.register(
ModdedStudioHotloadService.class, new ModdedStudioHotloadService());
createdServices.register(ModdedChunkUpdateService.class, new ModdedChunkUpdateService());
createdServices.register(ModdedEntitySpawnService.class, new ModdedEntitySpawnService());
bindService(PreservationRegistry.class, preservation, rollback);
bindService(EngineEffectsProvider.class,
(EngineEffectsProvider) ModdedEngineEffects::new, rollback);
bindService(EnginePlatformHooks.class, studioHotloadService, rollback);
bindService(EngineWorldManagerProvider.class,
(EngineWorldManagerProvider) (Engine engine) -> new ModdedWorldManager(engine), rollback);
ModdedCustomContentRegistry.Discovery customContentDiscovery =
ModdedCustomContentRegistry.discover();
rollback.add(customContentDiscovery::rollback);
ModdedIrisSplash.print(boundLoader);
createdServices.enableAll();
runtime = new BoundRuntime(created, createdServices);
rollback.clear();
return created;
} catch (Throwable failure) {
createdServices.rollback(failure);
rollback.restore(failure);
LOGGER.error("Iris modded platform binding failed", failure);
if (failure instanceof RuntimeException runtimeException) {
throw runtimeException;
}
if (failure instanceof Error fatalError) {
throw fatalError;
}
throw new IllegalStateException("Iris modded platform binding failed", failure);
}
ModdedIrisSplash.print(boundLoader);
return created;
}
}
private static <T> void bindService(Class<T> type, T service, BindRollback rollback) {
T previous = IrisServices.getOrNull(type);
IrisServices.register(type, service);
rollback.add(() -> restoreService(type, previous));
}
private static <T> void restoreService(Class<T> type, T service) {
if (service == null) {
IrisServices.remove(type);
return;
}
IrisServices.register(type, service);
}
private static void restorePlatform(IrisPlatform platform) {
IrisPlatforms.unbind();
if (platform != null) {
IrisPlatforms.bind(platform);
}
}
private record BoundRuntime(ModdedPlatform platform, ModdedServiceManager services) {
}
@FunctionalInterface
private interface RollbackAction {
void restore() throws Throwable;
}
private static final class BindRollback {
private final ArrayDeque<RollbackAction> actions = new ArrayDeque<>();
void add(RollbackAction action) {
actions.push(action);
}
void restore(Throwable failure) {
while (!actions.isEmpty()) {
try {
actions.pop().restore();
} catch (Throwable rollbackFailure) {
if (rollbackFailure != failure) {
failure.addSuppressed(rollbackFailure);
}
}
}
}
void clear() {
actions.clear();
}
}
}
@@ -20,6 +20,7 @@ package art.arcane.iris.modded;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.LootResolver;
import art.arcane.iris.engine.object.IrisAttributeModifier;
import art.arcane.iris.engine.object.IrisEntity;
import art.arcane.iris.engine.object.IrisEffect;
@@ -241,7 +242,7 @@ public final class ModdedEntitySpawner {
}
private static void setSlot(LivingEntity living, EquipmentSlot slot, IrisLoot loot, ServerLevel level, RNG rng) {
if (loot == null || rng.i(1, loot.getRarity()) != 1) {
if (loot == null || !LootResolver.oneIn(rng, loot.getRarity())) {
return;
}
ItemStack stack = ModdedItemTranslator.stack(loot, rng, level);
@@ -41,12 +41,15 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.stream.Stream;
@@ -62,9 +65,7 @@ public final class ModdedForcedDatapack {
public static RepositorySource repositorySource() {
return (Consumer<Pack> consumer) -> {
Pack pack = buildPack();
if (pack != null) {
consumer.accept(pack);
}
consumer.accept(pack);
};
}
@@ -78,9 +79,10 @@ public final class ModdedForcedDatapack {
private static Pack buildPack() {
Path directory = regenerate();
if (directory == null) {
return null;
}
return requireReadablePack(directory);
}
private static Pack requireReadablePack(Path directory) {
PackLocationInfo location = new PackLocationInfo(
PACK_ID,
Component.literal("Iris World Generation"),
@@ -90,7 +92,8 @@ public final class ModdedForcedDatapack {
PathPackResources.PathResourcesSupplier supplier = new PathPackResources.PathResourcesSupplier(directory);
Pack pack = Pack.readMetaAndCreate(location, supplier, PackType.SERVER_DATA, selection);
if (pack == null) {
LOGGER.error("Iris forced datapack at {} produced no readable pack metadata", directory);
throw new IllegalStateException("Iris forced datapack at " + directory
+ " produced no readable pack metadata");
}
return pack;
}
@@ -99,24 +102,41 @@ public final class ModdedForcedDatapack {
synchronized (LOCK) {
try {
return write();
} catch (IOException e) {
} catch (Throwable e) {
LOGGER.error("Iris failed to generate the forced startup datapack", e);
return null;
if (e instanceof RuntimeException runtimeException) {
throw runtimeException;
}
if (e instanceof Error fatalError) {
throw fatalError;
}
throw new IllegalStateException("Iris failed to generate the forced startup datapack", e);
}
}
}
private static Path write() throws IOException {
ModdedStartup.ensureDefaultPack();
Path datapackRoot = datapackRoot();
Files.createDirectories(datapackRoot);
Path stagingDirectory = Files.createTempDirectory(datapackRoot, PACK_FOLDER + ".staging-");
try {
ModdedStartup.ensureDefaultPack();
} catch (Throwable e) {
LOGGER.warn("Iris could not ensure the default pack before building the forced datapack", e);
writeStagedPack(stagingDirectory);
requireReadablePack(stagingDirectory);
publishDirectory(stagingDirectory, packDirectory());
return packDirectory();
} catch (IOException | RuntimeException | Error failure) {
try {
clean(stagingDirectory);
} catch (Throwable cleanupError) {
failure.addSuppressed(cleanupError);
}
throw failure;
}
Path packDirectory = packDirectory();
clean(packDirectory);
Files.createDirectories(packDirectory);
}
File packFolder = packDirectory.toFile();
private static void writeStagedPack(Path stagingDirectory) throws IOException {
File packFolder = stagingDirectory.toFile();
KList<File> folders = new KList<>();
folders.add(packFolder);
Map<String, KSet<String>> seenBiomes = new LinkedHashMap<>();
@@ -124,8 +144,13 @@ public final class ModdedForcedDatapack {
int packCount = 0;
KList<String> presetIds = new KList<>();
File[] packs = packsRoot().toFile().listFiles(File::isDirectory);
File root = packsRoot().toFile();
File[] packs = root.listFiles(File::isDirectory);
if (packs == null && root.exists()) {
throw new IOException("Iris could not read installed pack directory " + root.getAbsolutePath());
}
if (packs != null) {
Arrays.sort(packs, Comparator.comparing(File::getName));
for (File pack : packs) {
if (installPack(pack, fixer, folders, seenBiomes, presetIds)) {
packCount++;
@@ -133,46 +158,52 @@ public final class ModdedForcedDatapack {
}
}
writePackMeta(packDirectory);
writePackMeta(stagingDirectory);
if ("forge".equalsIgnoreCase(ModdedEngineBootstrap.loader().platformName())) {
writeForgeBlockLootModifier(packDirectory);
writeForgeBlockLootModifier(stagingDirectory);
}
if (!presetIds.isEmpty()) {
writeWorldPresetTag(packDirectory, presetIds);
writeWorldPresetTag(stagingDirectory, presetIds);
}
LOGGER.info("Iris forced startup datapack regenerated: {} pack(s), {} world preset(s), {} custom biome(s) at {}", packCount, presetIds.size(), countBiomes(seenBiomes), packDirectory);
LOGGER.info("Iris forced startup datapack staged: {} pack(s), {} world preset(s), {} custom biome(s) at {}", packCount, presetIds.size(), countBiomes(seenBiomes), stagingDirectory);
if (packCount == 0) {
LOGGER.warn("Iris installed NO worldgen packs into the forced datapack - custom biomes and their colors will NOT generate. Install a pack (e.g. /iris download overworld) and restart the server before creating an Iris world.");
}
return packDirectory;
}
private static boolean installPack(File packFolder, IDataFixer fixer, KList<File> folders, Map<String, KSet<String>> seenBiomes, KList<String> presetIds) {
private static boolean installPack(File packFolder, IDataFixer fixer, KList<File> folders,
Map<String, KSet<String>> seenBiomes,
KList<String> presetIds) throws IOException {
String packName = packFolder.getName();
File[] dimensionFiles = new File(packFolder, "dimensions").listFiles((File file) -> file.isFile() && file.getName().endsWith(".json"));
if (dimensionFiles == null || dimensionFiles.length == 0) {
File dimensionsDirectory = new File(packFolder, "dimensions");
if (!dimensionsDirectory.isDirectory()) {
return false;
}
boolean installed = false;
File[] dimensionFiles = dimensionsDirectory.listFiles(
(File file) -> file.isFile() && file.getName().endsWith(".json"));
if (dimensionFiles == null) {
throw new IOException("Iris could not read dimensions for pack '" + packName + "'");
}
if (dimensionFiles.length == 0) {
return false;
}
Arrays.sort(dimensionFiles, Comparator.comparing(File::getName));
IrisData data = IrisData.get(packFolder);
for (File dimensionFile : dimensionFiles) {
String dimensionKey = dimensionFile.getName().substring(0, dimensionFile.getName().length() - ".json".length());
try {
IrisData data = IrisData.get(packFolder);
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey);
if (dimension == null) {
continue;
}
dimension.installBiomes(fixer, () -> data, folders, biomesForNamespace(seenBiomes, dimension.getLoadKey()));
writeDimensionType(folders, fixer, dimension);
String presetKey = dimensionKey.equals(packName) ? packName : packName + "_" + dimensionKey;
writeWorldPreset(folders, dimension, packName, dimensionKey, presetKey);
presetIds.add("irisworldgen:" + presetKey);
installed = true;
} catch (Throwable e) {
LOGGER.error("Iris failed to install forced datapack content for pack '{}' dimension '{}'", packName, dimensionKey, e);
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey);
if (dimension == null) {
throw new IllegalStateException("Iris pack '" + packName + "' dimension '"
+ dimensionKey + "' did not load while building the forced datapack");
}
dimension.installBiomes(fixer, () -> data, folders,
biomesForNamespace(seenBiomes, dimension.getLoadKey()));
writeDimensionType(folders, fixer, dimension);
String presetKey = dimensionKey.equals(packName) ? packName : packName + "_" + dimensionKey;
writeWorldPreset(folders, dimension, packName, dimensionKey, presetKey);
presetIds.add("irisworldgen:" + presetKey);
}
return installed;
return true;
}
static KSet<String> biomesForNamespace(Map<String, KSet<String>> biomes, String namespace) {
@@ -319,6 +350,35 @@ public final class ModdedForcedDatapack {
}
}
static void publishDirectory(Path stagingDirectory, Path publishedDirectory) throws IOException {
Path backupDirectory = publishedDirectory.resolveSibling(
publishedDirectory.getFileName() + ".backup-" + UUID.randomUUID());
boolean hadPublishedDirectory = Files.exists(publishedDirectory);
if (hadPublishedDirectory) {
Files.move(publishedDirectory, backupDirectory, StandardCopyOption.ATOMIC_MOVE);
}
try {
Files.move(stagingDirectory, publishedDirectory, StandardCopyOption.ATOMIC_MOVE);
} catch (IOException | RuntimeException | Error failure) {
if (hadPublishedDirectory) {
try {
Files.move(backupDirectory, publishedDirectory, StandardCopyOption.ATOMIC_MOVE);
} catch (Throwable rollbackError) {
failure.addSuppressed(rollbackError);
}
}
throw failure;
}
if (hadPublishedDirectory) {
try {
clean(backupDirectory);
} catch (Throwable cleanupError) {
LOGGER.warn("Iris published the forced datapack but could not remove backup {}",
backupDirectory, cleanupError);
}
}
}
private static Path packsRoot() {
return ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve("packs");
}
@@ -18,12 +18,12 @@
package art.arcane.iris.modded;
import art.arcane.iris.engine.framework.LootResolver;
import art.arcane.iris.engine.object.InventorySlotType;
import art.arcane.iris.engine.object.IrisAttributeModifier;
import art.arcane.iris.engine.object.IrisEnchantment;
import art.arcane.iris.engine.object.IrisLoot;
import art.arcane.iris.engine.object.IrisLootTable;
import art.arcane.iris.engine.object.NoiseStyle;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
@@ -76,23 +76,25 @@ public final class ModdedItemTranslator {
private ModdedItemTranslator() {
}
public static KList<ItemStack> loot(IrisLootTable table, RNG rng, InventorySlotType slot, ServerLevel level, int x, int y, int z) {
public static KList<ItemStack> loot(IrisLootTable table, long lootSeed, InventorySlotType slot, ServerLevel level, int x, int y, int z) {
KList<ItemStack> out = new KList<>();
KList<IrisLoot> entries = table.getLoot();
if (entries.isEmpty()) {
if (entries.isEmpty() || table.getMaxTries() <= 0) {
return out;
}
RNG rng = LootResolver.tableRng(lootSeed, table, x, y, z);
int m = 0;
int c = 0;
int mx = rng.i(table.getMinPicked(), table.getMaxPicked());
int mx = Math.max(0, LootResolver.inclusive(rng, table.getMinPicked(), table.getMaxPicked()));
while (m < mx && c++ < table.getMaxTries()) {
IrisLoot entry = entries.get(rng.i(entries.size()));
if (entry.getSlotTypes() != slot) {
int entryIndex = rng.nextInt(entries.size());
IrisLoot entry = entries.get(entryIndex);
if (entry == null || entry.getSlotTypes() != slot) {
continue;
}
ItemStack item = item(entry, table, rng, level, x, y, z);
ItemStack item = item(entry, table, rng, lootSeed, entryIndex, level, x, y, z);
if (item != null && !item.isEmpty()) {
out.add(item);
m++;
@@ -117,8 +119,9 @@ public final class ModdedItemTranslator {
}
}
public static ItemStack item(IrisLoot loot, IrisLootTable table, RNG rng, ServerLevel level, int x, int y, int z) {
if (loot.getChance().aquire(() -> NoiseStyle.STATIC.create(rng)).fit(1, loot.getRarity() * table.getRarity(), x, y, z) != 1) {
public static ItemStack item(IrisLoot loot, IrisLootTable table, RNG rng, long lootSeed, int entryIndex, ServerLevel level, int x, int y, int z) {
long combinedRarity = LootResolver.combinedRarity(loot.getRarity(), table.getRarity());
if (!LootResolver.spatialOneIn(lootSeed, table, entryIndex, x, y, z, combinedRarity)) {
return null;
}
@@ -150,7 +153,7 @@ public final class ModdedItemTranslator {
}
Item item = BuiltInRegistries.ITEM.getValue(id);
return new ItemStack(item, Math.max(1, rng.i(loot.getMinAmount(), loot.getMaxAmount())));
return new ItemStack(item, Math.max(1, LootResolver.inclusive(rng, loot.getMinAmount(), loot.getMaxAmount())));
}
private static void applyComponents(IrisLoot loot, ItemStack stack, RNG rng, ServerLevel level) {
@@ -19,17 +19,14 @@
package art.arcane.iris.modded;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.LootResolver;
import art.arcane.iris.engine.framework.PlacedObject;
import art.arcane.iris.engine.object.IObjectLoot;
import art.arcane.iris.engine.object.InventorySlotType;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisLootMode;
import art.arcane.iris.engine.object.IrisLootReference;
import art.arcane.iris.engine.object.IrisLootTable;
import art.arcane.iris.engine.object.IrisObjectLoot;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisObjectVanillaLoot;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.volmlib.util.collection.KList;
@@ -42,10 +39,12 @@ import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.Container;
import net.minecraft.world.RandomizableContainer;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.ChestBlock;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.ChestType;
import net.minecraft.world.level.storage.loot.LootParams;
import net.minecraft.world.level.storage.loot.LootTable;
import net.minecraft.world.level.storage.loot.parameters.LootContextParamSets;
@@ -60,8 +59,13 @@ public final class ModdedLootApplier {
private ModdedLootApplier() {
}
public static void apply(Engine engine, ServerLevel level, BlockPos pos, BlockState state, MantleChunk<Matter> mc, RNG rng, int localX, int localZ) {
List<LootSource> sources = resolveSources(engine, level, rng, pos, state, mc);
public static void apply(Engine engine, ServerLevel level, BlockPos pos, BlockState state, MantleChunk<Matter> mc) {
if (!isCanonicalContainer(pos, state)) {
return;
}
RNG rng = LootResolver.containerRng(engine.getSeedManager().getLoot(), pos.getX(), pos.getY(), pos.getZ());
boolean nativeLootDefined = hasNativeLootTable(level, pos, state);
List<LootSource> sources = resolveSources(engine, level, rng, pos, state, mc, nativeLootDefined);
if (sources.isEmpty()) {
return;
}
@@ -80,7 +84,7 @@ public final class ModdedLootApplier {
if (table == null) {
continue;
}
items.addAll(ModdedItemTranslator.loot(table, rng, InventorySlotType.STORAGE, level, localX, pos.getY(), localZ));
items.addAll(ModdedItemTranslator.loot(table, engine.getSeedManager().getLoot(), InventorySlotType.STORAGE, level, pos.getX(), pos.getY(), pos.getZ()));
continue;
}
if (source instanceof NativeLootSource nativeSource) {
@@ -96,15 +100,18 @@ public final class ModdedLootApplier {
fillContainer(container, items, rng);
}
private static List<LootSource> resolveSources(Engine engine, ServerLevel level, RNG rng, BlockPos pos, BlockState state, MantleChunk<Matter> mc) {
private static List<LootSource> resolveSources(Engine engine, ServerLevel level, RNG rng, BlockPos pos, BlockState state, MantleChunk<Matter> mc, boolean nativeLootDefined) {
int rx = pos.getX();
int rz = pos.getZ();
int ry = pos.getY() - engine.getWorld().minHeight();
double he = engine.getComplex().getHeightStream().get(rx, rz);
List<LootSource> sources = new ArrayList<>();
boolean objectLootDefined = nativeLootDefined;
PlacedObject po = engine.getObjectPlacement(rx, ry, rz, mc);
if (po != null && po.getPlacement() != null && ModdedBlockResolution.isStorageChest(state)) {
objectLootDefined = objectLootDefined
|| po.getPlacement().getLoot().isNotEmpty()
|| po.getPlacement().getVanillaLoot().isNotEmpty();
Candidate picked = pickPlacementTable(engine, level, po.getPlacement(), state, rng);
if (picked != null) {
sources.add(picked.source());
@@ -114,17 +121,16 @@ public final class ModdedLootApplier {
}
}
IrisRegion region = engine.getComplex().getRegionStream().get(rx, rz);
IrisBiome biomeSurface = engine.getComplex().getTrueBiomeStream().get(rx, rz);
IrisBiome biomeUnder = ry < he ? engine.getCaveBiome(rx, ry, rz) : biomeSurface;
double multiplier = 1D * engine.getDimension().getLoot().getMultiplier() * region.getLoot().getMultiplier() * biomeSurface.getLoot().getMultiplier() * biomeUnder.getLoot().getMultiplier();
boolean fallback = sources.isEmpty();
injectReferenceSources(sources, engine.getDimension().getLoot(), engine, fallback);
injectReferenceSources(sources, region.getLoot(), engine, fallback);
injectReferenceSources(sources, biomeSurface.getLoot(), engine, fallback);
injectReferenceSources(sources, biomeUnder.getLoot(), engine, fallback);
scaleSources(sources, multiplier, rng);
LootResolver.resolveEnvironmentSources(
sources,
engine,
rng,
rx,
ry,
rz,
objectLootDefined,
IrisLootSource::new
);
return sources;
}
@@ -134,7 +140,7 @@ public final class ModdedLootApplier {
List<Candidate> global = new ArrayList<>();
for (IrisObjectLoot loot : placement.getLoot()) {
if (loot == null) {
if (loot == null || loot.getWeight() <= 0) {
continue;
}
IrisLootTable table = engine.getData().getLootLoader().load(loot.getName());
@@ -146,7 +152,7 @@ public final class ModdedLootApplier {
}
for (IrisObjectVanillaLoot loot : placement.getVanillaLoot()) {
if (loot == null) {
if (loot == null || loot.getWeight() <= 0) {
continue;
}
ResourceKey<LootTable> key = resolveNativeKey(loot.getName(), candidate -> hasNativeTable(level, candidate));
@@ -162,30 +168,7 @@ public final class ModdedLootApplier {
}
List<Candidate> pool = !exact.isEmpty() ? exact : !basic.isEmpty() ? basic : global;
return pickWeighted(pool, rng);
}
static <T> void injectSources(List<T> sources, List<? extends T> additions, IrisLootMode mode, boolean fallback) {
if (mode == IrisLootMode.FALLBACK && !fallback) {
return;
}
if (mode == IrisLootMode.CLEAR || mode == IrisLootMode.REPLACE) {
sources.clear();
}
sources.addAll(additions);
}
static <T> void scaleSources(List<T> sources, double multiplier, RNG rng) {
if (sources.isEmpty()) {
return;
}
int target = (int) Math.round(sources.size() * multiplier);
while (sources.size() < target && !sources.isEmpty()) {
sources.add(sources.get(rng.i(sources.size() - 1)));
}
while (sources.size() > target && !sources.isEmpty()) {
sources.remove(rng.i(sources.size() - 1));
}
return LootResolver.pickWeighted(pool, Candidate::weight, rng);
}
static ResourceKey<LootTable> resolveNativeKey(String name, Predicate<ResourceKey<LootTable>> registryContains) {
@@ -205,23 +188,6 @@ public final class ModdedLootApplier {
container.setChanged();
}
private static void injectReferenceSources(List<LootSource> sources, IrisLootReference reference, Engine engine, boolean fallback) {
IrisLootMode mode = reference.getMode();
if (mode == IrisLootMode.FALLBACK && !fallback) {
return;
}
injectSources(sources, irisSources(reference, engine), mode, fallback);
}
private static List<LootSource> irisSources(IrisLootReference reference, Engine engine) {
KList<IrisLootTable> tables = reference.getLootTables(engine.getComplex());
List<LootSource> sources = new ArrayList<>(tables.size());
for (IrisLootTable table : tables) {
sources.add(new IrisLootSource(table));
}
return sources;
}
private static boolean hasNativeTable(ServerLevel level, ResourceKey<LootTable> key) {
return level.getServer().reloadableRegistries().lookup()
.lookup(Registries.LOOT_TABLE)
@@ -244,25 +210,43 @@ public final class ModdedLootApplier {
}
}
private static Candidate pickWeighted(List<Candidate> pool, RNG rng) {
if (pool.isEmpty()) {
return null;
static boolean isCanonicalContainer(BlockPos pos, BlockState state) {
if (!(state.getBlock() instanceof ChestBlock) || state.getValue(ChestBlock.TYPE) == ChestType.SINGLE) {
return true;
}
int total = 0;
for (Candidate candidate : pool) {
total += Math.max(0, candidate.weight());
BlockPos connected = ChestBlock.getConnectedBlockPos(pos, state);
if (connected.equals(pos)) {
return true;
}
if (total <= 0) {
return pool.get(rng.nextInt(pool.size()));
return isCanonicalPair(pos, connected);
}
static boolean isCanonicalPair(BlockPos pos, BlockPos connected) {
return pos.getX() < connected.getX()
|| pos.getX() == connected.getX() && pos.getZ() <= connected.getZ();
}
static boolean hasNativeLootTable(ServerLevel level, BlockPos pos, BlockState state) {
if (hasNativeLootTableAt(level, pos)) {
return true;
}
int pull = rng.nextInt(total);
for (Candidate candidate : pool) {
pull -= Math.max(0, candidate.weight());
if (pull < 0) {
return candidate;
}
if (!(state.getBlock() instanceof ChestBlock) || state.getValue(ChestBlock.TYPE) == ChestType.SINGLE) {
return false;
}
return pool.get(pool.size() - 1);
BlockPos connected = ChestBlock.getConnectedBlockPos(pos, state);
return level.hasChunkAt(connected) && hasNativeLootTableAt(level, connected);
}
private static boolean hasNativeLootTableAt(ServerLevel level, BlockPos pos) {
return hasNativeLootTable(level.getBlockEntity(pos));
}
static boolean hasNativeLootTable(BlockEntity blockEntity) {
return blockEntity instanceof RandomizableContainer container && hasNativeLootTable(container);
}
static boolean hasNativeLootTable(RandomizableContainer container) {
return container != null && container.getLootTable() != null;
}
private static Container resolveContainer(ServerLevel level, BlockPos pos, BlockState state) {
@@ -33,6 +33,8 @@ public interface ModdedServerAccess {
ServerLevel putLevel(MinecraftServer server, ResourceKey<Level> key, ServerLevel level);
ServerLevel putLevelIfAbsent(MinecraftServer server, ResourceKey<Level> key, ServerLevel level);
ServerLevel removeLevel(MinecraftServer server, ResourceKey<Level> key);
boolean hasLevel(MinecraftServer server, ResourceKey<Level> key);
@@ -42,6 +42,11 @@ public final class ModdedServerLevels implements ModdedServerAccess {
return server.levels.put(key, level);
}
@Override
public ServerLevel putLevelIfAbsent(MinecraftServer server, ResourceKey<Level> key, ServerLevel level) {
return server.levels.putIfAbsent(key, level);
}
@Override
public ServerLevel removeLevel(MinecraftServer server, ResourceKey<Level> key) {
return server.levels.remove(key);
@@ -40,7 +40,13 @@ public final class ModdedServiceManager {
}
services.put(type, service);
if (enabled) {
enableService(service);
try {
service.onEnable();
} catch (Throwable failure) {
services.remove(type);
disableAfterFailure(service, failure);
throw serviceFailure(service, failure);
}
}
return service;
}
@@ -55,8 +61,18 @@ public final class ModdedServiceManager {
return;
}
enabled = true;
for (ModdedService service : services.values()) {
enableService(service);
ModdedService[] ordered = services.values().toArray(new ModdedService[0]);
for (int i = 0; i < ordered.length; i++) {
ModdedService service = ordered[i];
try {
service.onEnable();
} catch (Throwable failure) {
enabled = false;
for (int rollbackIndex = i; rollbackIndex >= 0; rollbackIndex--) {
disableAfterFailure(ordered[rollbackIndex], failure);
}
throw serviceFailure(service, failure);
}
}
}
@@ -79,12 +95,15 @@ public final class ModdedServiceManager {
forEachReversed(this::disableService);
}
private void enableService(ModdedService service) {
try {
service.onEnable();
} catch (Throwable error) {
LOGGER.error("Iris service onEnable failed for {}", service.getClass().getName(), error);
synchronized void rollback(Throwable failure) {
if (enabled) {
enabled = false;
ModdedService[] ordered = services.values().toArray(new ModdedService[0]);
for (int i = ordered.length - 1; i >= 0; i--) {
disableAfterFailure(ordered[i], failure);
}
}
services.clear();
}
private void disableService(ModdedService service) {
@@ -103,6 +122,28 @@ public final class ModdedServiceManager {
}
}
private void disableAfterFailure(ModdedService service, Throwable failure) {
try {
service.onDisable();
} catch (Throwable cleanupError) {
if (cleanupError != failure) {
failure.addSuppressed(cleanupError);
}
LOGGER.error("Iris service rollback failed for {}", service.getClass().getName(), cleanupError);
}
}
private RuntimeException serviceFailure(ModdedService service, Throwable failure) {
LOGGER.error("Iris service onEnable failed for {}", service.getClass().getName(), failure);
if (failure instanceof RuntimeException runtimeException) {
return runtimeException;
}
if (failure instanceof Error fatalError) {
throw fatalError;
}
return new IllegalStateException("Iris service failed to enable: " + service.getClass().getName(), failure);
}
private void forEachReversed(Consumer<ModdedService> action) {
ModdedService[] ordered = services.values().toArray(new ModdedService[0]);
for (int i = ordered.length - 1; i >= 0; i--) {
@@ -18,6 +18,7 @@
package art.arcane.iris.modded;
import art.arcane.iris.core.pack.BrokenPackException;
import art.arcane.iris.core.pack.PackDownloader;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.core.pack.PackValidationResult;
@@ -34,6 +35,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
public final class ModdedStartup {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final AtomicBoolean PREPARED = new AtomicBoolean(false);
private static final AtomicBoolean STARTED = new AtomicBoolean(false);
private static final Object PACK_LOCK = new Object();
@@ -41,9 +43,17 @@ public final class ModdedStartup {
}
public static void reset() {
PREPARED.set(false);
STARTED.set(false);
}
public static void prepareForStartup() {
if (!PREPARED.compareAndSet(false, true)) {
return;
}
validateAllPacks();
}
public static void prefetchDefaultPack() {
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
if (scheduler != null) {
@@ -56,30 +66,30 @@ public final class ModdedStartup {
}
public static void runOnce(MinecraftServer server) {
if (server == null || !STARTED.compareAndSet(false, true)) {
if (server == null) {
return;
}
prepareForStartup();
if (!STARTED.compareAndSet(false, true)) {
return;
}
reinjectPersistentDimensions(server);
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
if (scheduler == null) {
validateAllPacks();
ensureDefaultPack();
return;
}
scheduler.async(() -> {
validateAllPacks();
ensureDefaultPack();
});
scheduler.async(ModdedStartup::ensureDefaultPack);
}
public static void validateAllPacks() {
File packsRoot = ModdedPackCommands.packsRoot();
File[] packDirs = packsRoot.listFiles(File::isDirectory);
PackValidationRegistry.clear();
if (packDirs == null || packDirs.length == 0) {
return;
}
PackValidationRegistry.clear();
for (File packDir : packDirs) {
try {
PackValidationResult result = PackValidator.validate(packDir);
@@ -99,10 +109,50 @@ public final class ModdedStartup {
}
} catch (Throwable e) {
LOGGER.error("Iris pack validation failed for '{}'", packDir.getName(), e);
String detail = e.getMessage();
if (detail == null || detail.isBlank()) {
detail = e.getClass().getSimpleName();
}
PackValidationRegistry.publish(new PackValidationResult(
packDir.getName(),
List.of("Pack validation failed with " + e.getClass().getSimpleName() + ": " + detail),
List.of(),
System.currentTimeMillis()));
}
}
}
public static PackValidationResult requirePackForWorldCreation(String pack) {
if (pack == null || pack.isBlank()) {
throw new IllegalArgumentException("Pack name is required for world creation");
}
File packDir = new File(ModdedPackCommands.packsRoot(), pack);
if (!packDir.isDirectory()) {
throw new BrokenPackException(pack, List.of(
"Pack folder does not exist under " + ModdedPackCommands.packsRoot().getAbsolutePath() + "."));
}
try {
PackValidationResult result = PackValidator.validate(packDir);
PackValidationRegistry.publish(result);
return PackValidationRegistry.requireLoadable(pack);
} catch (BrokenPackException e) {
throw e;
} catch (Throwable e) {
LOGGER.error("Iris required world-creation validation failed for '{}'", pack, e);
String detail = e.getMessage();
if (detail == null || detail.isBlank()) {
detail = e.getClass().getSimpleName();
}
PackValidationResult failure = new PackValidationResult(
pack,
List.of("Pack validation failed with " + e.getClass().getSimpleName() + ": " + detail),
List.of(),
System.currentTimeMillis());
PackValidationRegistry.publish(failure);
throw new BrokenPackException(pack, failure.getBlockingErrors());
}
}
private static void reinjectPersistentDimensions(MinecraftServer server) {
List<ModdedDimensionRegistryStore.PersistentDimension> dimensions = ModdedDimensionRegistryStore.load(server);
if (dimensions.isEmpty()) {
@@ -115,6 +165,11 @@ public final class ModdedStartup {
injected++;
} catch (Throwable e) {
LOGGER.error("Iris failed to re-inject persistent dimension '{}' (pack={} dim={} seed={})", dimension.id(), dimension.pack(), dimension.dimension(), dimension.seed(), e);
if (e instanceof Error fatalError) {
throw fatalError;
}
throw new IllegalStateException("Iris failed to re-inject persistent dimension '"
+ dimension.id() + "' from pack '" + dimension.pack() + "'", e);
}
}
LOGGER.info("Iris re-injected {} persistent dimension(s) at startup", injected);
@@ -18,6 +18,7 @@
package art.arcane.iris.modded;
import art.arcane.iris.nativegen.NativeStructureGenerationException;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.PlatformStructureHooks;
import art.arcane.iris.spi.PlatformWorld;
@@ -51,6 +52,7 @@ import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;
@@ -59,7 +61,7 @@ public final class ModdedStructureHooks implements PlatformStructureHooks {
private final Supplier<MinecraftServer> server;
public ModdedStructureHooks(Supplier<MinecraftServer> server) {
this.server = server;
this.server = Objects.requireNonNull(server);
}
@Override
@@ -75,19 +77,16 @@ public final class ModdedStructureHooks implements PlatformStructureHooks {
@Override
public List<String> structureBiomeKeys(String structureKey) {
List<String> keys = new ArrayList<>();
MinecraftServer instance = requireServer("resolve biome keys for registered structure '" + structureKey + "'");
try {
MinecraftServer instance = server.get();
if (instance == null) {
return keys;
}
Identifier identifier = Identifier.tryParse(structureKey);
if (identifier == null) {
return keys;
throw new IllegalArgumentException("Registered structure key is invalid: " + structureKey);
}
Registry<Structure> registry = instance.registryAccess().lookupOrThrow(Registries.STRUCTURE);
Structure structure = registry.getValue(identifier);
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();
@@ -95,8 +94,13 @@ public final class ModdedStructureHooks implements PlatformStructureHooks {
keys.add(key.get().identifier().toString());
}
}
} catch (Throwable error) {
IrisLogging.reportError(error);
if (keys.isEmpty()) {
throw new IllegalStateException("Registered structure '" + structureKey
+ "' exposes no registered biome keys");
}
} catch (RuntimeException error) {
throw new IllegalStateException("Iris failed to resolve biome keys for registered structure '"
+ structureKey + "' from the modded structure registry", error);
}
return keys;
}
@@ -129,24 +133,19 @@ public final class ModdedStructureHooks implements PlatformStructureHooks {
@Override
public List<String> reachableStructureKeys(PlatformWorld world) {
List<String> keys = new ArrayList<>();
ServerLevel level = requireLevel(world, "resolve reachable structures");
try {
ServerLevel level = level(world);
if (level == null) {
return keys;
}
BiomeSource source = level.getChunkSource().getGenerator().getBiomeSource();
Set<String> possibleBiomes = possibleBiomeKeys(source);
if (possibleBiomes.isEmpty()) {
return keys;
}
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
for (Map.Entry<ResourceKey<Structure>, Structure> entry : registry.entrySet()) {
if (hasPossibleBiome(entry.getValue(), possibleBiomes)) {
keys.add(entry.getKey().identifier().toString());
}
}
} catch (Throwable error) {
IrisLogging.reportError(error);
} catch (RuntimeException error) {
throw new IllegalStateException("Iris failed to resolve reachable structures for modded level '"
+ level.dimension().identifier() + "'", error);
}
return keys;
}
@@ -154,14 +153,12 @@ public final class ModdedStructureHooks implements PlatformStructureHooks {
@Override
public List<String> possibleBiomeKeys(PlatformWorld world) {
List<String> keys = new ArrayList<>();
ServerLevel level = requireLevel(world, "resolve possible structure biome keys");
try {
ServerLevel level = level(world);
if (level == null) {
return keys;
}
keys.addAll(possibleBiomeKeys(level.getChunkSource().getGenerator().getBiomeSource()));
} catch (Throwable error) {
IrisLogging.reportError(error);
} catch (RuntimeException error) {
throw new IllegalStateException("Iris failed to resolve possible structure biome keys for modded level '"
+ level.dimension().identifier() + "'", error);
}
return keys;
}
@@ -230,9 +227,9 @@ public final class ModdedStructureHooks implements PlatformStructureHooks {
}
placeChunks(level, structureManager, generator, start, box, seed);
return bounds(box);
} catch (Throwable error) {
IrisLogging.reportError(error);
return null;
} catch (RuntimeException error) {
throw NativeStructureGenerationException.failure(
"capture placement", structureKey, chunkX, chunkZ, error);
}
}
@@ -264,16 +261,19 @@ public final class ModdedStructureHooks implements PlatformStructureHooks {
}
private 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<>();
for (Holder<Biome> holder : source.possibleBiomes()) {
Optional<ResourceKey<Biome>> key = holder.unwrapKey();
if (key.isPresent()) {
keys.add(key.get().identifier().toString());
}
}
if (keys.isEmpty()) {
throw new IllegalStateException("Minecraft biome source exposes no registered possible biomes");
}
return keys;
}
@@ -312,18 +312,38 @@ public final class ModdedStructureHooks implements PlatformStructureHooks {
private <T> List<String> registryKeys(ResourceKey<Registry<T>> registryKey) {
List<String> keys = new ArrayList<>();
MinecraftServer instance = requireServer("read registry '" + registryKey.identifier() + "'");
try {
MinecraftServer instance = server.get();
if (instance == null) {
return keys;
}
Registry<T> registry = instance.registryAccess().lookupOrThrow(registryKey);
for (Identifier identifier : registry.keySet()) {
keys.add(identifier.toString());
}
} catch (Throwable error) {
IrisLogging.reportError(error);
} catch (RuntimeException error) {
throw new IllegalStateException("Iris failed to read modded registry '"
+ registryKey.identifier() + "'", error);
}
return keys;
}
private MinecraftServer requireServer(String operation) {
MinecraftServer instance;
try {
instance = server.get();
} catch (RuntimeException error) {
throw new IllegalStateException("Iris failed to access the Minecraft server to " + operation, error);
}
if (instance == null) {
throw new IllegalStateException("Iris cannot " + operation + " before the Minecraft server is available");
}
return instance;
}
private static ServerLevel requireLevel(PlatformWorld world, String operation) {
ServerLevel level = level(world);
if (level == null) {
throw new IllegalStateException("Iris cannot " + operation
+ " without a bound modded ServerLevel");
}
return level;
}
}
@@ -19,6 +19,7 @@
package art.arcane.iris.modded;
import art.arcane.iris.core.nms.datapack.DataVersion;
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
import art.arcane.iris.engine.framework.StructureVerticalBounds;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
@@ -27,7 +28,6 @@ import art.arcane.volmlib.util.json.JSONObject;
import com.mojang.datafixers.util.Pair;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderSet;
import net.minecraft.core.Registry;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
@@ -43,14 +43,17 @@ import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
import net.minecraft.world.level.chunk.LevelChunkSection;
import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.placement.ConcentricRingsStructurePlacement;
import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement;
import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -74,6 +77,7 @@ public final class ModdedWorldCheck {
private static final int EXIT_FAILURE = 1;
private static final int MAX_FOOTPRINT_CHUNKS = 96;
private static final int MAX_START_REFERENCE_CHUNKS = 16;
private static final int MAX_STRUCTURE_CANDIDATES = 1024;
private static final long SERVER_WAIT_TIMEOUT_MILLIS = 600000L;
private static final long SERVER_WAIT_INTERVAL_MILLIS = 250L;
private static final List<StructureCheck> STRUCTURE_CHECKS = List.of(
@@ -136,8 +140,9 @@ public final class ModdedWorldCheck {
}
MinecraftServer serverRef = server;
WorldCheckPreparation preparation = serverRef.submit(() -> run(serverRef)).join();
exitCode = serverRef.submit(() -> runAndRequestStop(
() -> run(serverRef),
() -> completeWorldCheck(preparation),
() -> {
stopRequested.set(true);
serverRef.halt(false);
@@ -195,11 +200,12 @@ public final class ModdedWorldCheck {
processExit.exit(exitCode);
}
private static boolean run(MinecraftServer server) {
private static WorldCheckPreparation run(MinecraftServer server) {
ServerLevel level = targetLevel(server);
if (level == null) {
LOGGER.error("[worldcheck] no Iris dimension is loaded");
return false;
return new WorldCheckPreparation(false, false, false, false,
new NativeStructureGate(false, 0, false, null));
}
String levelId = level.dimension().identifier().toString();
@@ -268,16 +274,43 @@ public final class ModdedWorldCheck {
}
boolean entityMixinsOk = checkEntityMixins(level);
boolean structureOk = false;
if (generator != null) {
structureOk = checkNativeStructures(level, generator, spawn);
}
NativeStructureGate structureGate = generator == null
? new NativeStructureGate(false, 0, false, null)
: checkNativeStructures(level, generator, spawn);
boolean terrainOk = sectionsOk && varietyOk;
boolean nonStructurePass = irisGenerator && dimensionTypeOk && terrainOk && entityMixinsOk;
return new WorldCheckPreparation(nonStructurePass, terrainOk, dimensionTypeOk,
entityMixinsOk, structureGate);
}
boolean pass = irisGenerator && dimensionTypeOk && sectionsOk && varietyOk && entityMixinsOk && structureOk;
private static boolean completeWorldCheck(WorldCheckPreparation preparation) {
NativeStructureGate structureGate = preparation.structureGate();
boolean poiOk = false;
PendingVillagePoi pendingPoi = structureGate.pendingPoi();
if (pendingPoi != null) {
PoiAudit poi = auditStructurePois(pendingPoi.level(), pendingPoi.start());
poiOk = villagePoiPass(poi.inBounds(), poi.outOfBounds());
qaEvent("village_poi_metric", "village", poiOk,
"inBounds=" + poi.inBounds() + ",outOfBounds=" + poi.outOfBounds());
if (!poiOk) {
LOGGER.error("[worldcheck] village POI audit failed: inBounds={} outOfBounds={}",
poi.inBounds(), poi.outOfBounds());
}
} else {
qaEvent("village_poi_metric", "village", false, "skipped=structure");
}
int passed = structureGate.nonVillagePassed()
+ (structureGate.villagePassBeforePoi() && poiOk ? 1 : 0);
boolean structurePass = structureGate.passBeforePoi() && poiOk;
LOGGER.info("[worldcheck] native structure gate: {}/{} passed", passed, STRUCTURE_CHECKS.size());
qaEvent("structure_aggregate", "all", structurePass,
"passed=" + passed + ",total=" + STRUCTURE_CHECKS.size());
boolean pass = preparation.nonStructurePass() && structurePass;
LOGGER.info("[worldcheck] {}", pass ? "PASS" : "FAIL");
qaEvent("worldcheck_final", "all", pass,
"structures=" + STRUCTURE_CHECKS.size() + ",terrain=" + (sectionsOk && varietyOk)
+ ",dimensionType=" + dimensionTypeOk + ",entityMixins=" + entityMixinsOk);
"structures=" + STRUCTURE_CHECKS.size() + ",terrain=" + preparation.terrainOk()
+ ",dimensionType=" + preparation.dimensionTypeOk()
+ ",entityMixins=" + preparation.entityMixinsOk());
return pass;
}
@@ -354,25 +387,32 @@ public final class ModdedWorldCheck {
return pass;
}
private static boolean checkNativeStructures(ServerLevel level, IrisModdedChunkGenerator generator, BlockPos origin) {
private static NativeStructureGate checkNativeStructures(ServerLevel level,
IrisModdedChunkGenerator generator,
BlockPos origin) {
boolean pass = true;
int passed = 0;
int nonVillagePassed = 0;
boolean villagePass = false;
PendingVillagePoi pendingPoi = null;
for (StructureCheck check : STRUCTURE_CHECKS) {
boolean structurePass = checkNativeStructure(level, generator, origin, check);
if (structurePass) {
passed++;
} else {
StructureCheckResult result = checkNativeStructure(level, generator, origin, check);
if (!result.pass()) {
pass = false;
}
if (check.label().equals("village")) {
villagePass = result.pass();
pendingPoi = result.pendingPoi();
} else if (result.pass()) {
nonVillagePassed++;
}
}
LOGGER.info("[worldcheck] native structure gate: {}/{} passed", passed, STRUCTURE_CHECKS.size());
qaEvent("structure_aggregate", "all", pass,
"passed=" + passed + ",total=" + STRUCTURE_CHECKS.size());
return pass;
return new NativeStructureGate(pass, nonVillagePassed, villagePass, pendingPoi);
}
private static boolean checkNativeStructure(ServerLevel level, IrisModdedChunkGenerator generator,
BlockPos origin, StructureCheck check) {
private static StructureCheckResult checkNativeStructure(ServerLevel level,
IrisModdedChunkGenerator generator,
BlockPos origin,
StructureCheck check) {
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
List<Holder<Structure>> registered = new ArrayList<>(check.registryKeys().size());
LinkedHashSet<String> registeredKeys = new LinkedHashSet<>();
@@ -398,7 +438,7 @@ public final class ModdedWorldCheck {
emitSkipped(check, "registry", "structure_reachability", "structure_locate",
"structure_start_reference", "structure_footprint", "structure_material",
"structure_block_entity");
return false;
return new StructureCheckResult(false, null);
}
List<Holder<Structure>> reachable = new ArrayList<>(registered.size());
@@ -422,35 +462,35 @@ public final class ModdedWorldCheck {
LOGGER.error("[worldcheck] {} cannot generate in any biome produced by this Iris pack", check.label());
emitSkipped(check, "reachability", "structure_locate", "structure_start_reference",
"structure_footprint", "structure_material", "structure_block_entity");
return false;
return new StructureCheckResult(false, null);
}
long locateStart = System.nanoTime();
Pair<BlockPos, Holder<Structure>> found = generator.findNearestMapStructure(
level, HolderSet.direct(reachable), origin, check.locateRadius(), false);
Pair<BlockPos, Holder<Structure>> found = findGeneratedStructureCandidate(
level, reachable, origin, check.locateRadius());
long locateMillis = (System.nanoTime() - locateStart) / 1_000_000L;
Identifier foundKey = found == null ? null : registry.getKey(found.getSecond().value());
boolean locateOk = found != null && foundKey != null && reachableKeys.contains(foundKey.toString());
qaEvent("structure_locate", check.label(), locateOk,
"millis=" + locateMillis + ",radius=" + check.locateRadius()
"method=placement_candidates,millis=" + locateMillis + ",radius=" + check.locateRadius()
+ ",result=" + (foundKey == null ? "none" : foundKey));
if (found == null) {
LOGGER.error("[worldcheck] {} locate found no result within {} chunks after {}ms",
LOGGER.error("[worldcheck] {} native placement candidates produced no valid start within {} rings after {}ms",
check.label(), check.locateRadius(), locateMillis);
emitSkipped(check, "locate", "structure_start_reference", "structure_footprint",
"structure_material", "structure_block_entity");
return false;
return new StructureCheckResult(false, null);
}
BlockPos position = found.getFirst();
LOGGER.info("[worldcheck] {} locate: {} {} {} in {}ms (radius={}, unexplored=false, result={})",
LOGGER.info("[worldcheck] {} generated candidate: {} {} {} in {}ms (radius={}, result={})",
check.label(), position.getX(), position.getY(), position.getZ(), locateMillis,
check.locateRadius(), foundKey);
if (!locateOk) {
LOGGER.error("[worldcheck] {} locate returned unexpected structure {}", check.label(), foundKey);
LOGGER.error("[worldcheck] {} candidate scan returned unexpected structure {}", check.label(), foundKey);
emitSkipped(check, "locate", "structure_start_reference", "structure_footprint",
"structure_material", "structure_block_entity");
return false;
return new StructureCheckResult(false, null);
}
int chunkX = position.getX() >> 4;
@@ -471,11 +511,12 @@ public final class ModdedWorldCheck {
check.label(), chunkX, chunkZ);
emitSkipped(check, "start_reference", "structure_footprint", "structure_material",
"structure_block_entity");
return false;
return new StructureCheckResult(false, null);
}
IrisNativeStructureDecision decision = generator.commandEngine().getDimension().getImportedStructures()
.resolve(foundKey.toString(), NativeStructurePostProcessor.isUndergroundStep(structure.step()));
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(
generator.commandEngine(), foundKey.toString(),
NativeStructurePostProcessor.isUndergroundStep(structure.step()));
Integer appliedShift = generator.worldCheckStructureShift(foundKey.toString(), start.getChunkPos());
BoundingBox shiftedBounds = start.getBoundingBox();
boolean verticalShiftOk = verticalShiftMatches(
@@ -522,22 +563,17 @@ public final class ModdedWorldCheck {
}
boolean foundationOk = true;
boolean poiOk = true;
PendingVillagePoi pendingPoi = null;
if (check.label().equals("village")) {
foundationOk = villageFoundationPass(footprint.foundationGapColumns());
LOGGER.info("[worldcheck] village foundation metric: cobblestone below piece bases={} columns={} unsupported={}",
footprint.foundationBlocks(), footprint.foundationColumns(), footprint.foundationGapColumns());
LOGGER.info("[worldcheck] village foundation metric: bases={} cobblestone={} columns={} unsupported={}",
footprint.foundationBaseColumns(), footprint.foundationBlocks(),
footprint.foundationColumns(), footprint.foundationGapColumns());
qaEvent("village_foundation_metric", check.label(), foundationOk,
"cobblestoneBelowBase=" + footprint.foundationBlocks() + ",columns="
"bases=" + footprint.foundationBaseColumns() + ",cobblestoneBelowBase="
+ footprint.foundationBlocks() + ",columns="
+ footprint.foundationColumns() + ",unsupported=" + footprint.foundationGapColumns());
PoiAudit poi = auditStructurePois(level, start);
poiOk = villagePoiPass(poi.inBounds(), poi.outOfBounds());
qaEvent("village_poi_metric", check.label(), poiOk,
"inBounds=" + poi.inBounds() + ",outOfBounds=" + poi.outOfBounds());
if (!poiOk) {
LOGGER.error("[worldcheck] village POI audit failed: inBounds={} outOfBounds={}",
poi.inBounds(), poi.outOfBounds());
}
pendingPoi = new PendingVillagePoi(level, start);
}
boolean blockEntityOk = footprint.blockEntityStates() == footprint.blockEntitiesPresent();
@@ -562,8 +598,9 @@ public final class ModdedWorldCheck {
if (!foundationOk) {
LOGGER.error("[worldcheck] village has unsupported foundation columns after stilt placement");
}
return verticalShiftOk && footprintOk && materialOk && blockEntityOk
&& vegetationOk && foundationOk && poiOk;
boolean pass = verticalShiftOk && footprintOk && materialOk && blockEntityOk
&& vegetationOk && foundationOk;
return new StructureCheckResult(pass, pass ? pendingPoi : null);
}
private static StructureStart resolveStructureStart(ServerLevel level, ChunkAccess targetChunk,
@@ -586,6 +623,96 @@ public final class ModdedWorldCheck {
return direct;
}
private static Pair<BlockPos, Holder<Structure>> findGeneratedStructureCandidate(
ServerLevel level, List<Holder<Structure>> structures, BlockPos origin, int maxRadius) {
ChunkGeneratorStructureState state = level.getChunkSource().getGeneratorState();
Set<Long> attempted = new LinkedHashSet<>();
for (Holder<Structure> structure : structures) {
for (StructurePlacement placement : state.getPlacementsForStructure(structure)) {
if (!(placement instanceof ConcentricRingsStructurePlacement rings)) {
continue;
}
List<ChunkPos> positions = state.getRingPositionsFor(rings);
if (positions == null) {
continue;
}
List<ChunkPos> sorted = new ArrayList<>(positions);
sorted.sort(Comparator.comparingLong(position -> distanceSquared(origin, position)));
for (ChunkPos position : sorted) {
Pair<BlockPos, Holder<Structure>> found = inspectStructureCandidate(
level, structures, placement, position, attempted);
if (found != null) {
return found;
}
if (attempted.size() >= MAX_STRUCTURE_CANDIDATES) {
return null;
}
}
}
}
int originChunkX = origin.getX() >> 4;
int originChunkZ = origin.getZ() >> 4;
for (int radius = 0; radius <= maxRadius; radius++) {
for (Holder<Structure> structure : structures) {
for (StructurePlacement placement : state.getPlacementsForStructure(structure)) {
if (!(placement instanceof RandomSpreadStructurePlacement randomSpread)) {
continue;
}
for (int x = -radius; x <= radius; x++) {
boolean xEdge = x == -radius || x == radius;
for (int z = -radius; z <= radius; z++) {
if (!xEdge && z != -radius && z != radius) {
continue;
}
int sectorX = originChunkX + randomSpread.spacing() * x;
int sectorZ = originChunkZ + randomSpread.spacing() * z;
ChunkPos candidate = randomSpread.getPotentialStructureChunk(
state.getLevelSeed(), sectorX, sectorZ);
if (!placement.isStructureChunk(state, candidate.x(), candidate.z())) {
continue;
}
Pair<BlockPos, Holder<Structure>> found = inspectStructureCandidate(
level, structures, placement, candidate, attempted);
if (found != null) {
return found;
}
if (attempted.size() >= MAX_STRUCTURE_CANDIDATES) {
return null;
}
}
}
}
}
}
return null;
}
private static Pair<BlockPos, Holder<Structure>> inspectStructureCandidate(
ServerLevel level, List<Holder<Structure>> structures, StructurePlacement placement,
ChunkPos candidate, Set<Long> attempted) {
if (!attempted.add(candidate.pack())) {
return null;
}
ChunkAccess chunk = level.getChunk(candidate.x(), candidate.z());
for (Holder<Structure> structure : structures) {
StructureStart start = resolveStructureStart(level, chunk, structure.value());
if (start == null || !start.isValid()) {
continue;
}
BlockPos locate = placement.getLocatePos(start.getChunkPos());
BlockPos resolved = new BlockPos(locate.getX(), start.getBoundingBox().minY(), locate.getZ());
return Pair.of(resolved, structure);
}
return null;
}
private static long distanceSquared(BlockPos origin, ChunkPos position) {
long x = (long) position.getMinBlockX() - origin.getX();
long z = (long) position.getMinBlockZ() - origin.getZ();
return x * x + z * z;
}
private static FootprintAudit auditFootprint(ServerLevel level, Structure structure, StructureStart start,
StructureCheck check, Identifier structureKey) {
List<StructurePiece> pieces = start.getPieces();
@@ -600,11 +727,11 @@ public final class ModdedWorldCheck {
int characteristicChunks = 0;
int vegetationBlocks = 0;
int vegetationColumns = 0;
int foundationBaseColumns = 0;
int foundationBlocks = 0;
int foundationColumns = 0;
int foundationGapColumns = 0;
BitSet visited = new BitSet(level.getHeight() << 8);
int[] minimumPieceY = new int[256];
int[] maximumPieceY = new int[256];
for (ChunkPos chunkPos : selected) {
ChunkAccess chunk = level.getChunk(chunkPos.x(), chunkPos.z());
@@ -617,8 +744,8 @@ public final class ModdedWorldCheck {
BlockEntityAudit blockEntities = auditBlockEntities(level, chunk);
blockEntityStates += blockEntities.states();
blockEntitiesPresent += blockEntities.present();
StructureMaterialAudit material = auditStructureMaterial(level, chunk, pieces, check,
structureKey, visited, minimumPieceY, maximumPieceY);
StructureMaterialAudit material = auditStructureMaterial(level, chunk, start, check,
structureKey, visited, maximumPieceY);
if (material.scanned()) {
materialScannedChunks++;
}
@@ -628,6 +755,7 @@ public final class ModdedWorldCheck {
}
vegetationBlocks += material.vegetationBlocks();
vegetationColumns += material.vegetationColumns();
foundationBaseColumns += material.foundationBaseColumns();
foundationBlocks += material.foundationBlocks();
foundationColumns += material.foundationColumns();
foundationGapColumns += material.foundationGapColumns();
@@ -648,19 +776,18 @@ public final class ModdedWorldCheck {
return new FootprintAudit(selected.size(), availableChunks, evidenceChunks,
coveredPieces, pieces.size(), blockEntityStates, blockEntitiesPresent,
materialScannedChunks, characteristicBlocks, characteristicChunks,
vegetationBlocks, vegetationColumns, foundationBlocks, foundationColumns,
foundationGapColumns);
vegetationBlocks, vegetationColumns, foundationBaseColumns, foundationBlocks,
foundationColumns, foundationGapColumns);
}
private static StructureMaterialAudit auditStructureMaterial(ServerLevel level, ChunkAccess chunk,
List<StructurePiece> pieces,
StructureStart start,
StructureCheck check,
Identifier structureKey,
BitSet visited,
int[] minimumPieceY,
int[] maximumPieceY) {
List<StructurePiece> pieces = start.getPieces();
visited.clear();
Arrays.fill(minimumPieceY, Integer.MAX_VALUE);
Arrays.fill(maximumPieceY, Integer.MIN_VALUE);
int characteristicBlocks = 0;
int minimumWorldY = level.getMinY();
@@ -683,20 +810,10 @@ public final class ModdedWorldCheck {
continue;
}
scanned = true;
int metricMinimumY = minimumY;
boolean metricBaseValid = true;
if (check.label().equals("village") && piece instanceof PoolElementStructurePiece poolPiece) {
int groundY = bounds.minY() + poolPiece.getGroundLevelDelta();
metricBaseValid = groundY >= bounds.minY();
metricMinimumY = Math.max(minimumWorldY, Math.min(maximumWorldY, groundY));
}
for (int z = minimumZ; z <= maximumZ; z++) {
int localZ = z - minimumChunkZ;
for (int x = minimumX; x <= maximumX; x++) {
int column = (localZ << 4) | (x - minimumChunkX);
if (metricBaseValid) {
minimumPieceY[column] = Math.min(minimumPieceY[column], metricMinimumY);
}
maximumPieceY[column] = Math.max(maximumPieceY[column], maximumY);
}
}
@@ -747,55 +864,26 @@ public final class ModdedWorldCheck {
}
}
int foundationBaseColumns = 0;
int foundationBlocks = 0;
int foundationColumns = 0;
int foundationGapColumns = 0;
if (check.label().equals("village")) {
for (int column = 0; column < minimumPieceY.length; column++) {
int minimumY = minimumPieceY[column];
if (minimumY == Integer.MAX_VALUE) {
continue;
}
boolean foundationColumn = false;
int x = minimumChunkX + (column & 15);
int z = minimumChunkZ + (column >> 4);
int foundationY = Integer.MIN_VALUE;
int foundationMaximumY = Math.min(maximumWorldY, minimumY + 1);
for (int y = minimumY; y <= foundationMaximumY; y++) {
if (chunk.getBlockState(position.set(x, y, z)).isSolid()) {
foundationY = y;
break;
}
}
boolean basePresent = foundationY != Integer.MIN_VALUE;
boolean supportSolid = false;
boolean vegetationSupport = false;
if (basePresent && foundationY > minimumWorldY) {
BlockState support = chunk.getBlockState(position.set(x, foundationY - 1, z));
supportSolid = support.isSolid();
vegetationSupport = support.is(BlockTags.LOGS) || support.is(BlockTags.LEAVES);
}
boolean worldFloorBase = basePresent && foundationY == minimumWorldY;
if (!villageFoundationSupported(basePresent, worldFloorBase, supportSolid, vegetationSupport)) {
foundationGapColumns++;
continue;
}
for (int y = foundationY - 1; y >= minimumWorldY; y--) {
BlockState state = chunk.getBlockState(position.set(x, y, z));
if (!state.is(Blocks.COBBLESTONE)) {
break;
}
foundationBlocks++;
foundationColumn = true;
}
if (foundationColumn) {
foundationColumns++;
}
}
BoundingBox area = new BoundingBox(
minimumChunkX, minimumWorldY, minimumChunkZ,
maximumChunkX, maximumWorldY, maximumChunkZ);
NativeStructurePostProcessor.StiltSupportAudit foundation =
NativeStructurePostProcessor.auditStiltSupport(
level, area, start, Blocks.COBBLESTONE.defaultBlockState());
foundationBaseColumns = foundation.baseColumns();
foundationBlocks = foundation.stiltBlocks();
foundationColumns = foundation.stiltColumns();
foundationGapColumns = foundation.unsupportedColumns();
}
return new StructureMaterialAudit(scanned, characteristicBlocks, vegetationBlocks,
vegetationColumns, foundationBlocks, foundationColumns, foundationGapColumns);
vegetationColumns, foundationBaseColumns, foundationBlocks, foundationColumns,
foundationGapColumns);
}
private static List<ChunkPos> selectFootprintChunks(StructureStart start, int limit) {
@@ -1036,11 +1124,6 @@ public final class ModdedWorldCheck {
return vegetation && blockY > highestPieceY;
}
static boolean villageFoundationSupported(boolean basePresent, boolean worldFloorBase,
boolean supportSolid, boolean vegetationSupport) {
return basePresent && (worldFloorBase || (supportSolid && !vegetationSupport));
}
static boolean villageFoundationPass(int unsupportedColumns) {
return unsupportedColumns == 0;
}
@@ -1194,12 +1277,27 @@ public final class ModdedWorldCheck {
private record StructureCheck(String label, List<String> registryKeys, int locateRadius) {
}
private record WorldCheckPreparation(boolean nonStructurePass, boolean terrainOk,
boolean dimensionTypeOk, boolean entityMixinsOk,
NativeStructureGate structureGate) {
}
private record NativeStructureGate(boolean passBeforePoi, int nonVillagePassed,
boolean villagePassBeforePoi, PendingVillagePoi pendingPoi) {
}
private record StructureCheckResult(boolean pass, PendingVillagePoi pendingPoi) {
}
private record PendingVillagePoi(ServerLevel level, StructureStart start) {
}
private record FootprintAudit(int inspectedChunks, int availableChunks, int evidenceChunks,
int coveredPieces, int totalPieces, int blockEntityStates,
int blockEntitiesPresent, int materialScannedChunks,
int characteristicBlocks, int characteristicChunks,
int vegetationBlocks, int vegetationColumns,
int foundationBlocks, int foundationColumns,
int foundationBaseColumns, int foundationBlocks, int foundationColumns,
int foundationGapColumns) {
}
@@ -1208,7 +1306,8 @@ public final class ModdedWorldCheck {
private record StructureMaterialAudit(boolean scanned, int characteristicBlocks,
int vegetationBlocks, int vegetationColumns,
int foundationBlocks, int foundationColumns,
int foundationBaseColumns, int foundationBlocks,
int foundationColumns,
int foundationGapColumns) {
}
@@ -19,6 +19,7 @@
package art.arcane.iris.modded;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineTarget;
@@ -35,6 +36,7 @@ import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
public final class ModdedWorldEngines {
@@ -57,22 +59,48 @@ public final class ModdedWorldEngines {
}
public static void evict(ServerLevel level) {
Engine removed = ENGINES.remove(level);
if (removed == null) {
return;
}
try {
if (!removed.isClosed()) {
removed.close();
}
LOGGER.info("Iris engine evicted for {}", level.dimension().identifier());
evictOrThrow(level);
} catch (Throwable e) {
LOGGER.error("Iris engine evict close failed for {}", level.dimension().identifier(), e);
}
}
static void evictOrThrow(ServerLevel level) {
Engine[] removed = new Engine[1];
ENGINES.computeIfPresent(level, (ServerLevel ignored, Engine current) -> {
close(current);
removed[0] = current;
return null;
});
if (removed[0] == null) {
return;
}
LOGGER.info("Iris engine evicted for {}", level.dimension().identifier());
}
static Engine prepareReplacement(ServerLevel level, String pack, String dimensionKey, long seedOverride) {
return create(level, pack, dimensionKey, seedOverride);
}
static void installReplacement(ServerLevel level, Engine replacement) {
ServerLevel activeLevel = Objects.requireNonNull(level);
Engine activeReplacement = Objects.requireNonNull(replacement);
ENGINES.compute(activeLevel, (ServerLevel ignored, Engine current) -> {
if (current != null && current != activeReplacement) {
close(current);
}
return activeReplacement;
});
}
static void closeUnregistered(Engine engine) {
close(engine);
}
private static Engine create(ServerLevel level, String pack, String dimensionKey, long seedOverride) {
ModdedEngineBootstrap.bind();
PackValidationRegistry.requireLoadable(pack);
File packDir = resolvePack(pack, dimensionKey);
IrisData data = IrisData.get(packDir);
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey);
@@ -95,6 +123,16 @@ public final class ModdedWorldEngines {
.platformWorld(new ModdedPlatformWorld(level))
.build();
Engine engine = new IrisEngine(new EngineTarget(world, dimension, data), false);
if (engine.isClosed() || engine.getComplex() == null) {
IllegalStateException failure = new IllegalStateException("Iris engine for "
+ level.dimension().identifier() + " did not initialize a ready biome complex");
try {
close(engine);
} catch (Throwable cleanupError) {
failure.addSuppressed(cleanupError);
}
throw failure;
}
LOGGER.info("Iris engine up for {}: pack={} dim={} seed={} height={}..{}",
level.dimension().identifier(), packDir.getAbsolutePath(), dimension.getLoadKey(), seed, dimension.getMinHeight(), dimension.getMaxHeight());
@@ -131,24 +169,6 @@ public final class ModdedWorldEngines {
return packDir;
}
String parity = System.getProperty("iris.parity");
if (parity != null) {
String parityPath = parity;
int lastColon = parity.lastIndexOf(':');
if (lastColon > 0) {
try {
Integer.parseInt(parity.substring(lastColon + 1));
parityPath = parity.substring(0, lastColon);
} catch (NumberFormatException ignored) {
}
}
File parityPack = new File(parityPath);
if (parityPack.isDirectory()) {
LOGGER.warn("Iris pack missing at {}; falling back to parity pack {}", packDir.getAbsolutePath(), parityPack.getAbsolutePath());
return parityPack;
}
}
LOGGER.error("===============================================================");
LOGGER.error("Iris pack '{}' is not installed.", pack);
LOGGER.error("Expected a pack folder at: {}", packDir.getAbsolutePath());
@@ -157,6 +177,12 @@ public final class ModdedWorldEngines {
throw new IllegalStateException("Iris pack not installed: " + packDir.getAbsolutePath());
}
private static void close(Engine engine) {
if (engine != null && !engine.isClosed()) {
engine.close();
}
}
public static void shutdown() {
for (Map.Entry<ServerLevel, Engine> entry : ENGINES.entrySet()) {
Engine engine = entry.getValue();
@@ -23,6 +23,7 @@ import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineWorldManager;
import art.arcane.iris.engine.framework.LootResolver;
import art.arcane.iris.engine.object.IRare;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisEntity;
@@ -492,7 +493,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
int min = entry.getMinSpawns();
int max = entry.getMaxSpawns();
int count = min == max ? min : RNG.r.i(Math.min(min, max), Math.max(min, max));
int count = LootResolver.inclusive(RNG.r, min, max);
if (count <= 0) {
return 0;
}
@@ -516,8 +517,8 @@ public final class ModdedWorldManager implements EngineWorldManager {
worldY = caveFloor.getY() + 1;
worldZ = caveFloor.getZ();
} else {
worldX = (chunkX << 4) + RNG.r.i(15);
worldZ = (chunkZ << 4) + RNG.r.i(15);
worldX = (chunkX << 4) + RNG.r.i(16);
worldZ = (chunkZ << 4) + RNG.r.i(16);
int surfaceY = level.getMinY() + engine.getHeight(worldX, worldZ, false);
int solidY = level.getMinY() + engine.getHeight(worldX, worldZ, true);
worldY = group == IrisSpawnGroup.NORMAL
@@ -527,6 +528,9 @@ public final class ModdedWorldManager implements EngineWorldManager {
if (worldY <= level.getMinY() || worldY >= level.getMaxY()) {
continue;
}
if (!LootResolver.oneIn(entityRng, entry.getRarity())) {
continue;
}
if (!lightAllowed(spawner, level, worldX, worldY, worldZ)) {
continue;
}
@@ -551,7 +555,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
int min = entry.getMinSpawns();
int max = entry.getMaxSpawns();
int count = min == max ? min : RNG.r.i(Math.min(min, max), Math.max(min, max));
int count = LootResolver.inclusive(RNG.r, min, max);
if (count <= 0) {
return 0;
}
@@ -564,6 +568,9 @@ public final class ModdedWorldManager implements EngineWorldManager {
int worldZ = position.getZ();
int spawned = 0;
for (int i = 0; i < count; i++) {
if (!LootResolver.oneIn(entityRng, entry.getRarity())) {
continue;
}
if (!lightAllowed(spawner, level, worldX, worldY, worldZ)) {
continue;
}
@@ -28,9 +28,11 @@ import net.minecraft.world.level.block.state.BlockState;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.ServiceLoader;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
@@ -40,11 +42,12 @@ public final class ModdedCustomContentRegistry {
private static final List<ModdedDataProvider> PROVIDERS = new CopyOnWriteArrayList<>();
private static final Map<String, BlockState> CUSTOM_BLOCKS = new ConcurrentHashMap<>();
private static volatile boolean scanned = false;
private static DiscoveryBatch discoveryBatch;
private ModdedCustomContentRegistry() {
}
public static void registerCustomBlockData(String namespace, String key, String state) {
public static synchronized void registerCustomBlockData(String namespace, String key, String state) {
if (namespace == null || key == null || state == null) {
return;
}
@@ -60,14 +63,24 @@ public final class ModdedCustomContentRegistry {
LOGGER.error("Iris custom block data '{}:{}' has unparseable state '{}'", namespace, key, state, error);
return;
}
CUSTOM_BLOCKS.put(identifier.toString(), parsed);
DiscoveryBatch activeBatch = discoveryBatch;
if (activeBatch == null) {
CUSTOM_BLOCKS.put(identifier.toString(), parsed);
} else {
activeBatch.customBlocks.put(identifier.toString(), parsed);
}
LOGGER.info("Iris registered custom block data {}:{} -> {}", namespace, key, state);
}
public static void register(ModdedDataProvider provider) {
public static synchronized void register(ModdedDataProvider provider) {
if (provider == null) {
return;
}
DiscoveryBatch activeBatch = discoveryBatch;
if (activeBatch != null) {
activeBatch.add(provider);
return;
}
for (ModdedDataProvider existing : PROVIDERS) {
if (existing.modId().equals(provider.modId())) {
LOGGER.warn("Iris custom content provider for '{}' already registered; ignoring duplicate", provider.modId());
@@ -83,17 +96,66 @@ public final class ModdedCustomContentRegistry {
LOGGER.info("Iris registered custom content provider '{}'", provider.modId());
}
public static void discover() {
public static synchronized Discovery discover() {
if (scanned) {
return;
return Discovery.unchanged();
}
scanned = true;
ServiceLoader<ModdedDataProvider> loader = ServiceLoader.load(ModdedDataProvider.class, ModdedCustomContentRegistry.class.getClassLoader());
for (ModdedDataProvider provider : loader) {
register(provider);
return discover(ServiceLoader.load(
ModdedDataProvider.class, ModdedCustomContentRegistry.class.getClassLoader()));
}
static synchronized Discovery discover(Iterable<ModdedDataProvider> discoveredProviders) {
List<ModdedDataProvider> previousProviders = List.copyOf(PROVIDERS);
Map<String, BlockState> previousCustomBlocks = new LinkedHashMap<>(CUSTOM_BLOCKS);
boolean previousDiscoveryComplete = scanned;
DiscoveryBatch batch = new DiscoveryBatch(previousProviders, previousCustomBlocks);
discoveryBatch = batch;
try {
for (ModdedDataProvider provider : discoveredProviders) {
batch.add(provider);
}
PROVIDERS.addAll(batch.additions);
CUSTOM_BLOCKS.putAll(batch.customBlocks);
scanned = true;
for (ModdedDataProvider provider : batch.additions) {
LOGGER.info("Iris registered custom content provider '{}'", provider.modId());
}
return new Discovery(previousProviders, previousCustomBlocks,
previousDiscoveryComplete, true);
} catch (Throwable failure) {
try {
restore(previousProviders, previousCustomBlocks, previousDiscoveryComplete);
} catch (Throwable rollbackFailure) {
if (rollbackFailure != failure) {
failure.addSuppressed(rollbackFailure);
}
}
LOGGER.error("Iris custom content provider discovery failed", failure);
if (failure instanceof RuntimeException runtimeException) {
throw runtimeException;
}
if (failure instanceof Error fatalError) {
throw fatalError;
}
throw new IllegalStateException("Iris custom content provider discovery failed", failure);
} finally {
discoveryBatch = null;
}
}
static synchronized boolean hasProvider(String modId) {
for (ModdedDataProvider provider : PROVIDERS) {
if (Objects.equals(provider.modId(), modId)) {
return true;
}
}
return false;
}
static boolean discoveryComplete() {
return scanned;
}
public static boolean hasProviders() {
return !PROVIDERS.isEmpty() || !CUSTOM_BLOCKS.isEmpty();
}
@@ -203,4 +265,68 @@ public final class ModdedCustomContentRegistry {
}
return state;
}
private static synchronized void restore(List<ModdedDataProvider> providers,
Map<String, BlockState> customBlocks,
boolean discoveryComplete) {
PROVIDERS.clear();
PROVIDERS.addAll(providers);
CUSTOM_BLOCKS.clear();
CUSTOM_BLOCKS.putAll(customBlocks);
scanned = discoveryComplete;
}
public static final class Discovery {
private final List<ModdedDataProvider> providers;
private final Map<String, BlockState> customBlocks;
private final boolean discoveryComplete;
private boolean active;
private Discovery(List<ModdedDataProvider> providers, Map<String, BlockState> customBlocks,
boolean discoveryComplete, boolean active) {
this.providers = providers;
this.customBlocks = customBlocks;
this.discoveryComplete = discoveryComplete;
this.active = active;
}
private static Discovery unchanged() {
return new Discovery(List.of(), Map.of(), true, false);
}
public synchronized void rollback() {
if (!active) {
return;
}
restore(providers, customBlocks, discoveryComplete);
active = false;
}
}
private static final class DiscoveryBatch {
private final List<ModdedDataProvider> providers;
private final List<ModdedDataProvider> additions = new ArrayList<>();
private final Map<String, BlockState> customBlocks;
private DiscoveryBatch(List<ModdedDataProvider> providers, Map<String, BlockState> customBlocks) {
this.providers = new ArrayList<>(providers);
this.customBlocks = new LinkedHashMap<>(customBlocks);
}
private void add(ModdedDataProvider provider) {
ModdedDataProvider candidate = Objects.requireNonNull(provider,
"Iris discovered a null custom content provider");
String modId = Objects.requireNonNull(candidate.modId(),
"Iris custom content provider returned a null mod id");
for (ModdedDataProvider existing : providers) {
if (modId.equals(existing.modId())) {
LOGGER.warn("Iris custom content provider for '{}' already registered; ignoring duplicate", modId);
return;
}
}
providers.add(candidate);
additions.add(candidate);
candidate.init();
}
}
}
@@ -25,9 +25,11 @@ import art.arcane.iris.core.pack.PackDownloader;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.Locator;
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
import art.arcane.iris.engine.framework.WrongEngineBroException;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisImportedStructureControl;
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
@@ -889,16 +891,27 @@ public final class IrisModdedCommands {
fail(source, "This command can only be used by players.");
return 0;
}
if (IrisStructureLocator.isPlaced(engine, key)) {
locateIrisStructure(source, level, engine, player, key);
return 1;
}
Optional<NativeStructureTarget> resolved = resolveNativeStructure(source, level, engine, key);
if (resolved.isEmpty()) {
if (IrisStructureLocator.isPlaced(engine, key)) {
locateIrisStructure(source, level, engine, player, key);
return 1;
}
fail(source, "Unknown structure '" + key + "'. Use tab completion to choose an Iris placement or a registered native/datapack structure.");
return 0;
}
NativeStructureTarget target = resolved.get();
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine, target.key(), false);
if (!decision.generate()
&& decision.status() != NativeStructureGenerationStatus.REPLACED_BY_IRIS) {
fail(source, NativeStructureGenerationPolicy.generationStatusMessage(
target.key(), decision.status()));
return 0;
}
if (decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS) {
locateIrisStructure(source, level, engine, player, target.key());
return 1;
}
if (target.availability() != NativeStructureAvailability.AVAILABLE) {
fail(source, nativeUnavailableMessage(target.key(), target.availability()));
return 0;
@@ -1035,7 +1048,7 @@ public final class IrisModdedCommands {
}
}
int irisPlaced = IrisStructureLocator.placedKeys(engine).size();
ok(source, "Structure reachability: " + available + " native locatable, " + irisPlaced
ok(source, "Structure reachability: " + available + " native generation-eligible, " + irisPlaced
+ " Iris-placed, " + disabled + " native disabled, " + suppressed
+ " native replaced by Iris placements, " + unreachableBiomes
+ " native excluded by this pack's biomes, and " + unsupported
@@ -1044,16 +1057,22 @@ public final class IrisModdedCommands {
}
private static int verifyStructure(CommandSourceStack source, ServerLevel level, Engine engine, String key) {
if (IrisStructureLocator.isPlaced(engine, key)) {
ok(source, "Structure " + key + " is Iris-placed and locatable with /iris goto structure " + key + ".");
return 1;
}
Optional<NativeStructureTarget> target = resolveNativeStructure(source, level, engine, key);
if (target.isEmpty()) {
if (IrisStructureLocator.isPlaced(engine, key)) {
ok(source, "Structure " + key + " is Iris-placed and locatable with /iris goto structure " + key + ".");
return 1;
}
fail(source, "Unknown structure '" + key + "'. It is neither Iris-placed nor registered by vanilla or a datapack.");
return 0;
}
NativeStructureTarget resolved = target.get();
if (resolved.availability() == NativeStructureAvailability.IRIS_SUPPRESSED) {
ok(source, "Structure " + resolved.key()
+ " is explicitly replaced by an Iris placement and locatable with /iris goto structure "
+ resolved.key() + ".");
return 1;
}
if (resolved.availability() != NativeStructureAvailability.AVAILABLE) {
fail(source, nativeUnavailableMessage(resolved.key(), resolved.availability()));
return 0;
@@ -1084,9 +1103,9 @@ public final class IrisModdedCommands {
Engine engine, String key,
Holder.Reference<Structure> holder) {
boolean worldEnabled = source.getServer().getWorldGenSettings().options().generateStructures();
IrisImportedStructureControl control = engine.getDimension().getImportedStructures();
boolean selected = control != null && control.active() && control.shouldGenerate(key);
boolean suppressed = IrisStructureLocator.suppressesVanilla(engine, key);
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine, key, false);
boolean selected = decision.status() != NativeStructureGenerationStatus.DISABLED_BY_PACK;
boolean suppressed = decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS;
ChunkGenerator chunkGenerator = level.getChunkSource().getGenerator();
boolean biomeReachable = chunkGenerator instanceof IrisModdedChunkGenerator irisGenerator
&& irisGenerator.isNativeStructureReachable(holder);
@@ -1121,8 +1140,10 @@ public final class IrisModdedCommands {
private static String nativeUnavailableMessage(String key, NativeStructureAvailability availability) {
return switch (availability) {
case WORLD_DISABLED -> "Native structure generation is disabled for this world, so " + key + " cannot generate or be located.";
case FILTERED -> "Native structure " + key + " is disabled by this dimension's importedStructures settings.";
case IRIS_SUPPRESSED -> "Native structure " + key + " is replaced by an Iris placement in this pack; locate the Iris key instead.";
case FILTERED -> NativeStructureGenerationPolicy.generationStatusMessage(
key, NativeStructureGenerationStatus.DISABLED_BY_PACK);
case IRIS_SUPPRESSED -> NativeStructureGenerationPolicy.generationStatusMessage(
key, NativeStructureGenerationStatus.REPLACED_BY_IRIS);
case BIOME_UNREACHABLE -> "Native structure " + key + " cannot generate because none of its required biomes are produced by this Iris pack.";
case NO_PLACEMENT -> "Native structure " + key + " is registered, but its structure set has no placement supported by this dimension's generator state.";
case AVAILABLE -> "Native structure " + key + " is available.";
@@ -109,6 +109,7 @@ final class ModdedCommandHelp {
Entry.command("x&y", "", "Autoselect up, down and out", "xay"),
Entry.command("analyze", "<key>", "Show object composition"),
Entry.command("shrink", "<key>", "Shrink an object to its minimum size"),
Entry.command("plausibilize", "<key|prefix/> [dryrun=true] [reach=N]", "Grow branches so tree leaves survive vanilla decay"),
Entry.command("undo", "[amount]", "Undo pasted objects", "u")
));
SECTIONS.put("o", SECTIONS.get("object"));
@@ -19,6 +19,8 @@
package art.arcane.iris.modded.command;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.tools.TreePlausibilizeBatch;
import art.arcane.iris.core.tools.TreePlausibilizer;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisObjectPlacement;
@@ -27,6 +29,7 @@ import art.arcane.iris.engine.object.TileData;
import art.arcane.iris.modded.ModdedBlockState;
import art.arcane.iris.modded.ModdedTileData;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.math.RNG;
import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.arguments.StringArgumentType;
@@ -43,6 +46,7 @@ import net.minecraft.core.HolderLookup;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtUtils;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.block.Blocks;
@@ -157,7 +161,9 @@ public final class ModdedObjectCommands {
root.then(bukkitOnly("we", "WorldEdit selection import requires the Bukkit plugin with WorldEdit installed."));
root.then(bukkitOnly("studio", "The object studio world requires the Bukkit studio toolchain; it is not available on modded servers."));
root.then(bukkitOnly("convert", "Schematic conversion (.schem -> .iob) requires the Bukkit plugin."));
root.then(bukkitOnly("plausibilize", "Tree plausibilization requires the Bukkit plugin (its block-data tooling is not ported to modded yet)."));
root.then(Commands.literal("plausibilize")
.then(Commands.argument("args", StringArgumentType.greedyString()).suggests(OBJECT_KEYS)
.executes((CommandContext<CommandSourceStack> context) -> plausibilize(context.getSource(), StringArgumentType.getString(context, "args")))));
return root;
}
@@ -645,6 +651,48 @@ public final class ModdedObjectCommands {
return 1;
}
private static int plausibilize(CommandSourceStack source, String raw) {
Engine engine = IrisModdedCommands.engineFor(source.getLevel());
IrisData data = engine == null ? null : engine.getData();
boolean dryRun = false;
int reach = TreePlausibilizer.DEFAULT_REACH;
StringBuilder targetBuilder = new StringBuilder();
for (String token : raw.trim().split("\\s+")) {
String lower = token.toLowerCase(Locale.ROOT);
if (lower.startsWith("dryrun=")) {
dryRun = Boolean.parseBoolean(lower.substring("dryrun=".length()));
continue;
}
if (lower.startsWith("reach=")) {
try {
reach = Integer.parseInt(lower.substring("reach=".length()));
} catch (NumberFormatException e) {
IrisModdedCommands.fail(source, "Invalid reach: " + token);
return 0;
}
continue;
}
if (!targetBuilder.isEmpty()) {
targetBuilder.append(' ');
}
targetBuilder.append(token);
}
String target = targetBuilder.toString();
List<TreePlausibilizeBatch.Target> targets = TreePlausibilizeBatch.resolve(target, data);
if (targets.isEmpty()) {
IrisModdedCommands.fail(source, "No objects matched: " + target);
return 0;
}
IrisModdedCommands.ok(source, "Plausibilize [reach=" + reach + (dryRun ? ", DRY" : "")
+ "] queued " + targets.size() + " object(s)");
boolean dry = dryRun;
int reachFinal = reach;
MinecraftServer server = source.getServer();
J.a(() -> TreePlausibilizeBatch.run(targets, dry, reachFinal, data, (String line) ->
server.execute(() -> IrisModdedCommands.ok(source, line))));
return 1;
}
private static int undo(CommandSourceStack source, int amount) {
ServerPlayer player = source.getPlayer();
UUID owner = player == null ? ModdedObjectUndo.CONSOLE : player.getUUID();
@@ -24,6 +24,7 @@ import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.PlacedStructurePiece;
import art.arcane.iris.engine.framework.StructureAssembler;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.iris.engine.object.IrisStructure;
import art.arcane.iris.engine.object.ObjectPlaceMode;
import art.arcane.volmlib.util.collection.KList;
@@ -137,12 +138,13 @@ public final class ModdedStructureCommands {
return 0;
}
String key = keyRaw.trim();
IrisStructure structure = IrisData.loadAnyStructure(key, data);
IrisStructure structure = data.load(IrisStructure.class, key, false);
if (structure == null) {
IrisModdedCommands.fail(source, "No iris structure '" + key + "' in this pack");
return 0;
}
StructureAssembler assembler = new StructureAssembler(data, structure, 0, 64, 0);
StructureAssembler assembler = StructureAssembler.forData(
data, structure, new IrisPosition(0, 64, 0));
KList<PlacedStructurePiece> pieces = assembler.assemble(new RNG(1234));
if (pieces == null || pieces.isEmpty()) {
IrisModdedCommands.fail(source, "Structure '" + key + "' assembled 0 pieces (check startPool '" + structure.getStartPool() + "')");
@@ -176,7 +178,7 @@ public final class ModdedStructureCommands {
return 0;
}
String key = keyRaw.trim();
IrisStructure structure = IrisData.loadAnyStructure(key, data);
IrisStructure structure = data.load(IrisStructure.class, key, false);
if (structure == null) {
IrisModdedCommands.fail(source, "No iris structure '" + key + "' in this pack");
return 0;
@@ -184,7 +186,8 @@ public final class ModdedStructureCommands {
int originX = player.blockPosition().getX();
int originY = player.blockPosition().getY();
int originZ = player.blockPosition().getZ();
StructureAssembler assembler = new StructureAssembler(data, structure, originX, originY, originZ);
StructureAssembler assembler = StructureAssembler.forData(
data, structure, new IrisPosition(originX, originY, originZ));
RNG rng = new RNG((long) originX * 341873128712L + originZ);
KList<PlacedStructurePiece> pieces = assembler.assemble(rng);
if (pieces == null || pieces.isEmpty()) {
Loaded 100 of 327 files, more files were not shown because too many files have changed in this diff. Show more